From 3a378b4d29ca69443e03629473b5f28f0dc98c3f Mon Sep 17 00:00:00 2001 From: Pablo Takara Date: Fri, 10 May 2024 11:46:26 +0200 Subject: [PATCH 01/48] chore: Bump versions to `1.4.0` --- common/pom.xml | 2 +- .../docker-compose-ingestion/docker-compose-ingestion.yml | 2 +- .../docker-compose-openmetadata.yml | 4 ++-- docker/docker-compose-quickstart/Dockerfile | 4 ++-- .../docker-compose-quickstart/docker-compose-postgres.yml | 8 ++++---- docker/docker-compose-quickstart/docker-compose.yml | 8 ++++---- ingestion/Dockerfile | 2 +- ingestion/operators/docker/Dockerfile | 2 +- ingestion/pyproject.toml | 2 +- openmetadata-airflow-apis/pyproject.toml | 2 +- openmetadata-clients/openmetadata-java-client/pom.xml | 2 +- openmetadata-clients/pom.xml | 2 +- openmetadata-dist/pom.xml | 2 +- openmetadata-service/pom.xml | 2 +- openmetadata-shaded-deps/elasticsearch-dep/pom.xml | 2 +- openmetadata-shaded-deps/opensearch-dep/pom.xml | 2 +- openmetadata-shaded-deps/pom.xml | 2 +- openmetadata-spec/pom.xml | 2 +- openmetadata-ui/pom.xml | 2 +- pom.xml | 2 +- 20 files changed, 28 insertions(+), 28 deletions(-) diff --git a/common/pom.xml b/common/pom.xml index 42f9bd40227f..2b9b00cd738b 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -18,7 +18,7 @@ platform org.open-metadata - 1.4.0-SNAPSHOT + 1.4.0 4.0.0 diff --git a/docker/docker-compose-ingestion/docker-compose-ingestion.yml b/docker/docker-compose-ingestion/docker-compose-ingestion.yml index 8537698e7d51..f555453dcb7f 100644 --- a/docker/docker-compose-ingestion/docker-compose-ingestion.yml +++ b/docker/docker-compose-ingestion/docker-compose-ingestion.yml @@ -18,7 +18,7 @@ volumes: services: ingestion: container_name: openmetadata_ingestion - image: docker.getcollate.io/openmetadata/ingestion:1.4.0-SNAPSHOT + image: docker.getcollate.io/openmetadata/ingestion:1.4.0 environment: AIRFLOW__API__AUTH_BACKENDS: "airflow.api.auth.backend.basic_auth,airflow.api.auth.backend.session" AIRFLOW__CORE__EXECUTOR: LocalExecutor diff --git a/docker/docker-compose-openmetadata/docker-compose-openmetadata.yml b/docker/docker-compose-openmetadata/docker-compose-openmetadata.yml index 7083ed1017e1..a2a14a0f11ae 100644 --- a/docker/docker-compose-openmetadata/docker-compose-openmetadata.yml +++ b/docker/docker-compose-openmetadata/docker-compose-openmetadata.yml @@ -14,7 +14,7 @@ services: execute-migrate-all: container_name: execute_migrate_all command: "./bootstrap/openmetadata-ops.sh migrate" - image: docker.getcollate.io/openmetadata/server:1.4.0-SNAPSHOT + image: docker.getcollate.io/openmetadata/server:1.4.0 environment: OPENMETADATA_CLUSTER_NAME: ${OPENMETADATA_CLUSTER_NAME:-openmetadata} SERVER_PORT: ${SERVER_PORT:-8585} @@ -223,7 +223,7 @@ services: openmetadata-server: container_name: openmetadata_server restart: always - image: docker.getcollate.io/openmetadata/server:1.4.0-SNAPSHOT + image: docker.getcollate.io/openmetadata/server:1.4.0 environment: OPENMETADATA_CLUSTER_NAME: ${OPENMETADATA_CLUSTER_NAME:-openmetadata} SERVER_PORT: ${SERVER_PORT:-8585} diff --git a/docker/docker-compose-quickstart/Dockerfile b/docker/docker-compose-quickstart/Dockerfile index 72b2064227e7..01b2bd126c45 100644 --- a/docker/docker-compose-quickstart/Dockerfile +++ b/docker/docker-compose-quickstart/Dockerfile @@ -11,7 +11,7 @@ # Build stage FROM alpine:3.19 AS build -ARG RI_VERSION="1.3.0-SNAPSHOT" +ARG RI_VERSION="1.4.0" ENV RELEASE_URL="https://github.com/open-metadata/OpenMetadata/releases/download/${RI_VERSION}-release/openmetadata-${RI_VERSION}.tar.gz" RUN mkdir -p /opt/openmetadata && \ @@ -21,7 +21,7 @@ RUN mkdir -p /opt/openmetadata && \ # Final stage FROM alpine:3.19 -ARG RI_VERSION="1.3.0-SNAPSHOT" +ARG RI_VERSION="1.4.0" ARG BUILD_DATE ARG COMMIT_ID LABEL maintainer="OpenMetadata" diff --git a/docker/docker-compose-quickstart/docker-compose-postgres.yml b/docker/docker-compose-quickstart/docker-compose-postgres.yml index 995104590641..0f0242da7fe6 100644 --- a/docker/docker-compose-quickstart/docker-compose-postgres.yml +++ b/docker/docker-compose-quickstart/docker-compose-postgres.yml @@ -18,7 +18,7 @@ volumes: services: postgresql: container_name: openmetadata_postgresql - image: docker.getcollate.io/openmetadata/postgresql:1.4.0-SNAPSHOT + image: docker.getcollate.io/openmetadata/postgresql:1.4.0 restart: always command: "--work_mem=10MB" environment: @@ -61,7 +61,7 @@ services: execute-migrate-all: container_name: execute_migrate_all - image: docker.getcollate.io/openmetadata/server:1.4.0-SNAPSHOT + image: docker.getcollate.io/openmetadata/server:1.4.0 command: "./bootstrap/openmetadata-ops.sh migrate" environment: OPENMETADATA_CLUSTER_NAME: ${OPENMETADATA_CLUSTER_NAME:-openmetadata} @@ -271,7 +271,7 @@ services: openmetadata-server: container_name: openmetadata_server restart: always - image: docker.getcollate.io/openmetadata/server:1.4.0-SNAPSHOT + image: docker.getcollate.io/openmetadata/server:1.4.0 environment: OPENMETADATA_CLUSTER_NAME: ${OPENMETADATA_CLUSTER_NAME:-openmetadata} SERVER_PORT: ${SERVER_PORT:-8585} @@ -476,7 +476,7 @@ services: ingestion: container_name: openmetadata_ingestion - image: docker.getcollate.io/openmetadata/ingestion:1.4.0-SNAPSHOT + image: docker.getcollate.io/openmetadata/ingestion:1.4.0 depends_on: elasticsearch: condition: service_started diff --git a/docker/docker-compose-quickstart/docker-compose.yml b/docker/docker-compose-quickstart/docker-compose.yml index 38aa0c9431bb..8aadedf986f4 100644 --- a/docker/docker-compose-quickstart/docker-compose.yml +++ b/docker/docker-compose-quickstart/docker-compose.yml @@ -18,7 +18,7 @@ volumes: services: mysql: container_name: openmetadata_mysql - image: docker.getcollate.io/openmetadata/db:1.4.0-SNAPSHOT + image: docker.getcollate.io/openmetadata/db:1.4.0 command: "--sort_buffer_size=10M" restart: always environment: @@ -59,7 +59,7 @@ services: execute-migrate-all: container_name: execute_migrate_all - image: docker.getcollate.io/openmetadata/server:1.4.0-SNAPSHOT + image: docker.getcollate.io/openmetadata/server:1.4.0 command: "./bootstrap/openmetadata-ops.sh migrate" environment: OPENMETADATA_CLUSTER_NAME: ${OPENMETADATA_CLUSTER_NAME:-openmetadata} @@ -269,7 +269,7 @@ services: openmetadata-server: container_name: openmetadata_server restart: always - image: docker.getcollate.io/openmetadata/server:1.4.0-SNAPSHOT + image: docker.getcollate.io/openmetadata/server:1.4.0 environment: OPENMETADATA_CLUSTER_NAME: ${OPENMETADATA_CLUSTER_NAME:-openmetadata} SERVER_PORT: ${SERVER_PORT:-8585} @@ -474,7 +474,7 @@ services: ingestion: container_name: openmetadata_ingestion - image: docker.getcollate.io/openmetadata/ingestion:1.4.0-SNAPSHOT + image: docker.getcollate.io/openmetadata/ingestion:1.4.0 depends_on: elasticsearch: condition: service_started diff --git a/ingestion/Dockerfile b/ingestion/Dockerfile index 87e3eaf61410..ffbf5a2606d5 100644 --- a/ingestion/Dockerfile +++ b/ingestion/Dockerfile @@ -84,7 +84,7 @@ ARG INGESTION_DEPENDENCY="all" ENV PIP_NO_CACHE_DIR=1 # Make pip silent ENV PIP_QUIET=1 -ARG RI_VERSION="1.3.0.0.dev0" +ARG RI_VERSION="1.4.0.0" RUN pip install --upgrade pip RUN pip install "openmetadata-managed-apis~=${RI_VERSION}" --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-2.7.3/constraints-3.10.txt" RUN pip install "openmetadata-ingestion[${INGESTION_DEPENDENCY}]~=${RI_VERSION}" diff --git a/ingestion/operators/docker/Dockerfile b/ingestion/operators/docker/Dockerfile index 2221394cecf3..05766efc4720 100644 --- a/ingestion/operators/docker/Dockerfile +++ b/ingestion/operators/docker/Dockerfile @@ -87,7 +87,7 @@ ENV PIP_QUIET=1 RUN pip install --upgrade pip ARG INGESTION_DEPENDENCY="all" -ARG RI_VERSION="1.3.0.0.dev0" +ARG RI_VERSION="1.4.0.0" RUN pip install --upgrade pip RUN pip install "openmetadata-ingestion[airflow]~=${RI_VERSION}" RUN pip install "openmetadata-ingestion[${INGESTION_DEPENDENCY}]~=${RI_VERSION}" diff --git a/ingestion/pyproject.toml b/ingestion/pyproject.toml index e888255fb1f8..9c7e372e8d8f 100644 --- a/ingestion/pyproject.toml +++ b/ingestion/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" # since it helps us organize and isolate version management [project] name = "openmetadata-ingestion" -version = "1.4.0.0.dev0" +version = "1.4.0.0" dynamic = ["readme", "dependencies", "optional-dependencies"] authors = [ {name = "OpenMetadata Committers"} diff --git a/openmetadata-airflow-apis/pyproject.toml b/openmetadata-airflow-apis/pyproject.toml index 38871258438a..1164595a2e04 100644 --- a/openmetadata-airflow-apis/pyproject.toml +++ b/openmetadata-airflow-apis/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" # since it helps us organize and isolate version management [project] name = "openmetadata_managed_apis" -version = "1.4.0.0.dev0" +version = "1.4.0.0" readme = "README.md" authors = [ {name = "OpenMetadata Committers"} diff --git a/openmetadata-clients/openmetadata-java-client/pom.xml b/openmetadata-clients/openmetadata-java-client/pom.xml index 112fd0063264..bc4287e0ce03 100644 --- a/openmetadata-clients/openmetadata-java-client/pom.xml +++ b/openmetadata-clients/openmetadata-java-client/pom.xml @@ -5,7 +5,7 @@ openmetadata-clients org.open-metadata - 1.4.0-SNAPSHOT + 1.4.0 4.0.0 diff --git a/openmetadata-clients/pom.xml b/openmetadata-clients/pom.xml index a00e9e611994..ffac7f8db750 100644 --- a/openmetadata-clients/pom.xml +++ b/openmetadata-clients/pom.xml @@ -5,7 +5,7 @@ platform org.open-metadata - 1.4.0-SNAPSHOT + 1.4.0 4.0.0 diff --git a/openmetadata-dist/pom.xml b/openmetadata-dist/pom.xml index e027cbe69863..161bc5ea5b46 100644 --- a/openmetadata-dist/pom.xml +++ b/openmetadata-dist/pom.xml @@ -20,7 +20,7 @@ platform org.open-metadata - 1.4.0-SNAPSHOT + 1.4.0 openmetadata-dist diff --git a/openmetadata-service/pom.xml b/openmetadata-service/pom.xml index d097b39b2572..022761ed363f 100644 --- a/openmetadata-service/pom.xml +++ b/openmetadata-service/pom.xml @@ -5,7 +5,7 @@ platform org.open-metadata - 1.4.0-SNAPSHOT + 1.4.0 4.0.0 openmetadata-service diff --git a/openmetadata-shaded-deps/elasticsearch-dep/pom.xml b/openmetadata-shaded-deps/elasticsearch-dep/pom.xml index 6beda6e056e3..908acfd7c462 100644 --- a/openmetadata-shaded-deps/elasticsearch-dep/pom.xml +++ b/openmetadata-shaded-deps/elasticsearch-dep/pom.xml @@ -5,7 +5,7 @@ openmetadata-shaded-deps org.open-metadata - 1.4.0-SNAPSHOT + 1.4.0 4.0.0 elasticsearch-deps diff --git a/openmetadata-shaded-deps/opensearch-dep/pom.xml b/openmetadata-shaded-deps/opensearch-dep/pom.xml index 0e6cc2ae231f..9636507b1f28 100644 --- a/openmetadata-shaded-deps/opensearch-dep/pom.xml +++ b/openmetadata-shaded-deps/opensearch-dep/pom.xml @@ -5,7 +5,7 @@ openmetadata-shaded-deps org.open-metadata - 1.4.0-SNAPSHOT + 1.4.0 4.0.0 opensearch-deps diff --git a/openmetadata-shaded-deps/pom.xml b/openmetadata-shaded-deps/pom.xml index fc21ef779683..1184ec7a3968 100644 --- a/openmetadata-shaded-deps/pom.xml +++ b/openmetadata-shaded-deps/pom.xml @@ -5,7 +5,7 @@ platform org.open-metadata - 1.4.0-SNAPSHOT + 1.4.0 4.0.0 openmetadata-shaded-deps diff --git a/openmetadata-spec/pom.xml b/openmetadata-spec/pom.xml index be3cf6c811b8..ae31d76a0d85 100644 --- a/openmetadata-spec/pom.xml +++ b/openmetadata-spec/pom.xml @@ -5,7 +5,7 @@ platform org.open-metadata - 1.4.0-SNAPSHOT + 1.4.0 4.0.0 diff --git a/openmetadata-ui/pom.xml b/openmetadata-ui/pom.xml index c6dc101d7bd2..509ea6f7731e 100644 --- a/openmetadata-ui/pom.xml +++ b/openmetadata-ui/pom.xml @@ -5,7 +5,7 @@ platform org.open-metadata - 1.4.0-SNAPSHOT + 1.4.0 4.0.0 diff --git a/pom.xml b/pom.xml index 496c79328178..34ed620a68dc 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ based on Open Metadata Standards/APIs, supporting connectors to a wide range of data services, OpenMetadata enables end-to-end metadata management, giving you the freedom to unlock the value of your data assets. - 1.4.0-SNAPSHOT + 1.4.0 https://github.com/open-metadata/OpenMetadata openmetadata-spec From 23db85038e9bcf5e93a8556c8b1bd2afd092a87f Mon Sep 17 00:00:00 2001 From: Akash-Jain <15995028+akash-jain-10@users.noreply.github.com> Date: Wed, 22 May 2024 19:12:23 +0530 Subject: [PATCH 02/48] chore: Bump Ingestion Versions to `1.4.0.1` for Release --- ingestion/pyproject.toml | 2 +- openmetadata-airflow-apis/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ingestion/pyproject.toml b/ingestion/pyproject.toml index 9c7e372e8d8f..4f877881adb0 100644 --- a/ingestion/pyproject.toml +++ b/ingestion/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" # since it helps us organize and isolate version management [project] name = "openmetadata-ingestion" -version = "1.4.0.0" +version = "1.4.0.1" dynamic = ["readme", "dependencies", "optional-dependencies"] authors = [ {name = "OpenMetadata Committers"} diff --git a/openmetadata-airflow-apis/pyproject.toml b/openmetadata-airflow-apis/pyproject.toml index 1164595a2e04..3fea74718ca4 100644 --- a/openmetadata-airflow-apis/pyproject.toml +++ b/openmetadata-airflow-apis/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" # since it helps us organize and isolate version management [project] name = "openmetadata_managed_apis" -version = "1.4.0.0" +version = "1.4.0.1" readme = "README.md" authors = [ {name = "OpenMetadata Committers"} From 8f9d9e414881d6fcfa447b22c4a594ebe784f26e Mon Sep 17 00:00:00 2001 From: Akash-Jain <15995028+akash-jain-10@users.noreply.github.com> Date: Wed, 22 May 2024 19:14:52 +0530 Subject: [PATCH 03/48] chore: Bump Ingestion Versions to `1.4.0.1` in Dockerfiles for Release --- ingestion/Dockerfile | 2 +- ingestion/operators/docker/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ingestion/Dockerfile b/ingestion/Dockerfile index ffbf5a2606d5..685dc823f738 100644 --- a/ingestion/Dockerfile +++ b/ingestion/Dockerfile @@ -84,7 +84,7 @@ ARG INGESTION_DEPENDENCY="all" ENV PIP_NO_CACHE_DIR=1 # Make pip silent ENV PIP_QUIET=1 -ARG RI_VERSION="1.4.0.0" +ARG RI_VERSION="1.4.0.1" RUN pip install --upgrade pip RUN pip install "openmetadata-managed-apis~=${RI_VERSION}" --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-2.7.3/constraints-3.10.txt" RUN pip install "openmetadata-ingestion[${INGESTION_DEPENDENCY}]~=${RI_VERSION}" diff --git a/ingestion/operators/docker/Dockerfile b/ingestion/operators/docker/Dockerfile index 05766efc4720..f9670e976eaa 100644 --- a/ingestion/operators/docker/Dockerfile +++ b/ingestion/operators/docker/Dockerfile @@ -87,7 +87,7 @@ ENV PIP_QUIET=1 RUN pip install --upgrade pip ARG INGESTION_DEPENDENCY="all" -ARG RI_VERSION="1.4.0.0" +ARG RI_VERSION="1.4.0.1" RUN pip install --upgrade pip RUN pip install "openmetadata-ingestion[airflow]~=${RI_VERSION}" RUN pip install "openmetadata-ingestion[${INGESTION_DEPENDENCY}]~=${RI_VERSION}" From 11c15422fd766eb9ad891f178f9bbfa14eb9860f Mon Sep 17 00:00:00 2001 From: Mohit Yadav <105265192+mohityadav766@users.noreply.github.com> Date: Fri, 24 May 2024 13:07:26 +0530 Subject: [PATCH 04/48] Remove Retry From Abstract Event Consumer (#16405) (cherry picked from commit f8ed079731cc238dc136306fe018c5df35dd2f3b) --- .../changeEvent/AbstractEventConsumer.java | 84 +++---------------- 1 file changed, 11 insertions(+), 73 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/changeEvent/AbstractEventConsumer.java b/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/changeEvent/AbstractEventConsumer.java index b74b981436b6..be0f5039a155 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/changeEvent/AbstractEventConsumer.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/changeEvent/AbstractEventConsumer.java @@ -16,17 +16,14 @@ import static org.openmetadata.service.events.subscription.AlertUtil.getFilteredEvents; import static org.openmetadata.service.events.subscription.AlertUtil.getStartingOffset; -import com.fasterxml.jackson.core.type.TypeReference; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.UUID; -import java.util.stream.Collectors; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -91,62 +88,17 @@ public void handleFailedEvent(EventPublisherException ex) { failingSubscriptionId, changeEvent); - // Update Failed Event with details - FailedEvent failedEvent = - new FailedEvent() - .withFailingSubscriptionId(failingSubscriptionId) - .withChangeEvent(changeEvent) - .withRetriesLeft(eventSubscription.getRetries()) - .withTimestamp(System.currentTimeMillis()); - - if (eventSubscription.getRetries() == 0) { - Entity.getCollectionDAO() - .eventSubscriptionDAO() - .upsertFailedEvent( - eventSubscription.getId().toString(), - String.format("%s-%s", FAILED_EVENT_EXTENSION, changeEvent.getId()), - JsonUtils.pojoToJson(failedEvent)); - } else { - // Check in Qtz Map - Set failedEventsList = - JsonUtils.convertValue( - jobDetail.getJobDataMap().get(FAILED_EVENT_EXTENSION), new TypeReference<>() {}); - if (failedEventsList == null) { - failedEventsList = new HashSet<>(); - } else { - // Remove exising change event - boolean removeChangeEvent = - failedEventsList.removeIf( - failedEvent1 -> { - if (failedEvent1 - .getChangeEvent() - .getId() - .equals(failedEvent.getChangeEvent().getId()) - && failedEvent1.getFailingSubscriptionId().equals(failingSubscriptionId)) { - failedEvent.withRetriesLeft(failedEvent1.getRetriesLeft()); - return true; - } - return false; - }); - - if (removeChangeEvent) { - if (failedEvent.getRetriesLeft() == 0) { - // If the Retries are exhausted, then remove the Event from the List to DLQ - failedEvent.withRetriesLeft(0); - } else { - failedEvent.withRetriesLeft(failedEvent.getRetriesLeft() - 1); - } - } - } - failedEventsList.add(failedEvent); - jobDetail.getJobDataMap().put(FAILED_EVENT_EXTENSION, failedEventsList); - Entity.getCollectionDAO() - .eventSubscriptionDAO() - .upsertFailedEvent( - eventSubscription.getId().toString(), - String.format("%s-%s", FAILED_EVENT_EXTENSION, changeEvent.getId()), - JsonUtils.pojoToJson(failedEvent)); - } + Entity.getCollectionDAO() + .eventSubscriptionDAO() + .upsertFailedEvent( + eventSubscription.getId().toString(), + String.format("%s-%s", FAILED_EVENT_EXTENSION, changeEvent.getId()), + JsonUtils.pojoToJson( + new FailedEvent() + .withFailingSubscriptionId(failingSubscriptionId) + .withChangeEvent(changeEvent) + .withRetriesLeft(eventSubscription.getRetries()) + .withTimestamp(System.currentTimeMillis()))); } private long loadInitialOffset(JobExecutionContext context) { @@ -278,20 +230,6 @@ public void execute(JobExecutionContext jobExecutionContext) throws JobExecution int batchSize = batch.size(); Map> eventsWithReceivers = createEventsWithReceivers(batch); try { - // Retry Failed Events - Set failedEventsList = - JsonUtils.convertValue( - jobDetail.getJobDataMap().get(FAILED_EVENT_EXTENSION), new TypeReference<>() {}); - if (failedEventsList != null) { - Map> failedChangeEvents = - failedEventsList.stream() - .filter(failedEvent -> failedEvent.getRetriesLeft() > 0) - .collect( - Collectors.toMap( - FailedEvent::getChangeEvent, - failedEvent -> Set.of(failedEvent.getFailingSubscriptionId()))); - eventsWithReceivers.putAll(failedChangeEvents); - } // Publish Events if (!eventsWithReceivers.isEmpty()) { alertMetrics.withTotalEvents(alertMetrics.getTotalEvents() + eventsWithReceivers.size()); From 8265627686c4cf51724fefe53813fd12d08cff16 Mon Sep 17 00:00:00 2001 From: Ayush Shah Date: Fri, 24 May 2024 14:31:41 +0530 Subject: [PATCH 05/48] Fix Migrations: Add postgres migrations (#16403) (cherry picked from commit 9416a7ac5fa8fd9695063b108501790d813e8e6e) --- .../native/1.4.0/mysql/schemaChanges.sql | 9 ++++- .../native/1.4.0/postgres/schemaChanges.sql | 40 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/bootstrap/sql/migrations/native/1.4.0/mysql/schemaChanges.sql b/bootstrap/sql/migrations/native/1.4.0/mysql/schemaChanges.sql index 08ffeb4687ee..d5ea3d58d888 100644 --- a/bootstrap/sql/migrations/native/1.4.0/mysql/schemaChanges.sql +++ b/bootstrap/sql/migrations/native/1.4.0/mysql/schemaChanges.sql @@ -277,7 +277,14 @@ where serviceType = 'Airflow' AND JSON_EXTRACT(json, '$.connection.config.connection.type') = 'Mysql' AND JSON_EXTRACT(json, '$.connection.config.connection.sslCA') IS NOT NULL; - +UPDATE pipeline_service_entity +SET json = JSON_INSERT( +JSON_REMOVE(json, '$.connection.config.connection.sslConfig.certificatePath'), +'$.connection.config.connection.sslConfig.caCertificate', +JSON_EXTRACT(json, '$.connection.config.connection.sslConfig.certificatePath')) +where serviceType = 'Airflow' + AND JSON_EXTRACT(json, '$.connection.config.connection.type') = 'Postgres' + AND JSON_EXTRACT(json, '$.connection.config.connection.sslConfig.certificatePath') IS NOT NULL; UPDATE pipeline_service_entity SET json = JSON_INSERT( diff --git a/bootstrap/sql/migrations/native/1.4.0/postgres/schemaChanges.sql b/bootstrap/sql/migrations/native/1.4.0/postgres/schemaChanges.sql index facae02e0f9a..366e3a1eeb99 100644 --- a/bootstrap/sql/migrations/native/1.4.0/postgres/schemaChanges.sql +++ b/bootstrap/sql/migrations/native/1.4.0/postgres/schemaChanges.sql @@ -141,6 +141,8 @@ SET json = jsonb_set( ) WHERE serviceType IN ('Mysql', 'Doris') AND json#>'{connection,config,sslKey}' IS NOT NULL; + + UPDATE dbservice_entity SET json = jsonb_set( json #-'{connection,config,metastoreConnection,sslCert}', @@ -189,6 +191,44 @@ SET json = jsonb_set( ) WHERE serviceType IN ('Superset') AND json#>'{connection,config,connection,type}' = '"Postgres"' AND json#>'{connection,config,connection,sslConfig,certificatePath}' IS NOT NULL; + +UPDATE pipeline_service_entity +SET json = jsonb_set( + json #-'{connection,config,connection,sslConfig,certificatePath}', + '{connection,config,connection,sslConfig}', + jsonb_build_object('caCertificate', json#>'{connection,config,connection,sslConfig,certificatePath}') +) +WHERE serviceType IN ('Airflow') AND json#>'{connection,config,connection,type}' = '"Postgres"' AND json#>'{connection,config,connection,sslConfig,certificatePath}' IS NOT NULL; + +UPDATE dashboard_service_entity +SET json = jsonb_set( + json #-'{connection,config,certificates,rootCertificateData}', + '{connection,config,certificates,sslConfig}', + jsonb_build_object('caCertificate', json#>'{connection,config,certificates,rootCertificateData}') +) +WHERE serviceType IN ('QlikSense') AND json#>'{connection,config,certificates,rootCertificateData}' IS NOT NULL; + +UPDATE dashboard_service_entity +SET json = jsonb_set( + json #-'{connection,config,certificates,clientCertificateData}', + '{connection,config,certificates,sslConfig}', + json#>'{connection,config,certificates,sslConfig}' || jsonb_build_object('sslCertificate', json#>'{connection,config,certificates,clientCertificateData}') +) +WHERE serviceType IN ('QlikSense') AND json#>'{connection,config,certificates,clientCertificateData}' IS NOT NULL; + +UPDATE dashboard_service_entity +SET json = jsonb_set( + json #-'{connection,config,certificates,clientKeyCertificateData}', + '{connection,config,certificates,sslConfig}', + json#>'{connection,config,certificates,sslConfig}' || jsonb_build_object('sslKey', json#>'{connection,config,certificates,clientKeyCertificateData}') +) +WHERE serviceType IN ('QlikSense') AND json#>'{connection,config,certificates,clientKeyCertificateData}' IS NOT NULL; + + +update dashboard_service_entity +set json = json #-'{connection,config,certificates,stagingDir}' +WHERE serviceType IN ('QlikSense') AND json#>'{connection,config,certificates,stagingDir}' IS NOT NULL; + UPDATE dashboard_service_entity SET json = jsonb_set( json #-'{connection,config,connection,sslCert}', From 094383e22b6be270c88b974554fb413e8a342fb2 Mon Sep 17 00:00:00 2001 From: Mohit Yadav <105265192+mohityadav766@users.noreply.github.com> Date: Fri, 24 May 2024 15:58:41 +0530 Subject: [PATCH 06/48] Add Null Check for isAdmin (#16407) * Remove Retry From Abstract Event Consumer * - Add Check for null Or Empty in isAdmin * - Fix Test (cherry picked from commit fe2db2d63c5495b6c288d4252a19ab77481b6de0) --- .../security/auth/AuthenticatorHandler.java | 3 ++- .../security/auth/BasicAuthenticator.java | 3 ++- .../saml/SamlAssertionConsumerServlet.java | 3 ++- .../resources/teams/UserResourceTest.java | 17 +++++++++-------- .../json/schema/entity/teams/user.json | 3 ++- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/security/auth/AuthenticatorHandler.java b/openmetadata-service/src/main/java/org/openmetadata/service/security/auth/AuthenticatorHandler.java index 37830e82c0cd..b514b264e68a 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/security/auth/AuthenticatorHandler.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/security/auth/AuthenticatorHandler.java @@ -1,5 +1,6 @@ package org.openmetadata.service.security.auth; +import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty; import static org.openmetadata.service.exception.CatalogExceptionMessage.NOT_IMPLEMENTED_METHOD; import static org.openmetadata.service.util.UserUtil.getRoleListFromUser; @@ -106,7 +107,7 @@ default JwtResponse getJwtResponse(User storedUser, long expireInSeconds) { .generateJWTToken( storedUser.getName(), getRoleListFromUser(storedUser), - storedUser.getIsAdmin(), + !nullOrEmpty(storedUser.getIsAdmin()) && storedUser.getIsAdmin(), storedUser.getEmail(), expireInSeconds, false, diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/security/auth/BasicAuthenticator.java b/openmetadata-service/src/main/java/org/openmetadata/service/security/auth/BasicAuthenticator.java index 2e17253977eb..bc535ba9f26c 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/security/auth/BasicAuthenticator.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/security/auth/BasicAuthenticator.java @@ -16,6 +16,7 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; import static javax.ws.rs.core.Response.Status.NOT_IMPLEMENTED; +import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty; import static org.openmetadata.schema.api.teams.CreateUser.CreatePasswordType.ADMIN_CREATE; import static org.openmetadata.schema.auth.ChangePasswordRequest.RequestType.SELF; import static org.openmetadata.schema.auth.ChangePasswordRequest.RequestType.USER; @@ -389,7 +390,7 @@ public JwtResponse getNewAccessToken(TokenRefreshRequest request) { .generateJWTToken( storedUser.getName(), getRoleListFromUser(storedUser), - storedUser.getIsAdmin(), + !nullOrEmpty(storedUser.getIsAdmin()) && storedUser.getIsAdmin(), storedUser.getEmail(), loginConfiguration.getJwtTokenExpiryTime(), false, diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/security/saml/SamlAssertionConsumerServlet.java b/openmetadata-service/src/main/java/org/openmetadata/service/security/saml/SamlAssertionConsumerServlet.java index 066432650d46..a17842e129f7 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/security/saml/SamlAssertionConsumerServlet.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/security/saml/SamlAssertionConsumerServlet.java @@ -13,6 +13,7 @@ package org.openmetadata.service.security.saml; +import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty; import static org.openmetadata.service.util.UserUtil.getRoleListFromUser; import com.onelogin.saml2.Auth; @@ -90,7 +91,7 @@ private void handleResponse(HttpServletRequest req, HttpServletResponse resp) th .generateJWTToken( username, getRoleListFromUser(user), - user.getIsAdmin(), + !nullOrEmpty(user.getIsAdmin()) && user.getIsAdmin(), email, SamlSettingsHolder.getInstance().getTokenValidity(), false, diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/UserResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/UserResourceTest.java index 417aaa5a6ee1..6eee619f9861 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/UserResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/UserResourceTest.java @@ -1146,29 +1146,30 @@ void testUserImportExport() throws IOException { String user = "userImportExport,d,s,userImportExport@domain.com,America/Los_Angeles,true,teamImportExport,"; String user1 = - "userImportExport1,,,userImportExport1@domain.com,,,teamImportExport1,DataConsumer"; - String user11 = "userImportExport11,,,userImportExport11@domain.com,,,teamImportExport11,"; + "userImportExport1,,,userImportExport1@domain.com,,false,teamImportExport1,DataConsumer"; + String user11 = "userImportExport11,,,userImportExport11@domain.com,,false,teamImportExport11,"; List createRecords = listOf(user, user1, user11); // Update user descriptions - user = "userImportExport,displayName,,userImportExport@domain.com,,,teamImportExport,"; - user1 = "userImportExport1,displayName1,,userImportExport1@domain.com,,,teamImportExport1,"; + user = "userImportExport,displayName,,userImportExport@domain.com,,false,teamImportExport,"; + user1 = + "userImportExport1,displayName1,,userImportExport1@domain.com,,false,teamImportExport1,"; user11 = - "userImportExport11,displayName11,,userImportExport11@domain.com,,,teamImportExport11,"; + "userImportExport11,displayName11,,userImportExport11@domain.com,,false,teamImportExport11,"; List updateRecords = listOf(user, user1, user11); // Add new users String user2 = - "userImportExport2,displayName2,,userImportExport2@domain.com,,,teamImportExport1,"; + "userImportExport2,displayName2,,userImportExport2@domain.com,,false,teamImportExport1,"; String user21 = - "userImportExport21,displayName21,,userImportExport21@domain.com,,,teamImportExport11,"; + "userImportExport21,displayName21,,userImportExport21@domain.com,,false,teamImportExport11,"; List newRecords = listOf(user2, user21); testImportExport("teamImportExport", UserCsv.HEADERS, createRecords, updateRecords, newRecords); // Import to team11 a user in team1 - since team1 is not under team11 hierarchy, import should // fail String user3 = - "userImportExport3,displayName3,,userImportExport3@domain.com,,,teamImportExport1,"; + "userImportExport3,displayName3,,userImportExport3@domain.com,,false,teamImportExport1,"; csv = EntityCsvTest.createCsv(UserCsv.HEADERS, listOf(user3), null); result = importCsv("teamImportExport11", csv, false); String error = diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/teams/user.json b/openmetadata-spec/src/main/resources/json/schema/entity/teams/user.json index fd020b4a0ebd..dd2a81447f60 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/teams/user.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/teams/user.json @@ -85,7 +85,8 @@ }, "isAdmin": { "description": "When true indicates user is an administrator for the system with superuser privileges.", - "type": "boolean" + "type": "boolean", + "default": false }, "authenticationMechanism": { "$ref": "#/definitions/authenticationMechanism" From 59a264fd6ca6fbd8f2c416d2e807b5d55d2f735a Mon Sep 17 00:00:00 2001 From: Maxim Martynov Date: Mon, 27 May 2024 11:17:01 +0300 Subject: [PATCH 07/48] Fix OpenLineage ingestor (#16416) * Fix OpenLineage ingestor * py format --------- Co-authored-by: ulixius9 --- .../ingestion/source/pipeline/openlineage/metadata.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ingestion/src/metadata/ingestion/source/pipeline/openlineage/metadata.py b/ingestion/src/metadata/ingestion/source/pipeline/openlineage/metadata.py index 2e1b63a36a57..9804cce2690e 100644 --- a/ingestion/src/metadata/ingestion/source/pipeline/openlineage/metadata.py +++ b/ingestion/src/metadata/ingestion/source/pipeline/openlineage/metadata.py @@ -77,7 +77,9 @@ class OpenlineageSource(PipelineServiceSource): """ @classmethod - def create(cls, config_dict, metadata: OpenMetadata): + def create( + cls, config_dict, metadata: OpenMetadata, pipeline_name: Optional[str] = None + ): """Create class instance""" config: WorkflowSource = WorkflowSource.parse_obj(config_dict) connection: OpenLineageConnection = config.serviceConnection.__root__.config @@ -379,7 +381,7 @@ def yield_pipeline( {json.dumps(pipeline_details.run_facet, indent=4).strip()}```""" request = CreatePipelineRequest( name=pipeline_name, - service=self.context.pipeline_service, + service=self.context.get().pipeline_service, description=description, ) @@ -433,8 +435,8 @@ def yield_pipeline_lineage_details( pipeline_fqn = fqn.build( metadata=self.metadata, entity_type=Pipeline, - service_name=self.context.pipeline_service, - pipeline_name=self.context.pipeline, + service_name=self.context.get().pipeline_service, + pipeline_name=self.context.get().pipeline, ) pipeline_entity = self.metadata.get_by_name(entity=Pipeline, fqn=pipeline_fqn) From 61bb4090915b82fabcf221ee6363a7f0008f557b Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Mon, 27 May 2024 14:10:05 +0530 Subject: [PATCH 08/48] Minor: added whats new for 1.4.1 (#16420) * Minor: added whats new for 1.4.1 * added note in to whats new --- .../Modals/WhatsNewModal/WhatsNewModal.tsx | 10 +- .../Modals/WhatsNewModal/whatsNewData.ts | 198 +++++++++++++++++- 2 files changed, 201 insertions(+), 7 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Modals/WhatsNewModal/WhatsNewModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Modals/WhatsNewModal/WhatsNewModal.tsx index 8f7d095535e0..e6bb891820d4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Modals/WhatsNewModal/WhatsNewModal.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Modals/WhatsNewModal/WhatsNewModal.tsx @@ -110,19 +110,19 @@ const WhatsNewModal: FunctionComponent = ({
-
+

{activeData.version}

{activeData.description}

+ {activeData?.note && ( +

{activeData.note}

+ )}
{activeData.features.length > 0 && ( -
0, - })}> +
+
)); }); @@ -224,6 +227,28 @@ describe('Test User Profile Details Component', () => { expect(screen.getByText('InlineEdit')).toBeInTheDocument(); }); + it('should not render changed displayName in input if not saved', async () => { + render(, { + wrapper: MemoryRouter, + }); + + fireEvent.click(screen.getByTestId('edit-displayName')); + + act(() => { + fireEvent.change(screen.getByTestId('displayName'), { + target: { value: 'data-test' }, + }); + }); + + act(() => { + fireEvent.click(screen.getByTestId('display-name-cancel-button')); + }); + + fireEvent.click(screen.getByTestId('edit-displayName')); + + expect(screen.getByTestId('displayName')).toHaveValue(''); + }); + it('should call updateUserDetails on click of DisplayNameButton', async () => { render(, { wrapper: MemoryRouter, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileImage/UserProfileImage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileImage/UserProfileImage.component.tsx index 53f4e58b66fe..47fef910cfb6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileImage/UserProfileImage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileImage/UserProfileImage.component.tsx @@ -13,6 +13,7 @@ import { Image } from 'antd'; import React, { useEffect, useMemo, useState } from 'react'; +import { getEntityName } from '../../../../../utils/EntityUtils'; import { getImageWithResolutionAndFallback, ImageQuality, @@ -50,7 +51,7 @@ const UserProfileImage = ({ userData }: UserProfileImageProps) => { /> ) : ( { return jest.fn().mockReturnValue(

ProfilePicture

); }); +jest.mock('../../../../../utils/EntityUtils', () => ({ + getEntityName: jest.fn().mockReturnValue('getEntityName'), +})); + describe('Test User User Profile Image Component', () => { it('should render user profile image component', async () => { render(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.component.tsx index dbe62b8539de..fab09848755e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileInheritedRoles/UserProfileInheritedRoles.component.tsx @@ -15,6 +15,7 @@ import { Card, Typography } from 'antd'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as UserIcons } from '../../../../../assets/svg/user.svg'; +import { EntityType } from '../../../../../enums/entity.enum'; import Chip from '../../../../common/Chip/Chip.component'; import { UserProfileInheritedRolesProps } from './UserProfileInheritedRoles.interface'; @@ -37,6 +38,7 @@ const UserProfileInheritedRoles = ({ }> } noDataPlaceholder={t('message.no-inherited-roles-found')} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx index e6a69c06fdca..d117baffd3e7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileRoles/UserProfileRoles.component.tsx @@ -14,7 +14,7 @@ import { Card, Select, Space, Tooltip, Typography } from 'antd'; import { AxiosError } from 'axios'; import { isEmpty, toLower } from 'lodash'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as EditIcon } from '../../../../../assets/svg/edit-new.svg'; import { ReactComponent as UserIcons } from '../../../../../assets/svg/user.svg'; @@ -24,6 +24,7 @@ import { PAGE_SIZE_LARGE, TERM_ADMIN, } from '../../../../../constants/constants'; +import { EntityType } from '../../../../../enums/entity.enum'; import { Role } from '../../../../../generated/entity/teams/role'; import { useAuth } from '../../../../../hooks/authHooks'; import { getRoles } from '../../../../../rest/rolesAPIV1'; @@ -87,6 +88,15 @@ const UserProfileRoles = ({ } }; + const setUserRoles = useCallback(() => { + const defaultUserRoles = [ + ...(userRoles?.map((role) => role.id) ?? []), + ...(isUserAdmin ? [toLower(TERM_ADMIN)] : []), + ]; + + setSelectedRoles(defaultUserRoles); + }, [userRoles, isUserAdmin]); + const handleRolesSave = async () => { setIsLoading(true); // filter out the roles , and exclude the admin one @@ -122,6 +132,7 @@ const UserProfileRoles = ({ : []), ...(userRoles ?? []), ]} + entityType={EntityType.ROLE} icon={} noDataPlaceholder={t('message.no-roles-assigned')} showNoDataPlaceholder={!isUserAdmin} @@ -130,14 +141,14 @@ const UserProfileRoles = ({ [userRoles, isUserAdmin] ); - useEffect(() => { - const defaultUserRoles = [ - ...(userRoles?.map((role) => role.id) ?? []), - ...(isUserAdmin ? [toLower(TERM_ADMIN)] : []), - ]; + const handleCloseEditRole = useCallback(() => { + setIsRolesEdit(false); + setUserRoles(); + }, [setUserRoles]); - setSelectedRoles(defaultUserRoles); - }, [isUserAdmin, userRoles]); + useEffect(() => { + setUserRoles(); + }, [setUserRoles]); useEffect(() => { if (isRolesEdit && isEmpty(roles)) { @@ -176,7 +187,7 @@ const UserProfileRoles = ({ setIsRolesEdit(false)} + onCancel={handleCloseEditRole} onSave={handleRolesSave}> + onSelectionChange([ + { + id: '37a00e0b-383c-4451-b63f-0bad4c745abc', + name: 'admin', + type: 'team', + }, + ]) + } + /> +
+ )); }); describe('Test User Profile Teams Component', () => { @@ -67,18 +95,26 @@ describe('Test User Profile Teams Component', () => { expect(await screen.findAllByText('Chip')).toHaveLength(1); }); - it('should render teams select input on edit click', async () => { + it('should maintain initial state if edit is close without save', async () => { render(); - expect(screen.getByTestId('user-team-card-container')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('edit-teams-button')); - const editButton = screen.getByTestId('edit-teams-button'); + const selectInput = screen.getByTestId('select-user-teams'); + + act(() => { + fireEvent.change(selectInput, { + target: { + value: 'test', + }, + }); + }); - expect(editButton).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('cancel')); - fireEvent.click(editButton); + fireEvent.click(screen.getByTestId('edit-teams-button')); - expect(screen.getByText('InlineEdit')).toBeInTheDocument(); + expect(screen.getByText('Organization')).toBeInTheDocument(); }); it('should call updateUserDetails on click save', async () => { @@ -95,7 +131,14 @@ describe('Test User Profile Teams Component', () => { }); expect(mockPropsData.updateUserDetails).toHaveBeenCalledWith( - { teams: [] }, + { + teams: [ + { + id: '9e8b7464-3f3e-4071-af05-19be142d75db', + type: 'team', + }, + ], + }, 'teams' ); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.component.tsx index 646d5f314b75..318a9f588085 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.component.tsx @@ -13,17 +13,21 @@ import { Col, Popover, Row, Space, Tag, Typography } from 'antd'; import { isEmpty } from 'lodash'; import React, { useEffect, useMemo, useState } from 'react'; +import { Link } from 'react-router-dom'; import { NO_DATA_PLACEHOLDER, USER_DATA_SIZE, } from '../../../constants/constants'; import { EntityReference } from '../../../generated/entity/type'; +import entityUtilClassBase from '../../../utils/EntityUtilClassBase'; import { getEntityName } from '../../../utils/EntityUtils'; import { ChipProps } from './Chip.interface'; +import './chip.less'; const Chip = ({ data, icon, + entityType, noDataPlaceholder, showNoDataPlaceholder = true, }: ChipProps) => { @@ -35,14 +39,19 @@ const Chip = ({ ); const getChipElement = (item: EntityReference) => ( - - {icon} - - {getEntityName(item)} - + + + {icon} + + {getEntityName(item)} + + ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.interface.ts index 21b4af366f7a..819a6d03b800 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.interface.ts @@ -10,10 +10,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { EntityType } from '../../../enums/entity.enum'; import { EntityReference } from '../../../generated/entity/type'; export interface ChipProps { data: EntityReference[]; + entityType: EntityType; icon?: React.ReactElement; noDataPlaceholder?: string; showNoDataPlaceholder?: boolean; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.test.tsx new file mode 100644 index 000000000000..812df3f4f2f7 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.test.tsx @@ -0,0 +1,86 @@ +/* + * 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 { fireEvent, render, screen } from '@testing-library/react'; +import React from 'react'; +import { + NO_DATA_PLACEHOLDER, + USER_DATA_SIZE, +} from '../../../constants/constants'; +import { EntityType } from '../../../enums/entity.enum'; +import { MOCK_USER_ROLE } from '../../../mocks/User.mock'; +import Chip from './Chip.component'; + +const mockLinkButton = jest.fn(); + +jest.mock('react-router-dom', () => ({ + Link: jest.fn().mockImplementation(({ children, ...rest }) => ( + + {children} + + )), +})); + +jest.mock('../../../utils/EntityUtils', () => ({ + getEntityName: jest.fn().mockReturnValue('getEntityName'), +})); + +jest.mock('../../../utils/EntityUtilClassBase', () => ({ + getEntityLink: jest.fn(), +})); + +const mockProps = { + data: [], + entityType: EntityType.ROLE, +}; + +describe('Test Chip Component', () => { + it('should renders errorPlaceholder in case of no data', () => { + render(); + + expect(screen.getByText(NO_DATA_PLACEHOLDER)).toBeInTheDocument(); + }); + + it('should renders noDataPlaceholder if provided', () => { + const placeholder = 'this is custom placeholder'; + + render(); + + expect(screen.getByText(placeholder)).toBeInTheDocument(); + }); + + it('should renders tag chips', () => { + render( + + ); + + expect(screen.getAllByTestId('tag-chip')).toHaveLength(5); + expect(screen.getAllByText('getEntityName')).toHaveLength(5); + }); + + it('should renders more chip button if data is more than the size', () => { + render(); + + expect(screen.getByTestId('plus-more-count')).toBeInTheDocument(); + expect(screen.getByText('+3 more')).toBeInTheDocument(); + }); + + it('should redirect the page when click on tag chip', () => { + render(); + + const tagChip = screen.getByTestId('ApplicationBotRole-link'); + + fireEvent.click(tagChip); + + expect(mockLinkButton).toHaveBeenCalledTimes(1); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/chip.less b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/chip.less new file mode 100644 index 000000000000..fe5622b66ef8 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/Chip/chip.less @@ -0,0 +1,23 @@ +/* + * 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 (reference) url('../../../styles/variables.less'); + +.chip-tag-link { + display: flex; + color: @black; + gap: 4px; + + &:hover { + color: @black; + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/mocks/User.mock.ts b/openmetadata-ui/src/main/resources/ui/src/mocks/User.mock.ts index a70288c2a26b..064f934405a5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/mocks/User.mock.ts +++ b/openmetadata-ui/src/main/resources/ui/src/mocks/User.mock.ts @@ -150,3 +150,168 @@ export const USER_TEAMS = [ href: 'http://localhost:8585/api/v1/teams/9e8b7464-3f3e-4071-af05-19be142d75bc', }, ]; + +export const MOCK_USER_ROLE = [ + { + id: '37a00e0b-383c-4451-b63f-0bad4c745abc', + type: 'role', + name: 'ApplicationBotRole', + fullyQualifiedName: 'ApplicationBotRole', + description: 'Role corresponding to a Application bot.', + displayName: 'Application bot role', + deleted: false, + href: 'http://localhost:8585/api/v1/roles/37a00e0b-383c-4451-b63f-0bad4c745abc', + }, + { + id: 'afc5583c-e268-4f6c-a638-a876d04ebaa1', + type: 'role', + name: 'DataConsumer', + fullyQualifiedName: 'DataConsumer', + description: + 'Users with Data Consumer role use different data assets for their day to day work.', + displayName: 'Data Consumer', + deleted: false, + href: 'http://localhost:8585/api/v1/roles/afc5583c-e268-4f6c-a638-a876d04ebaa1', + }, + { + id: '013746ec-2159-496e-88f7-f7175a2af919', + type: 'role', + name: 'DataQualityBotRole', + fullyQualifiedName: 'DataQualityBotRole', + description: 'Role corresponding to a Data quality bot.', + displayName: 'Data quality Bot role', + deleted: false, + href: 'http://localhost:8585/api/v1/roles/013746ec-2159-496e-88f7-f7175a2af919', + }, + { + id: 'dd72bae6-1835-4ba9-9532-aaa4b648d3e8', + type: 'role', + name: 'DataSteward', + fullyQualifiedName: 'DataSteward', + description: + 'Users with Data Steward role are responsible for ensuring correctness of metadata for data assets, thereby facilitating data governance principles within the organization.', + displayName: 'Data Steward', + deleted: false, + href: 'http://localhost:8585/api/v1/roles/dd72bae6-1835-4ba9-9532-aaa4b648d3e8', + }, + { + id: '6b007040-1378-4de9-a8b0-f922fc9f4e25', + type: 'role', + name: 'IngestionBotRole', + fullyQualifiedName: 'IngestionBotRole', + description: 'Role corresponding to a Ingestion bot.', + displayName: 'Ingestion bot role', + deleted: false, + href: 'http://localhost:8585/api/v1/roles/6b007040-1378-4de9-a8b0-f922fc9f4e25', + }, + { + id: '7f8de4ae-8b08-431c-9911-8a355aa2976e', + type: 'role', + name: 'ProfilerBotRole', + fullyQualifiedName: 'ProfilerBotRole', + description: 'Role corresponding to a Profiler bot.', + displayName: 'Profiler bot role', + deleted: false, + href: 'http://localhost:8585/api/v1/roles/7f8de4ae-8b08-431c-9911-8a355aa2976e', + }, + { + id: '7082d70a-ddb2-42db-b639-3ec4c7884c52', + type: 'role', + name: 'QualityBotRole', + fullyQualifiedName: 'QualityBotRole', + description: 'Role corresponding to a Quality bot.', + displayName: 'Quality bot role', + deleted: false, + href: 'http://localhost:8585/api/v1/roles/7082d70a-ddb2-42db-b639-3ec4c7884c52', + }, + { + id: 'admin', + type: 'role', + name: 'Admin', + }, +]; + +export const UPDATED_USER_DATA = { + changeDescription: { + fieldsAdded: [], + fieldsDeleted: [], + fieldsUpdated: [], + previousVersion: 3.2, + }, + defaultPersona: { + description: 'Person-04', + displayName: 'Person-04', + fullyQualifiedName: 'Person-04', + href: 'http://localhost:8585/api/v1/personas/0430976d-092a-46c9-90a8-61c6091a6f38', + id: '0430976d-092a-46c9-90a8-61c6091a6f38', + name: 'Person-04', + type: 'persona', + }, + deleted: false, + description: '', + displayName: '', + domain: { + description: 'description', + fullyQualifiedName: 'Engineering', + href: 'http://localhost:8585/api/v1/domains/303ca53b-5050-4caa-9c4e-d4fdada76a53', + id: '303ca53b-5050-4caa-9c4e-d4fdada76a53', + inherited: true, + name: 'Engineering', + type: 'domain', + }, + email: 'admin@openmetadata.org', + fullyQualifiedName: 'admin', + href: 'http://localhost:8585/api/v1/users/7f196a28-c4fa-4579-b420-f828985e7861', + id: '7f196a28-c4fa-4579-b420-f828985e7861', + inheritedRoles: [ + { + deleted: false, + description: + 'Users with Data Consumer role use different data assets for their day to day work.', + displayName: 'Data Consumer', + fullyQualifiedName: 'DataConsumer', + href: 'http://localhost:8585/api/v1/roles/ed94fd7c-0974-4b87-9295-02b36c4c6bcd', + id: 'ed94fd7c-0974-4b87-9295-02b36c4c6bcd', + name: 'DataConsumer', + type: 'role', + }, + ], + isAdmin: false, + isBot: false, + isEmailVerified: true, + name: 'admin', + personas: [ + { + description: 'Person-04', + displayName: 'Person-04', + fullyQualifiedName: 'Person-04', + href: 'http://localhost:8585/api/v1/personas/0430976d-092a-46c9-90a8-61c6091a6f38', + id: '0430976d-092a-46c9-90a8-61c6091a6f38', + name: 'Person-04', + type: 'persona', + }, + ], + roles: [ + { + id: '7f8de4ae-8b08-431c-9911-8a355aa2976e', + name: 'ProfilerBotRole', + type: 'role', + }, + ], + teams: [ + { + deleted: false, + description: + 'Organization under which all the other team hierarchy is created', + displayName: 'Organization', + fullyQualifiedName: 'Organization', + href: 'http://localhost:8585/api/v1/teams/9e8b7464-3f3e-4071-af05-19be142d75db', + id: '9e8b7464-3f3e-4071-af05-19be142d75db', + name: 'Organization', + type: 'team', + }, + ], + updatedAt: 1698655259882, + updatedBy: 'admin', + version: 3.3, +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx index 1c6561caf739..393710ab512f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.component.tsx @@ -111,11 +111,21 @@ const UserPage = () => { try { const response = await updateUserDetail(userData.id, jsonPatch); if (response) { + let updatedKeyData; + + if (key === 'roles') { + updatedKeyData = { + roles: response.roles, + isAdmin: response.isAdmin, + }; + } else { + updatedKeyData = { [key]: response[key] }; + } const newCurrentUserData = { ...currentUser, - [key]: response[key], + ...updatedKeyData, }; - const newUserData = { ...userData, [key]: response[key] }; + const newUserData = { ...userData, ...updatedKeyData }; if (key === 'defaultPersona') { if (isUndefined(response.defaultPersona)) { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.test.tsx index 436d9a71b6c5..d322ed3418d1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/UserPage/UserPage.test.tsx @@ -21,8 +21,9 @@ import { } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; +import Users from '../../components/Settings/Users/Users.component'; import { useApplicationStore } from '../../hooks/useApplicationStore'; -import { USER_DATA } from '../../mocks/User.mock'; +import { UPDATED_USER_DATA, USER_DATA } from '../../mocks/User.mock'; import { getUserByName, updateUserDetail } from '../../rest/userAPI'; import UserPage from './UserPage.component'; @@ -145,6 +146,45 @@ describe('Test the User Page', () => { expect(mockUpdateCurrentUser).toHaveBeenCalled(); }); + it('should update user isAdmin details if changes along with user', async () => { + (Users as jest.Mock).mockImplementationOnce(({ updateUserDetails }) => ( +
+ +
+ )); + + (updateUserDetail as jest.Mock).mockImplementationOnce(() => + Promise.resolve(UPDATED_USER_DATA) + ); + + await act(async () => { + render(, { wrapper: MemoryRouter }); + }); + + await act(async () => { + fireEvent.click(screen.getByText('UserComponentSaveButton')); + }); + + expect(mockUpdateCurrentUser).toHaveBeenCalledWith(UPDATED_USER_DATA); + }); + it('Should not call updateCurrentUser if user is not currentUser logged in', async () => { (useApplicationStore as unknown as jest.Mock).mockImplementation(() => ({ currentUser: { ...USER_DATA, id: '123' }, From b51455b7a5e3a8f0b1bc98d725d413e016750282 Mon Sep 17 00:00:00 2001 From: Karan Hotchandani <33024356+karanh37@users.noreply.github.com> Date: Mon, 27 May 2024 17:05:15 +0530 Subject: [PATCH 43/48] Empty quick filters (#16402) * initial commit for empty quick filters * update progress * fix field title * cleanup * add tests * unit tests * fix encoding of search query * add cypress tests * add cypress * fix flaky cypress * fix review comments * revert tooltip changes * fix tests * fix tests (cherry picked from commit 5930cd7a7a4bef73f6850848c85118eb64843e2d) --- .../common/advancedSearchQuickFilters.ts | 87 ++++++++++-- .../ui/cypress/constants/EntityConstant.ts | 9 ++ .../advancedSearchQuickFilters.constants.ts | 29 +++- .../Flow/AdvancedSearchQuickFilters.spec.ts | 128 +++++++++++++++--- .../AssetSelectionModal.tsx | 7 +- .../CustomControls.component.tsx | 7 +- .../Explore/ExploreQuickFilters.interface.ts | 2 + .../Explore/ExploreQuickFilters.tsx | 7 +- .../ExploreV1/ExploreV1.component.tsx | 51 ++++--- .../tabs/AssetsTabs.component.tsx | 8 +- .../SearchDropdown.interface.ts | 1 + .../SearchDropdown/SearchDropdown.test.tsx | 50 +++++++ .../SearchDropdown/SearchDropdown.tsx | 52 ++++++- .../AsyncSelectList/TreeAsyncSelectList.tsx | 7 +- .../DomainSelectableList.component.tsx | 2 +- .../src/constants/AdvancedSearch.constants.ts | 2 + .../ui/src/constants/explore.constants.ts | 8 ++ .../ExplorePage/ExplorePage.interface.ts | 55 +++++--- .../ExplorePage/ExplorePageV1.component.tsx | 14 +- .../ui/src/utils/EntityLineageUtils.tsx | 79 ++++++----- .../ui/src/utils/Explore.utils.test.ts | 104 +++++++++++++- .../resources/ui/src/utils/Explore.utils.ts | 111 ++++++++------- .../src/utils/ExplorePage/ExplorePageUtils.ts | 12 +- .../resources/ui/src/utils/SearchUtils.tsx | 4 +- .../resources/ui/src/utils/StringsUtils.ts | 3 +- 25 files changed, 643 insertions(+), 196 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/cypress/common/advancedSearchQuickFilters.ts b/openmetadata-ui/src/main/resources/ui/cypress/common/advancedSearchQuickFilters.ts index af95d6f11ad0..7af0bd220f11 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/common/advancedSearchQuickFilters.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/common/advancedSearchQuickFilters.ts @@ -14,26 +14,83 @@ import { interceptURL, verifyResponseStatusCode } from './common'; export const searchAndClickOnOption = (asset, filter, checkedAfterClick) => { - // Search for filter - interceptURL( - 'GET', - `/api/v1/search/aggregate?index=${asset.searchIndex}&field=${filter.key}**`, - 'aggregateAPI' - ); + let testId = Cypress._.toLower(filter.selectOptionTestId1); + // Filtering for tiers is done on client side, so no API call will be triggered + if (filter.key !== 'tier.tagFQN') { + // Search for filter + interceptURL( + 'GET', + `/api/v1/search/aggregate?index=${asset.searchIndex}&field=${filter.key}**`, + 'aggregateAPI' + ); - cy.get('[data-testid="search-input"]').clear().type(filter.selectOption1); + cy.get('[data-testid="search-input"]').clear().type(filter.selectOption1); + verifyResponseStatusCode('@aggregateAPI', 200); + } else { + testId = filter.selectOptionTestId1; + } - verifyResponseStatusCode('@aggregateAPI', 200); + cy.get(`[data-testid="${testId}"]`).should('exist').and('be.visible').click(); + checkCheckboxStatus(`${testId}-checkbox`, checkedAfterClick); +}; - cy.get(`[data-testid="${Cypress._.toLower(filter.selectOptionTestId1)}"]`) - .should('exist') - .and('be.visible') +export const selectNullOption = (asset, filter, existingValue?: any) => { + const queryFilter = JSON.stringify({ + query: { + bool: { + must: [ + { + bool: { + should: [ + { + bool: { + must_not: { + exists: { field: `${filter.key}` }, + }, + }, + }, + ...(existingValue + ? [ + { + term: { + [filter.key]: + filter.key === 'tier.tagFQN' + ? existingValue + : Cypress._.toLower(existingValue), + }, + }, + ] + : []), + ], + }, + }, + ], + }, + }, + }); + + const querySearchURL = `api/v1/search/query?*index=${asset.searchIndex}*`; + const alias = `querySearchAPI${filter.label}`; + cy.get(`[data-testid="search-dropdown-${filter.label}"]`) + .scrollIntoView() .click(); - checkCheckboxStatus( - `${Cypress._.toLower(filter.selectOptionTestId1)}-checkbox`, - checkedAfterClick - ); + cy.get(`[data-testid="no-option-checkbox"]`).click(); + + if (existingValue) { + searchAndClickOnOption(asset, filter, true); + } + + interceptURL('GET', querySearchURL, alias); + cy.get('[data-testid="update-btn"]').click(); + + cy.wait(`@${alias}`).then((xhr) => { + const actualQueryFilter = xhr.request.query['query_filter'] as string; + + expect(actualQueryFilter).to.deep.equal(queryFilter); + }); + + cy.get(`[data-testid="clear-filters"]`).scrollIntoView().click(); }; export const checkCheckboxStatus = (boxId, isChecked) => { diff --git a/openmetadata-ui/src/main/resources/ui/cypress/constants/EntityConstant.ts b/openmetadata-ui/src/main/resources/ui/cypress/constants/EntityConstant.ts index 9f76bc15f847..249cff39319b 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/constants/EntityConstant.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/constants/EntityConstant.ts @@ -474,3 +474,12 @@ export const GLOSSARY_TERM_DETAILS = { glossary: GLOSSARY_DETAILS.name, description: 'glossary term description', }; + +export const DOMAIN_QUICK_FILTERS_DETAILS = { + name: `cypress-domain-${uuid()}`, + displayName: `Cypress Domain QfTest`, + description: 'Cypress domain description', + domainType: 'Aggregate', + experts: [], + style: {}, +}; diff --git a/openmetadata-ui/src/main/resources/ui/cypress/constants/advancedSearchQuickFilters.constants.ts b/openmetadata-ui/src/main/resources/ui/cypress/constants/advancedSearchQuickFilters.constants.ts index 36b5b5d44ea7..af518eda0f09 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/constants/advancedSearchQuickFilters.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/constants/advancedSearchQuickFilters.constants.ts @@ -13,7 +13,25 @@ import { DATA_ASSETS } from './constants'; +export type FilterItem = { + label: string; + key: string; + aggregateKey?: string; + filterSearchIndex?: string; + selectOption1?: string; + selectOptionTestId1?: string; + selectOption2?: string; + selectOptionTestId2?: string; + select?: boolean; +}; + export const COMMON_DROPDOWN_ITEMS = [ + { + label: 'Domain', + key: 'domain.displayName.keyword', + selectOption1: 'Cypress Domain QfTest', + selectOptionTestId1: 'Cypress Domain QfTest', + }, { label: 'Owner', key: 'owner.displayName.keyword', @@ -40,7 +58,9 @@ export const COMMON_DROPDOWN_ITEMS = [ }, { label: 'Tier', - key: 'tier.tagsFQN', + key: 'tier.tagFQN', + selectOption1: 'Tier1', + selectOptionTestId1: 'Tier.Tier1', }, { label: 'Service Type', @@ -126,6 +146,13 @@ export const TAG_DROPDOWN_ITEMS = [ }, ]; +export const SUPPORTED_EMPTY_FILTER_FIELDS = [ + 'domain.displayName.keyword', + 'owner.displayName.keyword', + 'tags.tagFQN', + 'tier.tagFQN', +]; + export const QUICK_FILTERS_BY_ASSETS = [ { label: 'Tables', diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AdvancedSearchQuickFilters.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AdvancedSearchQuickFilters.spec.ts index 683b577c62cf..dff0b6f82eb0 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AdvancedSearchQuickFilters.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AdvancedSearchQuickFilters.spec.ts @@ -11,41 +11,86 @@ * limitations under the License. */ -import { searchAndClickOnOption } from '../../common/advancedSearchQuickFilters'; +import { + searchAndClickOnOption, + selectNullOption, +} from '../../common/advancedSearchQuickFilters'; import { interceptURL, verifyResponseStatusCode } from '../../common/common'; import { goToAdvanceSearch } from '../../common/Utils/AdvancedSearch'; -import { visitEntityDetailsPage } from '../../common/Utils/Entity'; +import { addDomainToEntity } from '../../common/Utils/Domain'; +import { + createEntityViaREST, + deleteEntityViaREST, + visitEntityDetailsPage, +} from '../../common/Utils/Entity'; +import { getToken } from '../../common/Utils/LocalStorage'; import { addOwner, removeOwner } from '../../common/Utils/Owner'; -import { QUICK_FILTERS_BY_ASSETS } from '../../constants/advancedSearchQuickFilters.constants'; +import { assignTags, removeTags } from '../../common/Utils/Tags'; +import { addTier, removeTier } from '../../common/Utils/Tier'; +import { + FilterItem, + QUICK_FILTERS_BY_ASSETS, + SUPPORTED_EMPTY_FILTER_FIELDS, +} from '../../constants/advancedSearchQuickFilters.constants'; import { SEARCH_ENTITY_TABLE } from '../../constants/constants'; -import { SidebarItem } from '../../constants/Entity.interface'; +import { EntityType, SidebarItem } from '../../constants/Entity.interface'; +import { DOMAIN_QUICK_FILTERS_DETAILS } from '../../constants/EntityConstant'; const ownerName = 'Aaron Johnson'; +const preRequisitesForTests = () => { + cy.getAllLocalStorage().then((data) => { + const token = getToken(data); + + createEntityViaREST({ + body: DOMAIN_QUICK_FILTERS_DETAILS, + endPoint: EntityType.Domain, + token, + }); + + visitEntityDetailsPage({ + term: SEARCH_ENTITY_TABLE.table_1.term, + entity: SEARCH_ENTITY_TABLE.table_1.entity, + serviceName: SEARCH_ENTITY_TABLE.table_1.serviceName, + }); + addDomainToEntity(DOMAIN_QUICK_FILTERS_DETAILS.displayName); + addOwner(ownerName); + assignTags('PersonalData.Personal', EntityType.Table); + addTier('Tier1'); + }); +}; + +const postRequisitesForTests = () => { + cy.getAllLocalStorage().then((data) => { + const token = getToken(data); + // Domain 1 to test + deleteEntityViaREST({ + entityName: DOMAIN_QUICK_FILTERS_DETAILS.name, + endPoint: EntityType.Domain, + token, + }); + visitEntityDetailsPage({ + term: SEARCH_ENTITY_TABLE.table_1.term, + entity: SEARCH_ENTITY_TABLE.table_1.entity, + serviceName: SEARCH_ENTITY_TABLE.table_1.serviceName, + }); + removeOwner(ownerName); + removeTags('PersonalData.Personal', EntityType.Table); + removeTier(); + }); +}; + describe( `Advanced search quick filters should work properly for assets`, { tags: 'DataAssets' }, () => { before(() => { cy.login(); - - visitEntityDetailsPage({ - term: SEARCH_ENTITY_TABLE.table_1.term, - entity: SEARCH_ENTITY_TABLE.table_1.entity, - serviceName: SEARCH_ENTITY_TABLE.table_1.serviceName, - }); - - addOwner(ownerName); + preRequisitesForTests(); }); after(() => { cy.login(); - visitEntityDetailsPage({ - term: SEARCH_ENTITY_TABLE.table_1.term, - entity: SEARCH_ENTITY_TABLE.table_1.entity, - serviceName: SEARCH_ENTITY_TABLE.table_1.serviceName, - }); - - removeOwner(ownerName); + postRequisitesForTests(); }); beforeEach(() => { @@ -75,8 +120,8 @@ describe( cy.get(`[data-testid="${asset.tab}"]`).scrollIntoView().click(); asset.filters - .filter((item) => item.select) - .map((filter) => { + .filter((item: FilterItem) => item.select) + .map((filter: FilterItem) => { cy.get(`[data-testid="search-dropdown-${filter.label}"]`).click(); searchAndClickOnOption(asset, filter, true); @@ -93,6 +138,47 @@ describe( verifyResponseStatusCode('@querySearchAPI', 200); }); }); + + it('should search for empty or null filters', () => { + const initialQuery = encodeURI(JSON.stringify({ query: { bool: {} } })); + // Table + interceptURL( + 'GET', + `/api/v1/search/query?*index=table_search_index&*query_filter=${initialQuery}&*`, + 'initialQueryAPI' + ); + + const asset = QUICK_FILTERS_BY_ASSETS[0]; + cy.sidebarClick(SidebarItem.EXPLORE); + verifyResponseStatusCode('@initialQueryAPI', 200); + cy.get(`[data-testid="${asset.tab}"]`).scrollIntoView().click(); + asset.filters + .filter((item) => SUPPORTED_EMPTY_FILTER_FIELDS.includes(item.key)) + .map((filter) => { + selectNullOption(asset, filter); + }); + }); + + it('should search for multiple values alongwith null filters', () => { + const initialQuery = encodeURI(JSON.stringify({ query: { bool: {} } })); + // Table + interceptURL( + 'GET', + `/api/v1/search/query?*index=table_search_index&*query_filter=${initialQuery}&*`, + 'initialQueryAPI' + ); + + const asset = QUICK_FILTERS_BY_ASSETS[0]; + cy.sidebarClick(SidebarItem.EXPLORE); + verifyResponseStatusCode('@initialQueryAPI', 200); + cy.get(`[data-testid="${asset.tab}"]`).scrollIntoView().click(); + // Checking Owner with multiple values + asset.filters + .filter((item) => SUPPORTED_EMPTY_FILTER_FIELDS.includes(item.key)) + .map((filter: FilterItem) => { + selectNullOption(asset, filter, filter?.selectOptionTestId1); + }); + }); } ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetsSelectionModal/AssetSelectionModal.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetsSelectionModal/AssetSelectionModal.tsx index a730d719f2e9..75e2b5b611d8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetsSelectionModal/AssetSelectionModal.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataAssets/AssetsSelectionModal/AssetSelectionModal.tsx @@ -68,7 +68,6 @@ import { getEntityReferenceFromEntity } from '../../../utils/EntityUtils'; import { getAggregations, getQuickFilterQuery, - getSelectedValuesFromQuickFilter, } from '../../../utils/Explore.utils'; import { getCombinedQueryFilterObject } from '../../../utils/ExplorePage/ExplorePageUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; @@ -190,11 +189,7 @@ export const AssetSelectionModal = ({ setFilters( dropdownItems.map((item) => ({ ...item, - value: getSelectedValuesFromQuickFilter( - item, - dropdownItems, - undefined // pass in state variable - ), + value: [], })) ); }, [type]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/CustomControls.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/CustomControls.component.tsx index 8f155589dc81..f4d7150d8acc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/CustomControls.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityLineage/CustomControls.component.tsx @@ -36,10 +36,7 @@ import { SearchIndex } from '../../../enums/search.enum'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { getAssetsPageQuickFilters } from '../../../utils/AdvancedSearchUtils'; import { getLoadingStatusValue } from '../../../utils/EntityLineageUtils'; -import { - getQuickFilterQuery, - getSelectedValuesFromQuickFilter, -} from '../../../utils/Explore.utils'; +import { getQuickFilterQuery } from '../../../utils/Explore.utils'; import { ExploreQuickFilterField } from '../../Explore/ExplorePage.interface'; import ExploreQuickFilters from '../../Explore/ExploreQuickFilters'; import { AssetsOfEntity } from '../../Glossary/GlossaryTerms/tabs/AssetsTabs.interface'; @@ -92,7 +89,7 @@ const CustomControls: FC = ({ setFilters( dropdownItems.map((item) => ({ ...item, - value: getSelectedValuesFromQuickFilter(item, dropdownItems), + value: [], })) ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.interface.ts index 252c5d2f1866..01fafa69660d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.interface.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +import { EntityFields } from '../../enums/AdvancedSearch.enum'; import { SearchIndex } from '../../enums/search.enum'; import { Aggregations } from '../../interface/search.interface'; import { ExploreQuickFilterField } from './ExplorePage.interface'; @@ -24,6 +25,7 @@ export interface ExploreQuickFiltersProps { showDeleted?: boolean; onChangeShowDeleted?: (showDeleted: boolean) => void; independent?: boolean; // flag to indicate if the filters are independent of aggregations + fieldsWithNullValues?: EntityFields[]; } export interface FilterFieldsMenuItem { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.tsx index c2d222cdf66b..74981e6bcdea 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.tsx @@ -23,6 +23,7 @@ import { OWNER_QUICK_FILTER_DEFAULT_OPTIONS_KEY, } from '../../constants/AdvancedSearch.constants'; import { TIER_FQN_KEY } from '../../constants/explore.constants'; +import { EntityFields } from '../../enums/AdvancedSearch.enum'; import { SearchIndex } from '../../enums/search.enum'; import { QueryFilterInterface } from '../../pages/ExplorePage/ExplorePage.interface'; import { getAggregateFieldOptions } from '../../rest/miscAPI'; @@ -46,6 +47,7 @@ const ExploreQuickFilters: FC = ({ aggregations, independent = false, onFieldValueSelect, + fieldsWithNullValues = [], }) => { const location = useLocation(); const [options, setOptions] = useState(); @@ -81,7 +83,6 @@ const ExploreQuickFilters: FC = ({ key: string ) => { let buckets: Bucket[] = []; - if (aggregations?.[key] && key !== TIER_FQN_KEY) { buckets = aggregations[key].buckets; } else { @@ -182,6 +183,9 @@ const ExploreQuickFilters: FC = ({ return ( {fields.map((field) => { + const hasNullOption = fieldsWithNullValues.includes( + field.key as EntityFields + ); const selectedKeys = field.key === TIER_FQN_KEY && options?.length ? field.value?.map((value) => { @@ -195,6 +199,7 @@ const ExploreQuickFilters: FC = ({ = ({ // to form a queryFilter to pass as a search parameter data.forEach((filter) => { if (!isEmpty(filter.value)) { - const should = [] as Array; - if (filter.value) { - filter.value.forEach((filterValue) => { - const term = {} as QueryFieldValueInterface['term']; - - term[filter.key] = filterValue.key; + const should = [] as Array; + filter.value?.forEach((filterValue) => { + const term = { + [filter.key]: filterValue.key, + }; + if (filterValue.key === NULL_OPTION_KEY) { + should.push({ + bool: { + must_not: { exists: { field: filter.key } }, + }, + }); + } else { should.push({ term }); - }); - } + } + }); - must.push({ bool: { should } }); + if (should.length > 0) { + must.push({ bool: { should } }); + } } }); @@ -211,7 +218,11 @@ const ExploreV1: React.FC = ({ isEmpty(must) ? undefined : { - query: { bool: { must } }, + query: { + bool: { + must, + }, + }, } ); }; @@ -251,14 +262,15 @@ const ExploreV1: React.FC = ({ key: string; }> = getDropDownItems(activeTabKey); + const selectedValuesFromQuickFilter = getSelectedValuesFromQuickFilter( + dropdownItems, + quickFilters + ); + setSelectedQuickFilters( dropdownItems.map((item) => ({ ...item, - value: getSelectedValuesFromQuickFilter( - item, - dropdownItems, - quickFilters - ), + value: selectedValuesFromQuickFilter?.[item.label] ?? [], })) ); }, [activeTabKey, quickFilters]); @@ -317,6 +329,7 @@ const ExploreV1: React.FC = ({ toggleModal(true)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.component.tsx index a8194e508c10..a7aba6f8a189 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/AssetsTabs.component.tsx @@ -81,7 +81,6 @@ import { import { getAggregations, getQuickFilterQuery, - getSelectedValuesFromQuickFilter, } from '../../../../utils/Explore.utils'; import { escapeESReservedCharacters, @@ -743,15 +742,10 @@ const AssetsTabs = forwardRef( useEffect(() => { const dropdownItems = getAssetsPageQuickFilters(type); - setFilters( dropdownItems.map((item) => ({ ...item, - value: getSelectedValuesFromQuickFilter( - item, - dropdownItems, - undefined // pass in state variable - ), + value: [], })) ); }, [type]); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.interface.ts index b73fd3886665..a957308842d3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.interface.ts @@ -28,6 +28,7 @@ export interface SearchDropdownProps { onSearch: (searchText: string, searchKey: string) => void; independent?: boolean; // flag to indicate if the filters are independent of aggregations hideCounts?: boolean; // Determines if the count should be displayed or not. + hasNullOption?: boolean; // Determines if the null option should be displayed or not. For e.g No Owner, No Tier etc } export interface SearchDropdownOption { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.test.tsx index e804b67fd901..2f2e20935ee5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.test.tsx @@ -354,4 +354,54 @@ describe('Search DropDown Component', () => { expect(option1Checkbox).toBeChecked(); }); + + it('should render no option checkbox', async () => { + render(); + + const container = await screen.findByTestId('search-dropdown-Owner'); + + expect(container).toBeInTheDocument(); + + await act(async () => { + userEvent.click(container); + }); + + expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument(); + + const noOwnerCheckbox = await screen.findByTestId('no-option-checkbox'); + + expect(noOwnerCheckbox).toBeInTheDocument(); + }); + + it('Should send null option in payload if selected', async () => { + render(); + const container = await screen.findByTestId('search-dropdown-Owner'); + + expect(container).toBeInTheDocument(); + + await act(async () => { + userEvent.click(container); + }); + + expect(await screen.findByTestId('drop-down-menu')).toBeInTheDocument(); + + const noOwnerCheckbox = await screen.findByTestId('no-option-checkbox'); + await act(async () => { + userEvent.click(noOwnerCheckbox); + }); + + const updateButton = await screen.findByTestId('update-btn'); + await act(async () => { + userEvent.click(updateButton); + }); + + // onChange should be called with previous selected keys and current selected keys + expect(mockOnChange).toHaveBeenCalledWith( + [ + { key: 'OM_NULL_FIELD', label: 'label.no-entity' }, + { key: 'User 1', label: 'User 1' }, + ], + 'owner.displayName' + ); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.tsx b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.tsx index c03b2a104be5..1655f59b88de 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/SearchDropdown/SearchDropdown.tsx @@ -14,6 +14,7 @@ import { Button, Card, + Checkbox, Col, Divider, Dropdown, @@ -37,6 +38,7 @@ import React, { } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as DropDown } from '../../assets/svg/drop-down.svg'; +import { NULL_OPTION_KEY } from '../../constants/AdvancedSearch.constants'; import { generateSearchDropdownLabel, getSearchDropdownLabels, @@ -65,14 +67,20 @@ const SearchDropdown: FC = ({ index, independent = false, hideCounts = false, + hasNullOption = false, }) => { const tabsInfo = searchClassBase.getTabsInfo(); const { t } = useTranslation(); const [isDropDownOpen, setIsDropDownOpen] = useState(false); const [searchText, setSearchText] = useState(''); - const [selectedOptions, setSelectedOptions] = - useState(selectedKeys); + const [selectedOptions, setSelectedOptions] = useState< + SearchDropdownOption[] + >([]); + const [nullOptionSelected, setNullOptionSelected] = useState(false); + const nullLabelText = t('label.no-entity', { + entity: label, + }); // derive menu props from options and selected keys const menuOptions: MenuProps['items'] = useMemo(() => { @@ -165,7 +173,14 @@ const SearchDropdown: FC = ({ // Handle update button click const handleUpdate = () => { // call on change with updated value - onChange(selectedOptions, searchKey); + if (nullOptionSelected) { + onChange( + [{ key: NULL_OPTION_KEY, label: nullLabelText }, ...selectedOptions], + searchKey + ); + } else { + onChange(selectedOptions, searchKey); + } handleDropdownClose(); }; @@ -175,8 +190,17 @@ const SearchDropdown: FC = ({ ); useEffect(() => { - setSelectedOptions(selectedKeys); - }, [isDropDownOpen, selectedKeys, options]); + const isNullOptionSelected = selectedKeys.some( + (item) => item.key === NULL_OPTION_KEY + ); + setNullOptionSelected(isNullOptionSelected); + }, [isDropDownOpen]); + + useEffect(() => { + setSelectedOptions( + selectedKeys.filter((item) => item.key !== NULL_OPTION_KEY) + ); + }, [isDropDownOpen, selectedKeys]); const getDropdownBody = useCallback( (menuNode: ReactNode) => { @@ -248,6 +272,22 @@ const SearchDropdown: FC = ({ + {hasNullOption && ( + <> +
+ setNullOptionSelected(e.target.checked)}> + {nullLabelText} + +
+ + + + )} + {getDropdownBody(menuNode)}
)} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx index dd5509fd998e..7152ddbf8291 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryHeader/GlossaryHeader.component.tsx @@ -11,16 +11,7 @@ * limitations under the License. */ import Icon, { DownOutlined } from '@ant-design/icons'; -import { - Button, - Col, - Divider, - Dropdown, - Row, - Space, - Tooltip, - Typography, -} from 'antd'; +import { Button, Col, Dropdown, Row, Space, Tooltip, Typography } from 'antd'; import ButtonGroup from 'antd/lib/button/button-group'; import { ItemType } from 'antd/lib/menu/hooks/useItems'; import { AxiosError } from 'axios'; @@ -41,7 +32,6 @@ import { ReactComponent as VersionIcon } from '../../../assets/svg/ic-version.sv import { ReactComponent as IconDropdown } from '../../../assets/svg/menu.svg'; import { ReactComponent as StyleIcon } from '../../../assets/svg/style.svg'; import { ManageButtonItemLabel } from '../../../components/common/ManageButtonContentItem/ManageButtonContentItem.component'; -import StatusBadge from '../../../components/common/StatusBadge/StatusBadge.component'; import { useEntityExportModalProvider } from '../../../components/Entity/EntityExportModalProvider/EntityExportModalProvider.component'; import { EntityHeader } from '../../../components/Entity/EntityHeader/EntityHeader.component'; import EntityDeleteModal from '../../../components/Modals/EntityDeleteModal/EntityDeleteModal'; @@ -67,7 +57,6 @@ import { import { getEntityDeleteMessage } from '../../../utils/CommonUtils'; import { getEntityVoteStatus } from '../../../utils/EntityUtils'; import Fqn from '../../../utils/Fqn'; -import { StatusClass } from '../../../utils/GlossaryUtils'; import { getGlossaryPath, getGlossaryPathWithAction, @@ -79,6 +68,7 @@ import { TitleBreadcrumbProps } from '../../common/TitleBreadcrumb/TitleBreadcru import Voting from '../../Entity/Voting/Voting.component'; import ChangeParentHierarchy from '../../Modals/ChangeParentHierarchy/ChangeParentHierarchy.component'; import StyleModal from '../../Modals/StyleModal/StyleModal.component'; +import { GlossaryStatusBadge } from '../GlossaryStatusBadge/GlossaryStatusBadge.component'; import { GlossaryHeaderProps } from './GlossaryHeader.interface'; const GlossaryHeader = ({ @@ -439,15 +429,7 @@ const GlossaryHeader = ({ const entityStatus = (selectedData as GlossaryTerm).status ?? Status.Approved; - return ( - - - - - ); + return ; } return null; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryStatusBadge/GlossaryStatusBadge.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryStatusBadge/GlossaryStatusBadge.component.tsx new file mode 100644 index 000000000000..f6fadbba4eb7 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryStatusBadge/GlossaryStatusBadge.component.tsx @@ -0,0 +1,26 @@ +/* + * 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 { Divider, Space } from 'antd'; +import React from 'react'; +import { Status } from '../../../generated/entity/data/glossaryTerm'; +import { StatusClass } from '../../../utils/GlossaryUtils'; +import StatusBadge from '../../common/StatusBadge/StatusBadge.component'; + +export const GlossaryStatusBadge = ({ status }: { status: Status }) => { + return ( + + + + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryStatusBadge/GlossaryStatusBadge.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryStatusBadge/GlossaryStatusBadge.test.tsx new file mode 100644 index 000000000000..85aef12fec95 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryStatusBadge/GlossaryStatusBadge.test.tsx @@ -0,0 +1,46 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; +import { Status } from '../../../generated/entity/data/glossaryTerm'; +import { GlossaryStatusBadge } from './GlossaryStatusBadge.component'; + +describe('GlossaryStatusBadge', () => { + it('renders the correct status', () => { + render(); + const statusElement = screen.getByText('Approved'); + + expect(statusElement).toHaveClass('success'); + }); + + it('renders the correct class based on draft status', () => { + render(); + const statusElement = screen.getByText('Draft'); + + expect(statusElement).toHaveClass('warning'); + }); + + it('renders the correct class based on rejected status', () => { + render(); + const statusElement = screen.getByText('Rejected'); + + expect(statusElement).toHaveClass('failure'); + }); + + it('renders the correct class based on Deprecated status', () => { + render(); + const statusElement = screen.getByText('Deprecated'); + + expect(statusElement).toHaveClass('warning'); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx index e7e3bf99eb01..91991d22f997 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx @@ -24,21 +24,16 @@ import TagSelectForm from '../../../../components/Tag/TagsSelectForm/TagsSelectF import { DE_ACTIVE_COLOR, NO_DATA_PLACEHOLDER, - PAGE_SIZE, } from '../../../../constants/constants'; import { EntityField } from '../../../../constants/Feeds.constants'; import { NO_PERMISSION_FOR_ACTION } from '../../../../constants/HelperTextUtil'; import { OperationPermission } from '../../../../context/PermissionProvider/PermissionProvider.interface'; import { EntityType } from '../../../../enums/entity.enum'; -import { SearchIndex } from '../../../../enums/search.enum'; import { GlossaryTerm } from '../../../../generated/entity/data/glossaryTerm'; import { ChangeDescription, EntityReference, } from '../../../../generated/entity/type'; -import { Paging } from '../../../../generated/type/paging'; -import { searchData } from '../../../../rest/miscAPI'; -import { formatSearchGlossaryTermResponse } from '../../../../utils/APIUtils'; import { getEntityName, getEntityReferenceFromEntity, @@ -49,7 +44,6 @@ import { getDiffByFieldName, } from '../../../../utils/EntityVersionUtils'; import { VersionStatus } from '../../../../utils/EntityVersionUtils.interface'; -import { getEntityReferenceFromGlossary } from '../../../../utils/GlossaryUtils'; import { getGlossaryPath } from '../../../../utils/RouterUtils'; import TagButton from '../../../common/TagButton/TagButton.component'; @@ -101,46 +95,6 @@ const RelatedTerms = ({ setIsIconVisible(true); }; - const fetchGlossaryTerms = async ( - searchText = '', - page: number - ): Promise<{ - data: { - label: string; - value: string; - }[]; - paging: Paging; - }> => { - const res = await searchData( - searchText, - page, - PAGE_SIZE, - '', - '', - '', - SearchIndex.GLOSSARY_TERM - ); - - const termResult = formatSearchGlossaryTermResponse( - res.data.hits.hits - ).filter( - (item) => item.fullyQualifiedName !== glossaryTerm.fullyQualifiedName - ); - - const results = termResult.map(getEntityReferenceFromGlossary); - - return { - data: results.map((item) => ({ - data: item, - label: item.fullyQualifiedName ?? '', - value: item.fullyQualifiedName ?? '', - })), - paging: { - total: res.data.hits.total.value, - }, - }; - }; - const formatOptions = (data: EntityReference[]) => { return data.map((value) => ({ ...value, @@ -302,7 +256,6 @@ const RelatedTerms = ({ defaultValue={selectedOption.map( (item) => item.fullyQualifiedName ?? '' )} - fetchApi={fetchGlossaryTerms} placeholder={t('label.add-entity', { entity: t('label.related-term-plural'), })} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/TagsSelectForm.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/TagsSelectForm.component.tsx index 6f3ae81e5509..9dcf637d4e9c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/TagsSelectForm.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsSelectForm/TagsSelectForm.component.tsx @@ -48,7 +48,7 @@ const TagSelectForm = ({ name="tagsForm" onFinish={handleSave}> - {tagType === TagSource.Classification ? ( + {tagType === TagSource.Classification && fetchApi ? ( Promise; onCancel: () => void; tagType?: TagSource; - fetchApi: ( + fetchApi?: ( search: string, page: number ) => Promise<{ diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/AdvancedSearch.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/AdvancedSearch.constants.ts index 809cdd0b7fbd..0d4fb4291db1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/AdvancedSearch.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/AdvancedSearch.constants.ts @@ -143,6 +143,10 @@ export const GLOSSARY_DROPDOWN_ITEMS = [ label: t('label.glossary-plural'), key: EntityFields.GLOSSARY, }, + { + label: t('label.status'), + key: EntityFields.GLOSSARY_TERM_STATUS, + }, ]; export const TAG_DROPDOWN_ITEMS = [ diff --git a/openmetadata-ui/src/main/resources/ui/src/enums/AdvancedSearch.enum.ts b/openmetadata-ui/src/main/resources/ui/src/enums/AdvancedSearch.enum.ts index 411214eabd35..bf786b238cd4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/enums/AdvancedSearch.enum.ts +++ b/openmetadata-ui/src/main/resources/ui/src/enums/AdvancedSearch.enum.ts @@ -58,4 +58,5 @@ export enum EntityFields { COLUMN = 'columns.name.keyword', CHART = 'charts.displayName.keyword', TASK = 'tasks.displayName.keyword', + GLOSSARY_TERM_STATUS = 'status', } diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/glossaryAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/glossaryAPI.ts index 9d92f87c9412..73013ce72a9a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/glossaryAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/glossaryAPI.ts @@ -333,7 +333,7 @@ export const getFirstLevelGlossaryTerms = async (parentFQN: string) => { >(apiUrl, { params: { directChildrenOf: parentFQN, - fields: 'childrenCount', + fields: 'childrenCount,owner', limit: 100000, }, }); diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts index 6b1b24994a62..433e3a2a68da 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts @@ -220,19 +220,6 @@ export const getSearchedTeams = ( ); }; -export const getSearchedUsersAndTeams = async ( - queryString: string, - from: number, - size = 10 -) => { - const response = await searchData(queryString, from, size, '', '', '', [ - SearchIndex.USER, - SearchIndex.TEAM, - ]); - - return response.data; -}; - export const deleteEntity = async ( entityType: string, entityId: string, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.tsx index 39b837c8e1e4..ba0e2a0d5809 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/GlossaryUtils.tsx @@ -22,26 +22,10 @@ import { FQN_SEPARATOR_CHAR } from '../constants/char.constants'; import { EntityType } from '../enums/entity.enum'; import { Glossary } from '../generated/entity/data/glossary'; import { GlossaryTerm, Status } from '../generated/entity/data/glossaryTerm'; -import { EntityReference } from '../generated/type/entityReference'; import { getEntityName } from './EntityUtils'; import Fqn from './Fqn'; import { getGlossaryPath } from './RouterUtils'; -export const getEntityReferenceFromGlossary = ( - glossary: Glossary -): EntityReference => { - return { - deleted: glossary.deleted, - href: glossary.href, - fullyQualifiedName: glossary.fullyQualifiedName ?? '', - id: glossary.id, - type: 'glossaryTerm', - description: glossary.description, - displayName: glossary.displayName, - name: glossary.name, - }; -}; - export const buildTree = (data: GlossaryTerm[]): GlossaryTerm[] => { const nodes: Record = {}; @@ -111,6 +95,22 @@ export const getQueryFilterToExcludeTerm = (fqn: string) => ({ }, }); +export const getQueryFilterToIncludeApprovedTerm = () => { + return { + query: { + bool: { + must: [ + { + term: { + status: Status.Approved, + }, + }, + ], + }, + }, + }; +}; + export const StatusClass = { [Status.Approved]: StatusType.Success, [Status.Draft]: StatusType.Warning, 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 e67aec0e2c40..f397b6cd1dec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TagsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TagsUtils.tsx @@ -39,6 +39,7 @@ import { getClassificationByName, getTags, } from '../rest/tagAPI'; +import { getQueryFilterToIncludeApprovedTerm } from './GlossaryUtils'; import { getTagsWithoutTier } from './TableUtils'; export const getClassifications = async ( @@ -269,7 +270,7 @@ export const fetchGlossaryList = async ( query: searchQueryParam ? `*${searchQueryParam}*` : '*', pageNumber: page, pageSize: 10, - queryFilter: {}, + queryFilter: getQueryFilterToIncludeApprovedTerm(), searchIndex: SearchIndex.GLOSSARY_TERM, }); From b9c47b632b9a73738a2addb4226c6f5b3f5ed0c0 Mon Sep 17 00:00:00 2001 From: Mohit Yadav <105265192+mohityadav766@users.noreply.github.com> Date: Thu, 6 Jun 2024 20:23:37 +0530 Subject: [PATCH 45/48] [FIX] GlossaryTerm reviewers should be user or team only (#16372) * add teams as reviewer * Check Users to be reviewers * Reviewers can be a team or user * Fix check by id or name * Review can be team or user both * Validate Reviewers * add multi select control * - Fix Reviewers * - Centralize Reviewer Relationship to EntityRepository * - Sort * add team as reviewer for glossary terms * locales * cleanup * - Update Reviewer should remove existing reviewers * fix selectable owner control * fix code smells * fix reviewer issue * add glossary cypress * fix patch issue on reviewers set to null * update cypress tests * fix cypress * fix cypress * fix reviewers in glossary task and supported cypress * fix pytest * Fix * fix cypress * fix code smells * Inherited Reviewers need to be present always * filter out inherited users * fix cypress * fix backend tests failure * fix backend tests failure -checkstyle * restrict owner to accept task in case of reviewer present * fix pytest --------- Co-authored-by: karanh37 Co-authored-by: Pere Miquel Brull Co-authored-by: Karan Hotchandani <33024356+karanh37@users.noreply.github.com> Co-authored-by: Ashish Gupta Co-authored-by: ulixius9 Co-authored-by: sonikashah (cherry picked from commit 9ec3d94e3b8445e63a7d77239c92c92a32536bf2) --- .../integration/ometa/test_ometa_glossary.py | 4 +- .../exception/CatalogExceptionMessage.java | 4 + .../service/jdbi3/CollectionDAO.java | 7 + .../service/jdbi3/EntityRepository.java | 97 +- .../service/jdbi3/GlossaryRepository.java | 10 +- .../service/jdbi3/GlossaryTermRepository.java | 25 +- .../resources/glossary/GlossaryResource.java | 1 - .../glossary/GlossaryTermResource.java | 1 - .../glossary/GlossaryResourceTest.java | 13 +- .../glossary/GlossaryTermResourceTest.java | 11 +- .../org/openmetadata/schema/CreateEntity.java | 4 + .../json/schema/api/data/createGlossary.json | 5 +- .../schema/api/data/createGlossaryTerm.json | 7 +- .../ui/cypress/common/GlossaryUtils.ts | 409 +++++++- .../ui/cypress/common/Utils/Entity.ts | 8 +- .../ui/cypress/constants/constants.ts | 165 --- .../ui/cypress/constants/glossary.constant.ts | 121 +++ .../ui/cypress/e2e/Flow/PersonaFlow.spec.ts | 2 +- .../e2e/Pages/Customproperties.spec.ts | 240 +---- .../ui/cypress/e2e/Pages/Glossary.spec.ts | 953 ++++++++---------- .../e2e/Pages/GlossaryVersionPage.spec.ts | 16 +- .../ui/cypress/e2e/Pages/Teams.spec.ts | 4 +- .../ActivityFeedTab.component.tsx | 2 + .../ActivityFeedTab.interface.ts | 1 + .../TestSuiteList/TestSuites.component.tsx | 4 +- .../TableQueryRightPanel.component.tsx | 5 +- .../DocumentationTab.component.tsx | 8 +- .../Task/TaskTab/TaskTab.component.test.tsx | 8 + .../Entity/Task/TaskTab/TaskTab.component.tsx | 3 +- .../Entity/Task/TaskTab/TaskTab.interface.ts | 1 + .../AddGlossary/AddGlossary.component.tsx | 35 +- .../AddGlossaryTermForm.component.tsx | 17 +- .../GlossaryDetails.component.tsx | 2 + .../GlossaryDetailsRightPanel.component.tsx | 121 ++- .../GlossaryReviewers.tsx | 1 + .../GlossaryTermTab.component.tsx | 13 +- .../GlossaryTermTab/GlossaryTermTab.test.tsx | 1 + .../GlossaryTermsV1.component.tsx | 15 +- .../Glossary/GlossaryV1.component.tsx | 17 +- .../GlossaryVersion.component.tsx | 3 + .../components/Glossary/useGlossary.store.ts | 25 +- .../AsyncSelectList/AsyncSelectList.tsx | 1 - .../OwnerLabel/OwnerLabel.component.tsx | 2 +- .../SelectableList.component.tsx | 36 +- .../SelectableList.interface.ts | 4 +- .../user-select-dropdown.less | 28 +- .../common/UserTag/UserTag.component.tsx | 8 +- .../common/UserTag/UserTag.interface.ts | 1 + .../UserTeamSelectableList.component.tsx | 253 +++-- .../UserTeamSelectableList.interface.ts | 15 +- .../user-team-selectable-list.less | 57 +- .../resources/ui/src/enums/entity.enum.ts | 1 + .../ui/src/locale/languages/de-de.json | 1 + .../ui/src/locale/languages/en-us.json | 1 + .../ui/src/locale/languages/es-es.json | 1 + .../ui/src/locale/languages/fr-fr.json | 1 + .../ui/src/locale/languages/he-he.json | 1 + .../ui/src/locale/languages/ja-jp.json | 1 + .../ui/src/locale/languages/nl-nl.json | 1 + .../ui/src/locale/languages/pt-br.json | 1 + .../ui/src/locale/languages/ru-ru.json | 1 + .../ui/src/locale/languages/zh-cn.json | 1 + .../main/resources/ui/src/mocks/Task.mock.ts | 9 + .../RequestDescriptionPage.test.tsx | 2 + .../RequestDescriptionPage.tsx | 17 +- .../RequestTagPage/RequestTagPage.test.tsx | 2 + .../RequestTagPage/RequestTagPage.tsx | 17 +- .../UpdateDescriptionPage.test.tsx | 2 + .../UpdateDescriptionPage.tsx | 17 +- .../UpdateTagPage/UpdateTagPage.test.tsx | 2 + .../TasksPage/UpdateTagPage/UpdateTagPage.tsx | 17 +- .../resources/ui/src/styles/variables.less | 1 + .../ui/src/utils/TasksUtils.test.tsx | 73 +- .../main/resources/ui/src/utils/TasksUtils.ts | 36 +- 74 files changed, 1722 insertions(+), 1278 deletions(-) diff --git a/ingestion/tests/integration/ometa/test_ometa_glossary.py b/ingestion/tests/integration/ometa/test_ometa_glossary.py index f86d42bc497c..6899bf707862 100644 --- a/ingestion/tests/integration/ometa/test_ometa_glossary.py +++ b/ingestion/tests/integration/ometa/test_ometa_glossary.py @@ -439,7 +439,7 @@ def test_patch_reviewer(self): ) self.assertIsNotNone(res_glossary_term) - self.assertEqual(1, len(res_glossary_term.reviewers.__root__)) + self.assertEqual(2, len(res_glossary_term.reviewers.__root__)) self.assertEqual(self.user_1.id, res_glossary_term.reviewers.__root__[0].id) dest_glossary_term_1 = deepcopy(res_glossary_term) dest_glossary_term_1.reviewers.__root__.pop(0) @@ -449,7 +449,7 @@ def test_patch_reviewer(self): destination=dest_glossary_term_1, ) self.assertIsNotNone(res_glossary_term) - self.assertEqual(0, len(res_glossary_term.reviewers.__root__)) + self.assertEqual(2, len(res_glossary_term.reviewers.__root__)) def test_patch_glossary_term_synonyms(self): """ diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/exception/CatalogExceptionMessage.java b/openmetadata-service/src/main/java/org/openmetadata/service/exception/CatalogExceptionMessage.java index ed8d27831827..c098e1385170 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/exception/CatalogExceptionMessage.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/exception/CatalogExceptionMessage.java @@ -313,6 +313,10 @@ public static String invalidFieldForTask(String fieldName, TaskType type) { return String.format("The field name %s is not supported for %s task.", fieldName, type); } + public static String invalidReviewerType(String type) { + return String.format("Reviewers can only be a Team or User. Given Reviewer Type : %s", type); + } + public static String invalidEnumValue(Class> enumClass) { String className = enumClass.getSimpleName(); String classNameWithLowercaseFirstLetter = 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 507f7b5a1381..022e7f4d0343 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 @@ -908,6 +908,13 @@ void deleteTo( @Bind("relation") int relation, @Bind("fromEntity") String fromEntity); + @SqlUpdate( + "DELETE from entity_relationship WHERE toId = :toId AND toEntity = :toEntity AND relation = :relation") + void deleteTo( + @BindUUID("toId") UUID toId, + @Bind("toEntity") String toEntity, + @Bind("relation") int relation); + @SqlUpdate( "DELETE from entity_relationship WHERE (toId = :id AND toEntity = :entity) OR " + "(fromId = :id AND fromEntity = :entity)") diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java index 9326ff524905..e492c82d3449 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/EntityRepository.java @@ -44,6 +44,7 @@ import static org.openmetadata.service.Entity.FIELD_STYLE; import static org.openmetadata.service.Entity.FIELD_TAGS; import static org.openmetadata.service.Entity.FIELD_VOTES; +import static org.openmetadata.service.Entity.TEAM; import static org.openmetadata.service.Entity.USER; import static org.openmetadata.service.Entity.getEntityByName; import static org.openmetadata.service.Entity.getEntityFields; @@ -471,6 +472,7 @@ public final void initializeEntity(T entity) { public final T copy(T entity, CreateEntity request, String updatedBy) { EntityReference owner = validateOwner(request.getOwner()); EntityReference domain = validateDomain(request.getDomain()); + validateReviewers(request.getReviewers()); entity.setId(UUID.randomUUID()); entity.setName(request.getName()); entity.setDisplayName(request.getDisplayName()); @@ -483,6 +485,7 @@ public final T copy(T entity, CreateEntity request, String updatedBy) { entity.setExtension(request.getExtension()); entity.setUpdatedBy(updatedBy); entity.setUpdatedAt(System.currentTimeMillis()); + entity.setReviewers(request.getReviewers()); return entity; } @@ -754,6 +757,7 @@ public final void storeRelationshipsInternal(T entity) { applyTags(entity); storeDomain(entity, entity.getDomain()); storeDataProducts(entity, entity.getDataProducts()); + storeReviewers(entity, entity.getReviewers()); storeRelationships(entity); } @@ -854,7 +858,7 @@ public final PatchResponse patch(UriInfo uriInfo, UUID id, String user, JsonP EventType change = ENTITY_NO_CHANGE; if (entityUpdater.fieldsChanged()) { change = EventType.ENTITY_UPDATED; - setInheritedFields(original, patchFields); // Restore inherited fields after a change + setInheritedFields(updated, patchFields); // Restore inherited fields after a change } return new PatchResponse<>(Status.OK, withHref(uriInfo, updated), change); } @@ -883,7 +887,7 @@ public final PatchResponse patch(UriInfo uriInfo, String fqn, String user, Js EventType change = ENTITY_NO_CHANGE; if (entityUpdater.fieldsChanged()) { change = EventType.ENTITY_UPDATED; - setInheritedFields(original, patchFields); // Restore inherited fields after a change + setInheritedFields(updated, patchFields); // Restore inherited fields after a change } return new PatchResponse<>(Status.OK, withHref(uriInfo, updated), change); } @@ -1672,9 +1676,13 @@ public final void deleteRelationship( public final void deleteTo( UUID toId, String toEntityType, Relationship relationship, String fromEntityType) { - daoCollection - .relationshipDAO() - .deleteTo(toId, toEntityType, relationship.ordinal(), fromEntityType); + if (fromEntityType == null) { + daoCollection.relationshipDAO().deleteTo(toId, toEntityType, relationship.ordinal()); + } else { + daoCollection + .relationshipDAO() + .deleteTo(toId, toEntityType, relationship.ordinal(), fromEntityType); + } } public final void deleteFrom( @@ -1699,6 +1707,43 @@ public final void validateUsers(List entityReferences) { } } + private boolean validateIfAllRefsAreEntityType(List list, String entityType) { + return list.stream().allMatch(obj -> obj.getType().equals(entityType)); + } + + public final void validateReviewers(List entityReferences) { + if (!nullOrEmpty(entityReferences)) { + boolean areAllTeam = validateIfAllRefsAreEntityType(entityReferences, TEAM); + boolean areAllUsers = validateIfAllRefsAreEntityType(entityReferences, USER); + if (areAllTeam) { + // If all are team then only one team is allowed + if (entityReferences.size() > 1) { + throw new IllegalArgumentException("Only one team can be assigned as reviewer."); + } else { + EntityReference ref = + entityReferences.get(0).getId() != null + ? Entity.getEntityReferenceById(TEAM, entityReferences.get(0).getId(), ALL) + : Entity.getEntityReferenceByName( + TEAM, entityReferences.get(0).getFullyQualifiedName(), ALL); + EntityUtil.copy(ref, entityReferences.get(0)); + } + } else if (areAllUsers) { + for (EntityReference entityReference : entityReferences) { + EntityReference ref = + entityReference.getId() != null + ? Entity.getEntityReferenceById(USER, entityReference.getId(), ALL) + : Entity.getEntityReferenceByName( + USER, entityReference.getFullyQualifiedName(), ALL); + EntityUtil.copy(ref, entityReference); + } + } else { + throw new IllegalArgumentException( + "Invalid Reviewer Type. Only one team or multiple users can be assigned as reviewer."); + } + entityReferences.sort(EntityUtil.compareEntityReference); + } + } + public final void validateRoles(List roles) { if (roles != null) { for (EntityReference entityReference : roles) { @@ -1751,7 +1796,7 @@ protected List getChildren(T entity) { protected List getReviewers(T entity) { return supportsReviewers - ? findFrom(entity.getId(), entityType, Relationship.REVIEWS, Entity.USER) + ? findFrom(entity.getId(), entityType, Relationship.REVIEWS, null) : null; } @@ -1785,9 +1830,24 @@ public final void inheritExperts(T entity, Fields fields, EntityInterface parent } public final void inheritReviewers(T entity, Fields fields, EntityInterface parent) { - if (fields.contains(FIELD_REVIEWERS) && nullOrEmpty(entity.getReviewers()) && parent != null) { - entity.setReviewers(parent.getReviewers()); - listOrEmpty(entity.getReviewers()).forEach(reviewer -> reviewer.withInherited(true)); + if (fields.contains(FIELD_REVIEWERS) && parent != null) { + List combinedReviewers = new ArrayList<>(listOrEmpty(entity.getReviewers())); + // Fetch Unique Reviewers from parent as inherited + List uniqueEntityReviewers = + listOrEmpty(parent.getReviewers()).stream() + .filter( + parentReviewer -> + combinedReviewers.stream() + .noneMatch( + entityReviewer -> + parentReviewer.getId().equals(entityReviewer.getId()) + && parentReviewer.getType().equals(entityReviewer.getType()))) + .toList(); + uniqueEntityReviewers.forEach(reviewer -> reviewer.withInherited(true)); + + combinedReviewers.addAll(uniqueEntityReviewers); + combinedReviewers.sort(EntityUtil.compareEntityReference); + entity.setReviewers(combinedReviewers); } } @@ -1827,6 +1887,17 @@ protected void storeDomain(T entity, EntityReference domain) { } } + @Transaction + protected void storeReviewers(T entity, List reviewers) { + if (supportsReviewers) { + // Add relationship user/team --- reviews ---> entity + for (EntityReference reviewer : listOrEmpty(reviewers)) { + addRelationship( + reviewer.getId(), entity.getId(), reviewer.getType(), entityType, Relationship.REVIEWS); + } + } + } + @Transaction protected void storeDataProducts(T entity, List dataProducts) { if (supportsDataProducts && !nullOrEmpty(dataProducts)) { @@ -2467,10 +2538,12 @@ protected void updateReviewers() { } List origReviewers = getEntityReferences(original.getReviewers()); List updatedReviewers = getEntityReferences(updated.getReviewers()); - validateUsers(updatedReviewers); + validateReviewers(updatedReviewers); + // Either all users or team which is one team at a time, assuming all ref to have same type, + // validateReviewer checks it updateFromRelationships( "reviewers", - Entity.USER, + null, origReviewers, updatedReviewers, Relationship.REVIEWS, @@ -2746,7 +2819,7 @@ public final void updateFromRelationships( // Add relationships from updated for (EntityReference ref : updatedFromRefs) { - addRelationship(ref.getId(), toId, fromEntityType, toEntityType, relationshipType); + addRelationship(ref.getId(), toId, ref.getType(), toEntityType, relationshipType); } updatedFromRefs.sort(EntityUtil.compareEntityReference); originFromRefs.sort(EntityUtil.compareEntityReference); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryRepository.java index 8b1839e3a01a..2add4f80c006 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryRepository.java @@ -16,7 +16,6 @@ package org.openmetadata.service.jdbi3; -import static org.openmetadata.common.utils.CommonUtil.listOrEmpty; import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty; import static org.openmetadata.csv.CsvUtil.FIELD_SEPARATOR; import static org.openmetadata.csv.CsvUtil.addEntityReference; @@ -99,9 +98,7 @@ public void clearFields(Glossary glossary, Fields fields) { } @Override - public void prepare(Glossary glossary, boolean update) { - validateUsers(glossary.getReviewers()); - } + public void prepare(Glossary glossary, boolean update) {} @Override public void storeEntity(Glossary glossary, boolean update) { @@ -114,10 +111,7 @@ public void storeEntity(Glossary glossary, boolean update) { @Override public void storeRelationships(Glossary glossary) { - for (EntityReference reviewer : listOrEmpty(glossary.getReviewers())) { - addRelationship( - reviewer.getId(), glossary.getId(), Entity.USER, Entity.GLOSSARY, Relationship.REVIEWS); - } + // Nothing to do } private Integer getUsageCount(Glossary glossary) { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java index 3be4a779d8cb..fd82d197aae0 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/GlossaryTermRepository.java @@ -21,6 +21,7 @@ import static org.openmetadata.schema.type.Include.ALL; import static org.openmetadata.service.Entity.GLOSSARY; import static org.openmetadata.service.Entity.GLOSSARY_TERM; +import static org.openmetadata.service.Entity.TEAM; import static org.openmetadata.service.exception.CatalogExceptionMessage.invalidGlossaryTermMove; import static org.openmetadata.service.exception.CatalogExceptionMessage.notReviewer; import static org.openmetadata.service.resources.tags.TagLabelUtil.checkMutuallyExclusive; @@ -63,6 +64,7 @@ import org.openmetadata.schema.entity.data.GlossaryTerm; import org.openmetadata.schema.entity.data.GlossaryTerm.Status; import org.openmetadata.schema.entity.feed.Thread; +import org.openmetadata.schema.entity.teams.Team; import org.openmetadata.schema.type.ApiStatus; import org.openmetadata.schema.type.EntityReference; import org.openmetadata.schema.type.Include; @@ -181,9 +183,6 @@ public void prepare(GlossaryTerm entity, boolean update) { // Validate related terms EntityUtil.populateEntityReferences(entity.getRelatedTerms()); - // Validate reviewers - EntityUtil.populateEntityReferences(entity.getReviewers()); - if (!update || entity.getStatus() == null) { // If parentTerm or glossary has reviewers set, the glossary term can only be created in // `Draft` mode @@ -224,10 +223,6 @@ public void storeRelationships(GlossaryTerm entity) { Relationship.RELATED_TO, true); } - for (EntityReference reviewer : listOrEmpty(entity.getReviewers())) { - addRelationship( - reviewer.getId(), entity.getId(), Entity.USER, GLOSSARY_TERM, Relationship.REVIEWS); - } } @Override @@ -698,8 +693,20 @@ private void checkUpdatedByReviewer(GlossaryTerm term, String updatedBy) { boolean isReviewer = reviewers.stream() .anyMatch( - e -> - e.getName().equals(updatedBy) || e.getFullyQualifiedName().equals(updatedBy)); + e -> { + if (e.getType().equals(TEAM)) { + Team team = + Entity.getEntityByName(TEAM, e.getName(), "users", Include.NON_DELETED); + return team.getUsers().stream() + .anyMatch( + u -> + u.getName().equals(updatedBy) + || u.getFullyQualifiedName().equals(updatedBy)); + } else { + return e.getName().equals(updatedBy) + || e.getFullyQualifiedName().equals(updatedBy); + } + }); if (!isReviewer) { throw new AuthorizationException(notReviewer(updatedBy)); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryResource.java index b529fcf51e19..8603204b9ba0 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryResource.java @@ -563,7 +563,6 @@ public static Glossary getGlossary( GlossaryRepository repository, CreateGlossary create, String updatedBy) { return repository .copy(new Glossary(), create, updatedBy) - .withReviewers(getEntityReferences(Entity.USER, create.getReviewers())) .withProvider(create.getProvider()) .withMutuallyExclusive(create.getMutuallyExclusive()); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java index 134be1ceba73..115ed0d060cb 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/glossary/GlossaryTermResource.java @@ -686,7 +686,6 @@ private GlossaryTerm getGlossaryTerm(CreateGlossaryTerm create, String user) { .withParent(getEntityReference(Entity.GLOSSARY_TERM, create.getParent())) .withRelatedTerms(getEntityReferences(Entity.GLOSSARY_TERM, create.getRelatedTerms())) .withReferences(create.getReferences()) - .withReviewers(getEntityReferences(Entity.USER, create.getReviewers())) .withProvider(create.getProvider()) .withMutuallyExclusive(create.getMutuallyExclusive()); } diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/glossary/GlossaryResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/glossary/GlossaryResourceTest.java index 7d163e47e45e..e4a52023e21d 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/glossary/GlossaryResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/glossary/GlossaryResourceTest.java @@ -31,7 +31,6 @@ import static org.openmetadata.service.util.EntityUtil.fieldAdded; import static org.openmetadata.service.util.EntityUtil.fieldUpdated; import static org.openmetadata.service.util.EntityUtil.getFqn; -import static org.openmetadata.service.util.EntityUtil.getFqns; import static org.openmetadata.service.util.EntityUtil.toTagLabels; import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS; import static org.openmetadata.service.util.TestUtils.UpdateType.CHANGE_CONSOLIDATED; @@ -111,7 +110,7 @@ public void setupGlossaries() throws IOException { .withRelatedTerms(null) .withGlossary(GLOSSARY1.getName()) .withTags(List.of(PII_SENSITIVE_TAG_LABEL, PERSONAL_DATA_TAG_LABEL)) - .withReviewers(getFqns(GLOSSARY1.getReviewers())); + .withReviewers(GLOSSARY1.getReviewers()); GLOSSARY1_TERM1 = glossaryTermResourceTest.createEntity(createGlossaryTerm, ADMIN_AUTH_HEADERS); GLOSSARY1_TERM1_LABEL = EntityUtil.toTagLabel(GLOSSARY1_TERM1); validateTagLabel(GLOSSARY1_TERM1_LABEL); @@ -121,7 +120,7 @@ public void setupGlossaries() throws IOException { .createRequest("g2t1", "", "", null) .withRelatedTerms(List.of(GLOSSARY1_TERM1.getFullyQualifiedName())) .withGlossary(GLOSSARY2.getName()) - .withReviewers(getFqns(GLOSSARY1.getReviewers())); + .withReviewers(GLOSSARY1.getReviewers()); GLOSSARY2_TERM1 = glossaryTermResourceTest.createEntity(createGlossaryTerm, ADMIN_AUTH_HEADERS); GLOSSARY2_TERM1_LABEL = EntityUtil.toTagLabel(GLOSSARY2_TERM1); validateTagLabel(GLOSSARY2_TERM1_LABEL); @@ -460,8 +459,8 @@ void testGlossaryImportExport() throws IOException { ",g2,dsp2,dsc3,h1;h3;h3,,term2;https://term2,PII.NonSensitive,%s,user;%s,%s", user1, user2, "Approved"), String.format( - "importExportTest.g1,g11,dsp2,dsc11,h1;h3;h3,,,,%s,team;%s,%s", - user1, team11, "Draft")); + "importExportTest.g1,g11,dsp2,dsc11,h1;h3;h3,,,,%s;%s,team;%s,%s", + user1, user2, team11, "Draft")); // Update terms with change in description List updateRecords = @@ -473,8 +472,8 @@ void testGlossaryImportExport() throws IOException { ",g2,dsp2,new-dsc3,h1;h3;h3,,term2;https://term2,PII.NonSensitive,%s,user;%s,%s", user1, user2, "Approved"), String.format( - "importExportTest.g1,g11,dsp2,new-dsc11,h1;h3;h3,,,,%s,team;%s,%s", - user1, team11, "Draft")); + "importExportTest.g1,g11,dsp2,new-dsc11,h1;h3;h3,,,,%s;%s,team;%s,%s", + user1, user2, team11, "Draft")); // Add new row to existing rows List newRecords = diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/glossary/GlossaryTermResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/glossary/GlossaryTermResourceTest.java index 8233314b30ba..99eb3534de8a 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/glossary/GlossaryTermResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/glossary/GlossaryTermResourceTest.java @@ -35,7 +35,6 @@ import static org.openmetadata.service.util.EntityUtil.fieldDeleted; import static org.openmetadata.service.util.EntityUtil.fieldUpdated; import static org.openmetadata.service.util.EntityUtil.getFqn; -import static org.openmetadata.service.util.EntityUtil.getFqns; import static org.openmetadata.service.util.EntityUtil.getId; import static org.openmetadata.service.util.EntityUtil.toTagLabels; import static org.openmetadata.service.util.TestUtils.*; @@ -345,7 +344,7 @@ void test_GlossaryTermApprovalWorkflow(TestInfo test) throws IOException { createGlossary = glossaryTest .createRequest(getEntityName(test, 2)) - .withReviewers(listOf(USER1.getFullyQualifiedName(), USER2.getFullyQualifiedName())); + .withReviewers(listOf(USER1.getEntityReference(), USER2.getEntityReference())); Glossary glossary2 = glossaryTest.createEntity(createGlossary, ADMIN_AUTH_HEADERS); // Creating a glossary term g2t1 should be in `Draft` mode (because glossary has reviewers) @@ -868,7 +867,7 @@ public GlossaryTerm createTerm( .withStyle(new Style().withColor("#FF5733").withIconURL("https://img")) .withParent(getFqn(parent)) .withOwner(owner) - .withReviewers(getFqns(reviewers)); + .withReviewers(reviewers); return createAndCheckEntity(createGlossaryTerm, createdBy); } @@ -902,7 +901,7 @@ public CreateGlossaryTerm createRequest(String name) { .withSynonyms(List.of("syn1", "syn2", "syn3")) .withGlossary(GLOSSARY1.getName()) .withRelatedTerms(Arrays.asList(getFqn(GLOSSARY1_TERM1), getFqn(GLOSSARY2_TERM1))) - .withReviewers(List.of(USER1_REF.getFullyQualifiedName())); + .withReviewers(List.of(USER1_REF)); } @Override @@ -928,7 +927,7 @@ public void validateCreatedEntity( } assertEntityReferenceNames(request.getRelatedTerms(), entity.getRelatedTerms()); - assertEntityReferenceNames(request.getReviewers(), entity.getReviewers()); + assertEntityReferences(request.getReviewers(), entity.getReviewers()); // Entity specific validation TestUtils.validateTags(request.getTags(), entity.getTags()); @@ -1152,7 +1151,7 @@ public Glossary createGlossary( public Glossary createGlossary( String name, List reviewers, EntityReference owner) throws IOException { CreateGlossary create = - glossaryTest.createRequest(name).withReviewers(getFqns(reviewers)).withOwner(owner); + glossaryTest.createRequest(name).withReviewers(reviewers).withOwner(owner); return glossaryTest.createAndCheckEntity(create, ADMIN_AUTH_HEADERS); } diff --git a/openmetadata-spec/src/main/java/org/openmetadata/schema/CreateEntity.java b/openmetadata-spec/src/main/java/org/openmetadata/schema/CreateEntity.java index 14aaf7c33c03..e5875156469d 100644 --- a/openmetadata-spec/src/main/java/org/openmetadata/schema/CreateEntity.java +++ b/openmetadata-spec/src/main/java/org/openmetadata/schema/CreateEntity.java @@ -31,6 +31,10 @@ default EntityReference getOwner() { return null; } + default List getReviewers() { + return null; + } + default List getTags() { return null; } diff --git a/openmetadata-spec/src/main/resources/json/schema/api/data/createGlossary.json b/openmetadata-spec/src/main/resources/json/schema/api/data/createGlossary.json index cdc288d72ecc..cf94947a50c8 100644 --- a/openmetadata-spec/src/main/resources/json/schema/api/data/createGlossary.json +++ b/openmetadata-spec/src/main/resources/json/schema/api/data/createGlossary.json @@ -22,10 +22,7 @@ }, "reviewers": { "description": "User references of the reviewers for this glossary.", - "type": "array", - "items": { - "$ref": "../../type/basic.json#/definitions/entityName" - } + "$ref": "../../type/entityReferenceList.json" }, "owner": { "description": "Owner of this glossary", diff --git a/openmetadata-spec/src/main/resources/json/schema/api/data/createGlossaryTerm.json b/openmetadata-spec/src/main/resources/json/schema/api/data/createGlossaryTerm.json index 09b84822d2fc..4c7e707283f7 100644 --- a/openmetadata-spec/src/main/resources/json/schema/api/data/createGlossaryTerm.json +++ b/openmetadata-spec/src/main/resources/json/schema/api/data/createGlossaryTerm.json @@ -53,11 +53,8 @@ } }, "reviewers": { - "description": "User names of the reviewers for this glossary.", - "type" : "array", - "items" : { - "$ref" : "../../type/basic.json#/definitions/entityName" - } + "description": "User or Team references of the reviewers for this glossary.", + "$ref": "../../type/entityReferenceList.json" }, "owner": { "description": "Owner of this glossary term.", diff --git a/openmetadata-ui/src/main/resources/ui/cypress/common/GlossaryUtils.ts b/openmetadata-ui/src/main/resources/ui/cypress/common/GlossaryUtils.ts index ddc624c4e795..c64e077f181d 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/common/GlossaryUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/common/GlossaryUtils.ts @@ -11,48 +11,68 @@ * limitations under the License. */ -import { DELETE_TERM } from '../constants/constants'; +import { + DELETE_TERM, + INVALID_NAMES, + NAME_MAX_LENGTH_VALIDATION_ERROR, + NAME_VALIDATION_ERROR, +} from '../constants/constants'; import { SidebarItem } from '../constants/Entity.interface'; import { + descriptionBox, interceptURL, toastNotification, verifyResponseStatusCode, } from './common'; -export const visitGlossaryPage = () => { - interceptURL('GET', '/api/v1/glossaries?fields=*', 'getGlossaries'); +export const validateForm = () => { + // error messages + cy.get('#name_help') + .scrollIntoView() + .should('be.visible') + .contains('Name is required'); + cy.get('#description_help') + .should('be.visible') + .contains('Description is required'); - cy.sidebarClick(SidebarItem.GLOSSARY); + // max length validation + cy.get('[data-testid="name"]') + .scrollIntoView() + .should('be.visible') + .type(INVALID_NAMES.MAX_LENGTH); + cy.get('#name_help') + .should('be.visible') + .contains(NAME_MAX_LENGTH_VALIDATION_ERROR); - verifyResponseStatusCode('@getGlossaries', 200); + // with special char validation + cy.get('[data-testid="name"]') + .should('be.visible') + .clear() + .type(INVALID_NAMES.WITH_SPECIAL_CHARS); + cy.get('#name_help').should('be.visible').contains(NAME_VALIDATION_ERROR); }; -export const addReviewer = (reviewerName, entity) => { - interceptURL('GET', '/api/v1/users?limit=25&isBot=false', 'getUsers'); - - cy.get('[data-testid="glossary-reviewer"] [data-testid="Add"]').click(); - - verifyResponseStatusCode('@getUsers', 200); - - interceptURL( - 'GET', - `api/v1/search/query?q=*${encodeURI(reviewerName)}*`, - 'searchOwner' - ); - - cy.get('[data-testid="searchbar"]').type(reviewerName); - - verifyResponseStatusCode('@searchOwner', 200); - - interceptURL('PATCH', `/api/v1/${entity}/*`, 'patchOwner'); +export const selectActiveGlossary = (glossaryName) => { + interceptURL('GET', '/api/v1/glossaryTerms*', 'getGlossaryTerms'); + cy.get('.ant-menu-item').contains(glossaryName).click(); + verifyResponseStatusCode('@getGlossaryTerms', 200); +}; - cy.get(`.ant-popover [title="${reviewerName}"]`).click(); +export const checkDisplayName = (displayName) => { + cy.get('[data-testid="entity-header-display-name"]') + .filter(':visible') + .scrollIntoView() + .within(() => { + cy.contains(displayName); + }); +}; - cy.get('[data-testid="selectable-list-update-btn"]').click(); +export const visitGlossaryPage = () => { + interceptURL('GET', '/api/v1/glossaries?fields=*', 'getGlossaries'); - verifyResponseStatusCode('@patchOwner', 200); + cy.sidebarClick(SidebarItem.GLOSSARY); - cy.get('[data-testid="glossary-reviewer"]').should('contain', reviewerName); + verifyResponseStatusCode('@getGlossaries', 200); }; export const removeReviewer = (entity) => { @@ -95,3 +115,338 @@ export const deleteGlossary = (glossary) => { toastNotification('"Glossary" deleted successfully!'); }; + +export const addOwnerInGlossary = ( + ownerNames: string | string[], + activatorBtnDataTestId: string, + resultTestId = 'owner-link', + isSelectableInsideForm = false +) => { + const isMultipleOwners = Array.isArray(ownerNames); + const owners = isMultipleOwners ? ownerNames : [ownerNames]; + + interceptURL('GET', '/api/v1/users?*isBot=false*', 'getUsers'); + + cy.get(`[data-testid="${activatorBtnDataTestId}"]`).click(); + cy.get("[data-testid='select-owner-tabs']").should('be.visible'); + cy.wait(500); // Due to popover positioning issue adding wait here, will handle this with playwright @karan + cy.get('.ant-tabs [id*=tab-users]').click({ + waitForAnimations: true, + }); + verifyResponseStatusCode('@getUsers', 200); + + interceptURL( + 'GET', + `api/v1/search/query?q=*&index=user_search_index*`, + 'searchOwner' + ); + interceptURL('PATCH', `/api/v1/**`, 'patchOwner'); + + if (isMultipleOwners) { + cy.get('[data-testid="clear-all-button"]').scrollIntoView().click(); + } + + owners.forEach((ownerName) => { + cy.get('[data-testid="owner-select-users-search-bar"]') + .clear() + .type(ownerName); + verifyResponseStatusCode('@searchOwner', 200); + cy.get(`.ant-popover [title="${ownerName}"]`).click(); + }); + + if (isMultipleOwners) { + cy.get('[data-testid="selectable-list-update-btn"]').click(); + } + + if (!isSelectableInsideForm) { + verifyResponseStatusCode('@patchOwner', 200); + } + + cy.get(`[data-testid=${resultTestId}]`).within(() => { + owners.forEach((name) => { + cy.contains(name); + }); + }); +}; + +export const addTeamAsReviewer = ( + teamName: string, + activatorBtnDataTestId: string, + dataTestId?: string, + isSelectableInsideForm = false +) => { + interceptURL( + 'GET', + '/api/v1/search/query?q=*&from=0&size=*&index=team_search_index&sort_field=displayName.keyword&sort_order=asc', + 'getTeams' + ); + + cy.get(`[data-testid="${activatorBtnDataTestId}"]`).click(); + + cy.get("[data-testid='select-owner-tabs']").should('be.visible'); + + verifyResponseStatusCode('@getTeams', 200); + + interceptURL( + 'GET', + `api/v1/search/query?q=*${encodeURI(teamName)}*`, + 'searchTeams' + ); + + cy.get('[data-testid="owner-select-teams-search-bar"]').type(teamName); + + verifyResponseStatusCode('@searchTeams', 200); + + interceptURL('PATCH', `/api/v1/**`, 'patchOwner'); + cy.get(`.ant-popover [title="${teamName}"]`).click(); + + if (!isSelectableInsideForm) { + verifyResponseStatusCode('@patchOwner', 200); + } + + cy.get(`[data-testid=${dataTestId ?? 'owner-link'}]`).should( + 'contain', + teamName + ); +}; + +export const createGlossary = (glossaryData, bValidateForm) => { + // Intercept API calls + interceptURL('POST', '/api/v1/glossaries', `create_${glossaryData.name}`); + interceptURL( + 'GET', + '/api/v1/search/query?q=*disabled:false&index=tag_search_index&from=0&size=10&query_filter=%7B%7D', + 'fetchTags' + ); + + // Click on the "Add Glossary" button + cy.get('[data-testid="add-glossary"]').click(); + + // Validate redirection to the add glossary page + cy.get('[data-testid="form-heading"]') + .contains('Add Glossary') + .should('be.visible'); + + // Perform glossary creation steps + cy.get('[data-testid="save-glossary"]') + .scrollIntoView() + .should('be.visible') + .click(); + + if (bValidateForm) { + validateForm(); + } + + cy.get('[data-testid="name"]') + .scrollIntoView() + .should('be.visible') + .clear() + .type(glossaryData.name); + + cy.get(descriptionBox) + .scrollIntoView() + .should('be.visible') + .type(glossaryData.description); + + if (glossaryData.isMutually) { + cy.get('[data-testid="mutually-exclusive-button"]') + .scrollIntoView() + .click(); + } + + if (glossaryData.tag) { + // Add tag + cy.get('[data-testid="tag-selector"] .ant-select-selection-overflow') + .scrollIntoView() + .type(glossaryData.tag); + + verifyResponseStatusCode('@fetchTags', 200); + cy.get(`[data-testid="tag-${glossaryData.tag}"]`).click(); + cy.get('[data-testid="right-panel"]').click(); + } + + if (glossaryData.reviewers.length > 0) { + // Add reviewer + if (glossaryData.reviewers[0].type === 'user') { + addOwnerInGlossary( + glossaryData.reviewers.map((reviewer) => reviewer.name), + 'add-reviewers', + 'reviewers-container', + true + ); + } else { + addTeamAsReviewer( + glossaryData.reviewers[0].name, + 'add-reviewers', + 'reviewers-container', + true + ); + } + } + + cy.get('[data-testid="save-glossary"]') + .scrollIntoView() + .should('be.visible') + .click(); + + cy.wait(`@create_${glossaryData.name}`).then(({ request }) => { + expect(request.body.name).equals(glossaryData.name); + expect(request.body.description).equals(glossaryData.description); + }); + + cy.url().should('include', '/glossary/'); + checkDisplayName(glossaryData.name); +}; + +const fillGlossaryTermDetails = ( + term, + isMutually = false, + validateCreateForm = true +) => { + cy.get('[data-testid="add-new-tag-button-header"]').click(); + + cy.contains('Add Glossary Term').should('be.visible'); + + // validation should work + cy.get('[data-testid="save-glossary-term"]') + .scrollIntoView() + .should('be.visible') + .click(); + + if (validateCreateForm) { + validateForm(); + } + + cy.get('[data-testid="name"]') + .scrollIntoView() + .should('be.visible') + .clear() + .type(term.name); + cy.get(descriptionBox) + .scrollIntoView() + .should('be.visible') + .type(term.description); + + const synonyms = term.synonyms.split(','); + cy.get('[data-testid="synonyms"]') + .scrollIntoView() + .should('be.visible') + .type(synonyms.join('{enter}')); + if (isMutually) { + cy.get('[data-testid="mutually-exclusive-button"]') + .scrollIntoView() + .should('exist') + .should('be.visible') + .click(); + } + cy.get('[data-testid="add-reference"]') + .scrollIntoView() + .should('be.visible') + .click(); + + cy.get('#name-0').scrollIntoView().should('be.visible').type('test'); + cy.get('#url-0') + .scrollIntoView() + .should('be.visible') + .type('https://test.com'); + + if (term.icon) { + cy.get('[data-testid="icon-url"]').scrollIntoView().type(term.icon); + } + if (term.color) { + cy.get('[data-testid="color-color-input"]') + .scrollIntoView() + .type(term.color); + } + + if (term.owner) { + addOwnerInGlossary(term.owner, 'add-owner', 'owner-container', true); + } +}; + +export const createGlossaryTerm = ( + term, + status, + isMutually = false, + validateCreateForm = true +) => { + fillGlossaryTermDetails(term, isMutually, validateCreateForm); + + interceptURL('POST', '/api/v1/glossaryTerms', `createGlossaryTerms`); + cy.get('[data-testid="save-glossary-term"]') + .scrollIntoView() + .should('be.visible') + .click(); + + verifyResponseStatusCode('@createGlossaryTerms', 201); + + cy.get( + `[data-row-key="${Cypress.$.escapeSelector(term.fullyQualifiedName)}"]` + ) + .scrollIntoView() + .should('be.visible') + .contains(term.name); + + cy.get( + `[data-testid="${Cypress.$.escapeSelector( + term.fullyQualifiedName + )}-status"]` + ) + .should('be.visible') + .contains(status); +}; + +export const verifyGlossaryDetails = (glossaryDetails) => { + cy.get('[data-testid="glossary-left-panel"]') + .contains(glossaryDetails.name) + .click(); + + checkDisplayName(glossaryDetails.name); + + cy.get('[data-testid="viewer-container"]') + .invoke('text') + .then((text) => { + expect(text).to.contain(glossaryDetails.description); + }); + + // Owner + cy.get(`[data-testid="glossary-right-panel-owner-link"]`).should( + 'contain', + glossaryDetails.owner ? glossaryDetails.owner : 'No Owner' + ); + + // Reviewer + if (glossaryDetails.reviewers.length > 0) { + cy.get(`[data-testid="glossary-reviewer-name"]`).within(() => { + glossaryDetails.reviewers.forEach((reviewer) => { + cy.contains(reviewer.name); + }); + }); + } + + // Tags + if (glossaryDetails.tag) { + cy.get(`[data-testid="tag-${glossaryDetails.tag}"]`).should('be.visible'); + } +}; + +const verifyGlossaryTermDataInTable = (term, status: string) => { + const escapedName = Cypress.$.escapeSelector(term.fullyQualifiedName); + const selector = `[data-row-key=${escapedName}]`; + cy.get(selector).scrollIntoView().should('be.visible'); + cy.get(`${selector} [data-testid="${escapedName}-status"]`).contains(status); + // If empty owner, the creator is the owner + cy.get(`${selector} [data-testid="owner-link"]`).contains( + term.owner ?? 'admin' + ); +}; + +export const createGlossaryTerms = (glossaryDetails) => { + selectActiveGlossary(glossaryDetails.name); + const termStatus = + glossaryDetails.reviewers.length > 0 ? 'Draft' : 'Approved'; + glossaryDetails.terms.forEach((term, index) => { + createGlossaryTerm(term, termStatus, true, index === 0); + verifyGlossaryTermDataInTable(term, termStatus); + }); +}; diff --git a/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Entity.ts b/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Entity.ts index dee9c4c72a35..3885263d6518 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Entity.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Entity.ts @@ -352,9 +352,6 @@ export const deletedEntityCommonChecks = ({ if (isTableEntity) { checkLineageTabActions({ deleted }); - } - - if (isTableEntity) { checkForTableSpecificFields({ deleted }); } @@ -395,6 +392,7 @@ export const deletedEntityCommonChecks = ({ '[data-testid="manage-dropdown-list-container"] [data-testid="delete-button"]' ).should('be.visible'); } + cy.clickOutside(); }; @@ -460,7 +458,9 @@ export const deleteEntity = ( 'getDatabaseSchemas' ); - cy.get('[data-testid="breadcrumb-link"]:last-child').click({ force: true }); + cy.get('[data-testid="entity-page-header"] [data-testid="breadcrumb-link"]') + .last() + .click(); verifyResponseStatusCode('@getDatabaseSchemas', 200); cy.get('[data-testid="show-deleted"]') diff --git a/openmetadata-ui/src/main/resources/ui/cypress/constants/constants.ts b/openmetadata-ui/src/main/resources/ui/cypress/constants/constants.ts index 5f86e4e96ca3..1a87b37bfbe5 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/constants/constants.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/constants/constants.ts @@ -305,171 +305,6 @@ export const NEW_TAG = { color: '#FF5733', icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAF8AAACFCAMAAAAKN9SOAAAAA1BMVEXmGSCqexgYAAAAI0lEQVRoge3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAHgaMeAAAUWJHZ4AAAAASUVORK5CYII=', }; -const cypressGlossaryName = `Cypress Glossary ${uuid()}`; - -export const NEW_GLOSSARY = { - name: cypressGlossaryName, - description: 'This is the Cypress Glossary', - reviewer: 'Aaron Johnson', - addReviewer: true, - tag: 'PersonalData.Personal', - isMutually: true, -}; - -const cypressProductGlossaryName = `Cypress Product%Glossary ${uuid()}`; - -export const NEW_GLOSSARY_1 = { - name: cypressProductGlossaryName, - description: 'This is the Product glossary with percentage', - reviewer: 'Brandy Miller', - addReviewer: false, -}; -const cypressAssetsGlossaryName = `Cypress Assets Glossary ${uuid()}`; - -export const CYPRESS_ASSETS_GLOSSARY = { - name: cypressAssetsGlossaryName, - description: 'This is the Assets Cypress Glossary', - reviewer: '', - addReviewer: false, - tag: 'PII.None', -}; - -const cypressAssetsGlossary1Name = `Cypress Assets Glossary 1 ${uuid()}`; - -export const CYPRESS_ASSETS_GLOSSARY_1 = { - name: cypressAssetsGlossary1Name, - description: 'Cypress Assets Glossary 1 desc', - reviewer: '', - addReviewer: false, - tag: 'PII.None', -}; - -const COMMON_ASSETS = [ - { - name: 'dim_customer', - fullyQualifiedName: 'sample_data.ecommerce_db.shopify.dim_customer', - }, - { - name: 'raw_order', - fullyQualifiedName: 'sample_data.ecommerce_db.shopify.raw_order', - }, - { - name: 'presto_etl', - fullyQualifiedName: 'sample_airflow.presto_etl', - }, -]; - -export const CYPRESS_ASSETS_GLOSSARY_TERMS = { - term_1: { - name: `Cypress%PercentTerm`, - description: 'This is the Cypress PercentTerm', - synonyms: 'buy,collect,acquire', - fullyQualifiedName: `${cypressAssetsGlossaryName}.Cypress%PercentTerm`, - assets: COMMON_ASSETS, - }, - term_2: { - name: 'Cypress Space GTerm', - description: 'This is the Cypress Sales', - synonyms: 'give,disposal,deal', - fullyQualifiedName: `${cypressAssetsGlossaryName}.Cypress Space GTerm`, - assets: COMMON_ASSETS, - }, - term_3: { - name: 'Cypress.Dot.GTerm', - description: 'This is the Cypress with space', - synonyms: 'tea,coffee,water', - fullyQualifiedName: `${cypressAssetsGlossaryName}."Cypress.Dot.GTerm"`, - displayFqn: `${cypressAssetsGlossaryName}."Cypress.Dot.GTerm"`, - assets: COMMON_ASSETS, - }, -}; - -const assetTermsUUId = uuid(); - -export const CYPRESS_ASSETS_GLOSSARY_TERMS_1 = { - term_1: { - name: `Term1_${assetTermsUUId}`, - description: 'term1 desc', - fullyQualifiedName: `${cypressAssetsGlossary1Name}.Term1_${assetTermsUUId}`, - synonyms: 'buy,collect,acquire', - assets: COMMON_ASSETS, - }, - term_2: { - name: `Term2_${assetTermsUUId}`, - description: 'term2 desc', - synonyms: 'give,disposal,deal', - fullyQualifiedName: `${cypressAssetsGlossary1Name}.Term2_${assetTermsUUId}`, - assets: COMMON_ASSETS, - }, - term_3: { - name: `Term3_${assetTermsUUId}`, - synonyms: 'tea,coffee,water', - description: 'term3 desc', - fullyQualifiedName: `${cypressAssetsGlossary1Name}.Term3_${assetTermsUUId}`, - assets: COMMON_ASSETS, - }, - term_4: { - name: `Term4_${assetTermsUUId}`, - description: 'term4 desc', - synonyms: 'milk,biscuit,water', - fullyQualifiedName: `${cypressAssetsGlossary1Name}.Term4_${assetTermsUUId}`, - assets: COMMON_ASSETS, - }, -}; - -export const NEW_GLOSSARY_TERMS = { - term_1: { - name: 'CypressPurchase', - description: 'This is the Cypress Purchase', - synonyms: 'buy,collect,acquire', - fullyQualifiedName: `${cypressGlossaryName}.CypressPurchase`, - owner: 'Aaron Johnson', - }, - term_2: { - name: 'CypressSales', - description: 'This is the Cypress Sales', - synonyms: 'give,disposal,deal', - fullyQualifiedName: `${cypressGlossaryName}.CypressSales`, - owner: 'Aaron Johnson', - }, - term_3: { - name: 'Cypress Space', - description: 'This is the Cypress with space', - synonyms: 'tea,coffee,water', - fullyQualifiedName: `${cypressGlossaryName}.Cypress Space`, - assets: COMMON_ASSETS, - owner: 'admin', - }, -}; -export const GLOSSARY_TERM_WITH_DETAILS = { - name: 'Accounts', - description: 'This is the Accounts', - tag: 'PersonalData.Personal', - synonyms: 'book,ledger,results', - relatedTerms: 'CypressSales', - reviewer: 'Colin Ho', - inheritedReviewer: 'Aaron Johnson', - fullyQualifiedName: `${cypressGlossaryName}.Accounts`, -}; - -export const NEW_GLOSSARY_1_TERMS = { - term_1: { - name: 'Features%Term', - description: 'This is the Features', - synonyms: 'data,collect,time', - fullyQualifiedName: `${cypressProductGlossaryName}.Features%Term`, - color: '#FF5733', - icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAF8AAACFCAMAAAAKN9SOAAAAA1BMVEXmGSCqexgYAAAAI0lEQVRoge3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAHgaMeAAAUWJHZ4AAAAASUVORK5CYII=', - }, - term_2: { - name: 'Uses', - description: 'This is the Uses', - synonyms: 'home,business,adventure', - fullyQualifiedName: `${cypressProductGlossaryName}.Uses`, - color: '#50C878', - icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKEAAAB5CAMAAABm4rHGAAAAA1BMVEUA7gBnh+O4AAAAKUlEQVR4nO3BAQEAAACCIP+vbkhAAQAAAAAAAAAAAAAAAAAAAAAAAL8GTJIAAVDbVToAAAAASUVORK5CYII=', - }, -}; export const service = { name: 'Glue', diff --git a/openmetadata-ui/src/main/resources/ui/cypress/constants/glossary.constant.ts b/openmetadata-ui/src/main/resources/ui/cypress/constants/glossary.constant.ts index 964f5c964b09..e4b1cd39c518 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/constants/glossary.constant.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/constants/glossary.constant.ts @@ -36,3 +36,124 @@ export const GLOSSARY_TERM_DETAILS1 = { style: {}, glossary: GLOSSARY_DETAILS1.name, }; + +const COMMON_ASSETS = [ + { + name: 'dim_customer', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.dim_customer', + }, + { + name: 'raw_order', + fullyQualifiedName: 'sample_data.ecommerce_db.shopify.raw_order', + }, + { + name: 'presto_etl', + fullyQualifiedName: 'sample_airflow.presto_etl', + }, +]; + +const cypressGlossaryName = `Cypress Glossary ${uuid()}`; + +// Glossary with Multiple Users as Reviewers +export const GLOSSARY_1 = { + name: cypressGlossaryName, + description: 'This is the Cypress Glossary', + reviewers: [ + { name: 'Aaron Johnson', type: 'user' }, + { name: 'Aaron Singh', type: 'user' }, + ], + tag: 'PersonalData.Personal', + isMutually: true, + owner: 'admin', + updatedOwner: 'Aaron Warren', + terms: [ + { + name: 'CypressPurchase', + description: 'This is the Cypress Purchase', + synonyms: 'buy,collect,acquire', + fullyQualifiedName: `${cypressGlossaryName}.CypressPurchase`, + owner: 'Aaron Johnson', + reviewers: [], + }, + { + name: 'CypressSales', + description: 'This is the Cypress Sales', + synonyms: 'give,disposal,deal', + fullyQualifiedName: `${cypressGlossaryName}.CypressSales`, + owner: 'Aaron Johnson', + reviewers: [], + }, + { + name: 'Cypress Space', + description: 'This is the Cypress with space', + synonyms: 'tea,coffee,water', + fullyQualifiedName: `${cypressGlossaryName}.Cypress Space`, + assets: COMMON_ASSETS, + owner: 'admin', + reviewers: [], + }, + ], +}; + +const cypressProductGlossaryName = `Cypress Product%Glossary ${uuid()}`; + +// Glossary with Team as Reviewers +export const GLOSSARY_2 = { + name: cypressProductGlossaryName, + description: 'This is the Product glossary with percentage', + reviewers: [{ name: 'Applications', type: 'team' }], + owner: 'admin', + terms: [ + { + name: 'Features%Term', + description: 'This is the Features', + synonyms: 'data,collect,time', + fullyQualifiedName: `${cypressProductGlossaryName}.Features%Term`, + color: '#FF5733', + icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAF8AAACFCAMAAAAKN9SOAAAAA1BMVEXmGSCqexgYAAAAI0lEQVRoge3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAHgaMeAAAUWJHZ4AAAAASUVORK5CYII=', + }, + { + name: 'Uses', + description: 'This is the Uses', + synonyms: 'home,business,adventure', + fullyQualifiedName: `${cypressProductGlossaryName}.Uses`, + color: '#50C878', + icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKEAAAB5CAMAAABm4rHGAAAAA1BMVEUA7gBnh+O4AAAAKUlEQVR4nO3BAQEAAACCIP+vbkhAAQAAAAAAAAAAAAAAAAAAAAAAAL8GTJIAAVDbVToAAAAASUVORK5CYII=', + }, + ], +}; + +const cypressAssetsGlossaryName = `Cypress Assets Glossary ${uuid()}`; +const assetTermsUUId = uuid(); + +// Glossary with No Reviewer +export const GLOSSARY_3 = { + name: cypressAssetsGlossaryName, + description: 'This is the Product glossary with percentage', + reviewers: [], + owner: 'admin', + newDescription: 'This is the new Product glossary with percentage.', + terms: [ + { + name: `Term1_${assetTermsUUId}`, + description: 'term1 desc', + fullyQualifiedName: `${cypressAssetsGlossaryName}.Term1_${assetTermsUUId}`, + synonyms: 'buy,collect,acquire', + assets: COMMON_ASSETS, + }, + { + name: `Term2_${assetTermsUUId}`, + description: 'term2 desc', + synonyms: 'give,disposal,deal', + fullyQualifiedName: `${cypressAssetsGlossaryName}.Term2_${assetTermsUUId}`, + assets: COMMON_ASSETS, + }, + { + name: `Term3_${assetTermsUUId}`, + synonyms: 'tea,coffee,water', + description: 'term3 desc', + fullyQualifiedName: `${cypressAssetsGlossaryName}.Term3_${assetTermsUUId}`, + assets: COMMON_ASSETS, + }, + ], +}; diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/PersonaFlow.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/PersonaFlow.spec.ts index 07b5fa9c4799..23b21de5ff47 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/PersonaFlow.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/PersonaFlow.spec.ts @@ -95,7 +95,7 @@ describe('Persona operations', { tags: 'Settings' }, () => { cy.get('[data-testid="searchbar"]').type(userSearchText); - cy.get(`[title="${userSearchText}"] .ant-checkbox-input`).check(); + cy.get(`.ant-popover [title="${userSearchText}"]`).click(); cy.get('[data-testid="selectable-list-update-btn"]') .scrollIntoView() .click(); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Customproperties.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Customproperties.spec.ts index 9343086d66de..278fe7ca6179 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Customproperties.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Customproperties.spec.ts @@ -11,13 +11,13 @@ * limitations under the License. */ -import { lowerCase, omit } from 'lodash'; +import { lowerCase } from 'lodash'; +import { interceptURL, verifyResponseStatusCode } from '../../common/common'; import { - descriptionBox, - interceptURL, - verifyResponseStatusCode, -} from '../../common/common'; -import { deleteGlossary } from '../../common/GlossaryUtils'; + createGlossary, + createGlossaryTerms, + deleteGlossary, +} from '../../common/GlossaryUtils'; import { addCustomPropertiesForEntity, customPropertiesArray, @@ -37,17 +37,10 @@ import { visitEntityDetailsPage, } from '../../common/Utils/Entity'; import { getToken } from '../../common/Utils/LocalStorage'; -import { - ENTITIES, - INVALID_NAMES, - NAME_MAX_LENGTH_VALIDATION_ERROR, - NAME_VALIDATION_ERROR, - NEW_GLOSSARY, - NEW_GLOSSARY_TERMS, - uuid, -} from '../../constants/constants'; +import { ENTITIES, uuid } from '../../constants/constants'; import { EntityType, SidebarItem } from '../../constants/Entity.interface'; import { DATABASE_SERVICE } from '../../constants/EntityConstant'; +import { GLOSSARY_1 } from '../../constants/glossary.constant'; const CREDENTIALS = { name: 'aaron_johnson0', @@ -81,199 +74,6 @@ const customPropertyValue = { }, }; -const validateForm = () => { - // error messages - cy.get('#name_help') - .scrollIntoView() - .should('be.visible') - .contains('Name is required'); - cy.get('#description_help') - .should('be.visible') - .contains('Description is required'); - - // max length validation - cy.get('[data-testid="name"]') - .scrollIntoView() - .should('be.visible') - .type(INVALID_NAMES.MAX_LENGTH); - cy.get('#name_help') - .should('be.visible') - .contains(NAME_MAX_LENGTH_VALIDATION_ERROR); - - // with special char validation - cy.get('[data-testid="name"]') - .should('be.visible') - .clear() - .type(INVALID_NAMES.WITH_SPECIAL_CHARS); - cy.get('#name_help').should('be.visible').contains(NAME_VALIDATION_ERROR); -}; -const createGlossary = (glossaryData) => { - // Intercept API calls - interceptURL('POST', '/api/v1/glossaries', 'createGlossary'); - interceptURL( - 'GET', - '/api/v1/search/query?q=*disabled:false&index=tag_search_index&from=0&size=10&query_filter=%7B%7D', - 'fetchTags' - ); - - // Click on the "Add Glossary" button - cy.get('[data-testid="add-glossary"]').click(); - - // Validate redirection to the add glossary page - cy.get('[data-testid="form-heading"]') - .contains('Add Glossary') - .should('be.visible'); - - // Perform glossary creation steps - cy.get('[data-testid="save-glossary"]') - .scrollIntoView() - .should('be.visible') - .click(); - - validateForm(); - - cy.get('[data-testid="name"]') - .scrollIntoView() - .should('be.visible') - .clear() - .type(glossaryData.name); - - cy.get(descriptionBox) - .scrollIntoView() - .should('be.visible') - .type(glossaryData.description); - - if (glossaryData.isMutually) { - cy.get('[data-testid="mutually-exclusive-button"]') - .scrollIntoView() - .click(); - } - - if (glossaryData.tag) { - // Add tag - cy.get('[data-testid="tag-selector"] .ant-select-selection-overflow') - .scrollIntoView() - .type(glossaryData.tag); - - verifyResponseStatusCode('@fetchTags', 200); - cy.get(`[data-testid="tag-${glossaryData.tag}"]`).click(); - cy.get('[data-testid="right-panel"]').click(); - } - - if (glossaryData.addReviewer) { - // Add reviewer - cy.get('[data-testid="add-reviewers"]').scrollIntoView().click(); - cy.get('[data-testid="searchbar"]').type(CREDENTIALS.displayName); - cy.get(`[title="${CREDENTIALS.displayName}"]`) - .scrollIntoView() - .should('be.visible') - .click(); - cy.get('[data-testid="selectable-list-update-btn"]') - .should('exist') - .and('be.visible') - .click(); - } - - cy.get('[data-testid="save-glossary"]') - .scrollIntoView() - .should('be.visible') - .click(); - - cy.wait('@createGlossary').then(({ request }) => { - expect(request.body.name).equals(glossaryData.name); - expect(request.body.description).equals(glossaryData.description); - }); - - cy.url().should('include', '/glossary/'); -}; -const fillGlossaryTermDetails = (term, glossary, isMutually = false) => { - cy.get('[data-testid="add-new-tag-button-header"]').click(); - - cy.contains('Add Glossary Term').should('be.visible'); - - // validation should work - cy.get('[data-testid="save-glossary-term"]') - .scrollIntoView() - .should('be.visible') - .click(); - - validateForm(); - - cy.get('[data-testid="name"]') - .scrollIntoView() - .should('be.visible') - .clear() - .type(term.name); - cy.get(descriptionBox) - .scrollIntoView() - .should('be.visible') - .type(term.description); - - const synonyms = term.synonyms.split(','); - cy.get('[data-testid="synonyms"]') - .scrollIntoView() - .should('be.visible') - .type(synonyms.join('{enter}')); - if (isMutually) { - cy.get('[data-testid="mutually-exclusive-button"]') - .scrollIntoView() - .should('exist') - .should('be.visible') - .click(); - } - cy.get('[data-testid="add-reference"]') - .scrollIntoView() - .should('be.visible') - .click(); - - cy.get('#name-0').scrollIntoView().should('be.visible').type('test'); - cy.get('#url-0') - .scrollIntoView() - .should('be.visible') - .type('https://test.com'); - - if (term.icon) { - cy.get('[data-testid="icon-url"]').scrollIntoView().type(term.icon); - } - if (term.color) { - cy.get('[data-testid="color-color-input"]') - .scrollIntoView() - .type(term.color); - } -}; -const createGlossaryTerm = (term, glossary, status, isMutually = false) => { - fillGlossaryTermDetails(term, glossary, isMutually); - - interceptURL('POST', '/api/v1/glossaryTerms', 'createGlossaryTerms'); - cy.get('[data-testid="save-glossary-term"]') - .scrollIntoView() - .should('be.visible') - .click(); - - verifyResponseStatusCode('@createGlossaryTerms', 201); - - cy.get( - `[data-row-key="${Cypress.$.escapeSelector(term.fullyQualifiedName)}"]` - ) - .scrollIntoView() - .should('be.visible') - .contains(term.name); - - cy.get( - `[data-testid="${Cypress.$.escapeSelector( - term.fullyQualifiedName - )}-status"]` - ) - .should('be.visible') - .contains(status); - - if (glossary.name === NEW_GLOSSARY.name) { - cy.get(`[data-testid="${NEW_GLOSSARY_TERMS.term_1.name}"]`) - .scrollIntoView() - .click(); - } -}; - describe('Custom Properties should work properly', { tags: 'Settings' }, () => { beforeEach(() => { cy.login(); @@ -675,6 +475,8 @@ describe('Custom Properties should work properly', { tags: 'Settings' }, () => { // Verify field exists cy.get(`[title="${propertyName}"]`).should('be.visible'); + + cy.get('[data-testid="cancel-btn"]').click(); }); it(`Delete created property for glossary term entity`, () => { @@ -696,17 +498,11 @@ describe('Custom Properties should work properly', { tags: 'Settings' }, () => { interceptURL('GET', '/api/v1/glossaries?fields=*', 'fetchGlossaries'); cy.sidebarClick(SidebarItem.GLOSSARY); + const glossary = GLOSSARY_1; + glossary.terms = [GLOSSARY_1.terms[0]]; - createGlossary({ - ...omit(NEW_GLOSSARY, ['reviewer']), - addReviewer: false, - }); - createGlossaryTerm( - NEW_GLOSSARY_TERMS.term_1, - NEW_GLOSSARY, - 'Approved', - true - ); + createGlossary(GLOSSARY_1, false); + createGlossaryTerms(glossary); cy.settingClick(glossaryTerm.entityApiType, true); @@ -721,10 +517,10 @@ describe('Custom Properties should work properly', { tags: 'Settings' }, () => { }); visitEntityDetailsPage({ - term: NEW_GLOSSARY_TERMS.term_1.name, - serviceName: NEW_GLOSSARY_TERMS.term_1.fullyQualifiedName, + term: glossary.terms[0].name, + serviceName: glossary.terms[0].fullyQualifiedName, entity: 'glossaryTerms' as EntityType, - dataTestId: `${NEW_GLOSSARY.name}-${NEW_GLOSSARY_TERMS.term_1.name}`, + dataTestId: `${glossary.name}-${glossary.terms[0].name}`, }); // set custom property value @@ -771,7 +567,7 @@ describe('Custom Properties should work properly', { tags: 'Settings' }, () => { // delete glossary and glossary term cy.sidebarClick(SidebarItem.GLOSSARY); - deleteGlossary(NEW_GLOSSARY.name); + deleteGlossary(glossary.name); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Glossary.spec.ts index d295c17429c5..99d183b96172 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Glossary.spec.ts @@ -18,7 +18,15 @@ import { verifyMultipleResponseStatusCode, verifyResponseStatusCode, } from '../../common/common'; -import { deleteGlossary } from '../../common/GlossaryUtils'; +import { + addOwnerInGlossary, + checkDisplayName, + createGlossary, + createGlossaryTerms, + deleteGlossary, + selectActiveGlossary, + verifyGlossaryDetails, +} from '../../common/GlossaryUtils'; import { dragAndDropElement } from '../../common/Utils/DragAndDrop'; import { visitEntityDetailsPage } from '../../common/Utils/Entity'; import { confirmationDragAndDropGlossary } from '../../common/Utils/Glossary'; @@ -28,51 +36,30 @@ import { generateRandomUser, removeOwner, } from '../../common/Utils/Owner'; +import { assignTags, removeTags } from '../../common/Utils/Tags'; import { GLOSSARY_DROPDOWN_ITEMS } from '../../constants/advancedSearchQuickFilters.constants'; import { COLUMN_NAME_FOR_APPLY_GLOSSARY_TERM, - CYPRESS_ASSETS_GLOSSARY, - CYPRESS_ASSETS_GLOSSARY_1, - CYPRESS_ASSETS_GLOSSARY_TERMS, - CYPRESS_ASSETS_GLOSSARY_TERMS_1, DELETE_TERM, - INVALID_NAMES, - NAME_MAX_LENGTH_VALIDATION_ERROR, - NAME_VALIDATION_ERROR, - NEW_GLOSSARY, - NEW_GLOSSARY_1, - NEW_GLOSSARY_1_TERMS, - NEW_GLOSSARY_TERMS, SEARCH_ENTITY_TABLE, } from '../../constants/constants'; -import { SidebarItem } from '../../constants/Entity.interface'; -import { GLOSSARY_OWNER_LINK_TEST_ID } from '../../constants/glossary.constant'; +import { EntityType, SidebarItem } from '../../constants/Entity.interface'; +import { + GLOSSARY_1, + GLOSSARY_2, + GLOSSARY_3, + GLOSSARY_OWNER_LINK_TEST_ID, +} from '../../constants/glossary.constant'; +import { GlobalSettingOptions } from '../../constants/settings.constant'; const CREDENTIALS = generateRandomUser(); const userName = `${CREDENTIALS.firstName}${CREDENTIALS.lastName}`; -let createdUserId = ''; - -const selectOwner = (ownerName: string, dataTestId?: string) => { - interceptURL('GET', '/api/v1/users?*isBot=false*', 'getUsers'); - cy.get('[data-testid="add-owner"]').scrollIntoView().click(); - cy.get("[data-testid='select-owner-tabs']").should('be.visible'); - cy.get('.ant-tabs [id*=tab-users]').click(); - verifyResponseStatusCode('@getUsers', 200); - interceptURL( - 'GET', - `api/v1/search/query?q=*&index=user_search_index*`, - 'searchOwner' - ); +const CREDENTIALS_2 = generateRandomUser(); +const userName2 = `${CREDENTIALS_2.firstName}${CREDENTIALS_2.lastName}`; - cy.get('[data-testid="owner-select-users-search-bar"]').type(ownerName); - verifyResponseStatusCode('@searchOwner', 200); - cy.get(`.ant-popover [title="${ownerName}"]`).click(); - cy.get(`[data-testid=${dataTestId ?? 'owner-link'}]`).should( - 'contain', - ownerName - ); -}; +let createdUserId = ''; +let createdUserId_2 = ''; const visitGlossaryTermPage = ( termName: string, @@ -105,208 +92,12 @@ const visitGlossaryTermPage = ( cy.get('.ant-tabs .glossary-overview-tab').should('be.visible').click(); }; -const createGlossary = (glossaryData, bValidateForm) => { - // Intercept API calls - interceptURL('POST', '/api/v1/glossaries', 'createGlossary'); - interceptURL( - 'GET', - '/api/v1/search/query?q=*disabled:false&index=tag_search_index&from=0&size=10&query_filter=%7B%7D', - 'fetchTags' - ); - - // Click on the "Add Glossary" button - cy.get('[data-testid="add-glossary"]').click(); - - // Validate redirection to the add glossary page - cy.get('[data-testid="form-heading"]') - .contains('Add Glossary') - .should('be.visible'); - - // Perform glossary creation steps - cy.get('[data-testid="save-glossary"]') - .scrollIntoView() - .should('be.visible') - .click(); - - if (bValidateForm) { - validateForm(); - } - - cy.get('[data-testid="name"]') - .scrollIntoView() - .should('be.visible') - .clear() - .type(glossaryData.name); - - cy.get(descriptionBox) - .scrollIntoView() - .should('be.visible') - .type(glossaryData.description); - - if (glossaryData.isMutually) { - cy.get('[data-testid="mutually-exclusive-button"]') - .scrollIntoView() - .click(); - } - - if (glossaryData.tag) { - // Add tag - cy.get('[data-testid="tag-selector"] .ant-select-selection-overflow') - .scrollIntoView() - .type(glossaryData.tag); - - verifyResponseStatusCode('@fetchTags', 200); - cy.get(`[data-testid="tag-${glossaryData.tag}"]`).click(); - cy.get('[data-testid="right-panel"]').click(); - } - - if (glossaryData.addReviewer) { - // Add reviewer - cy.get('[data-testid="add-reviewers"]').scrollIntoView().click(); - cy.get('[data-testid="searchbar"]').type(userName); - cy.get(`[title="${userName}"]`) - .scrollIntoView() - .should('be.visible') - .click(); - cy.get('[data-testid="selectable-list-update-btn"]') - .should('exist') - .and('be.visible') - .click(); - } - - cy.get('[data-testid="save-glossary"]') - .scrollIntoView() - .should('be.visible') - .click(); - - cy.wait('@createGlossary').then(({ request }) => { - expect(request.body.name).equals(glossaryData.name); - expect(request.body.description).equals(glossaryData.description); - }); - - cy.url().should('include', '/glossary/'); - checkDisplayName(glossaryData.name); -}; - -const checkDisplayName = (displayName) => { - cy.get('[data-testid="entity-header-display-name"]') - .filter(':visible') - .scrollIntoView() - .within(() => { - cy.contains(displayName); - }); -}; - -const verifyGlossaryTermDataInTable = (term, status: string) => { - const escapedName = Cypress.$.escapeSelector(term.fullyQualifiedName); - const selector = `[data-row-key=${escapedName}]`; - cy.get(selector).scrollIntoView().should('be.visible'); - cy.get(`${selector} [data-testid="${escapedName}-status"]`).contains(status); - // If empty owner, the creator is the owner - cy.get(`${selector} [data-testid="owner-link"]`).contains( - term.owner ?? 'admin' - ); -}; - const checkAssetsCount = (assetsCount) => { cy.get('[data-testid="assets"] [data-testid="filter-count"]') .scrollIntoView() .should('have.text', assetsCount); }; -const validateForm = () => { - // error messages - cy.get('#name_help') - .scrollIntoView() - .should('be.visible') - .contains('Name is required'); - cy.get('#description_help') - .should('be.visible') - .contains('Description is required'); - - // max length validation - cy.get('[data-testid="name"]') - .scrollIntoView() - .should('be.visible') - .type(INVALID_NAMES.MAX_LENGTH); - cy.get('#name_help') - .should('be.visible') - .contains(NAME_MAX_LENGTH_VALIDATION_ERROR); - - // with special char validation - cy.get('[data-testid="name"]') - .should('be.visible') - .clear() - .type(INVALID_NAMES.WITH_SPECIAL_CHARS); - cy.get('#name_help').should('be.visible').contains(NAME_VALIDATION_ERROR); -}; - -const fillGlossaryTermDetails = ( - term, - isMutually = false, - validateCreateForm = true -) => { - cy.get('[data-testid="add-new-tag-button-header"]').click(); - - cy.contains('Add Glossary Term').should('be.visible'); - - // validation should work - cy.get('[data-testid="save-glossary-term"]') - .scrollIntoView() - .should('be.visible') - .click(); - - if (validateCreateForm) { - validateForm(); - } - - cy.get('[data-testid="name"]') - .scrollIntoView() - .should('be.visible') - .clear() - .type(term.name); - cy.get(descriptionBox) - .scrollIntoView() - .should('be.visible') - .type(term.description); - - const synonyms = term.synonyms.split(','); - cy.get('[data-testid="synonyms"]') - .scrollIntoView() - .should('be.visible') - .type(synonyms.join('{enter}')); - if (isMutually) { - cy.get('[data-testid="mutually-exclusive-button"]') - .scrollIntoView() - .should('exist') - .should('be.visible') - .click(); - } - cy.get('[data-testid="add-reference"]') - .scrollIntoView() - .should('be.visible') - .click(); - - cy.get('#name-0').scrollIntoView().should('be.visible').type('test'); - cy.get('#url-0') - .scrollIntoView() - .should('be.visible') - .type('https://test.com'); - - if (term.icon) { - cy.get('[data-testid="icon-url"]').scrollIntoView().type(term.icon); - } - if (term.color) { - cy.get('[data-testid="color-color-input"]') - .scrollIntoView() - .type(term.color); - } - - if (term.owner) { - selectOwner(term.owner, 'owner-container'); - } -}; - const addAssetToGlossaryTerm = (glossaryTerm, glossary) => { goToGlossaryPage(); selectActiveGlossary(glossary.name); @@ -366,51 +157,6 @@ const removeAssetsFromGlossaryTerm = (glossaryTerm, glossary) => { }); }; -const createGlossaryTerm = ( - term, - glossary, - status, - isMutually = false, - validateCreateForm = true -) => { - fillGlossaryTermDetails(term, isMutually, validateCreateForm); - - interceptURL('POST', '/api/v1/glossaryTerms', 'createGlossaryTerms'); - cy.get('[data-testid="save-glossary-term"]') - .scrollIntoView() - .should('be.visible') - .click(); - - verifyResponseStatusCode('@createGlossaryTerms', 201); - - cy.get( - `[data-row-key="${Cypress.$.escapeSelector(term.fullyQualifiedName)}"]` - ) - .scrollIntoView() - .should('be.visible') - .contains(term.name); - - cy.get( - `[data-testid="${Cypress.$.escapeSelector( - term.fullyQualifiedName - )}-status"]` - ) - .should('be.visible') - .contains(status); - - if (glossary.name === NEW_GLOSSARY.name) { - cy.get(`[data-testid="${NEW_GLOSSARY_TERMS.term_1.name}"]`) - .scrollIntoView() - .click(); - - cy.get('[data-testid="glossary-reviewer-name"]') - .scrollIntoView() - .contains(userName) - .should('be.visible'); - cy.get(':nth-child(2) > .link-title').click(); - } -}; - const deleteGlossaryTerm = ({ name, fullyQualifiedName }) => { visitGlossaryTermPage(name, fullyQualifiedName); @@ -456,12 +202,6 @@ const goToAssetsTab = ( cy.get('.ant-tabs-tab-active').contains('Assets').should('be.visible'); }; -const selectActiveGlossary = (glossaryName) => { - interceptURL('GET', '/api/v1/glossaryTerms*', 'getGlossaryTerms'); - cy.get('.ant-menu-item').contains(glossaryName).click(); - verifyResponseStatusCode('@getGlossaryTerms', 200); -}; - const updateSynonyms = (uSynonyms) => { cy.get('[data-testid="synonyms-container"]') .scrollIntoView() @@ -490,31 +230,6 @@ const updateSynonyms = (uSynonyms) => { }); }; -const updateTags = (inTerm: boolean) => { - // visit glossary page - interceptURL( - 'GET', - '/api/v1/search/query?q=*&index=tag_search_index&from=0&size=*&query_filter=*', - 'tags' - ); - cy.get('[data-testid="tags-container"] [data-testid="add-tag"]').click(); - - verifyResponseStatusCode('@tags', 200); - - cy.get('[data-testid="tag-selector"]') - .scrollIntoView() - .should('be.visible') - .type('personal'); - cy.get('[data-testid="tag-PersonalData.Personal"]').click(); - - cy.get('[data-testid="saveAssociatedTag"]').scrollIntoView().click(); - const container = inTerm - ? '[data-testid="tags-container"]' - : '[data-testid="glossary-details"]'; - cy.wait(1000); - cy.get(container).scrollIntoView().contains('Personal').should('be.visible'); -}; - const updateTerms = (newTerm: string) => { interceptURL( 'GET', @@ -746,6 +461,14 @@ const deleteUser = () => { }).then((response) => { expect(response.status).to.eq(200); }); + + cy.request({ + method: 'DELETE', + url: `/api/v1/users/${createdUserId_2}?hardDelete=true&recursive=false`, + headers: { Authorization: `Bearer ${token}` }, + }).then((response) => { + expect(response.status).to.eq(200); + }); }); }; @@ -778,7 +501,6 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { cy.login(); cy.getAllLocalStorage().then((data) => { const token = getToken(data); - // Create a new user cy.request({ method: 'POST', @@ -787,6 +509,51 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { body: CREDENTIALS, }).then((response) => { createdUserId = response.body.id; + + // Assign user to the team + cy.sidebarClick(SidebarItem.SETTINGS); + // Clicking on teams + cy.settingClick(GlobalSettingOptions.TEAMS); + const appName = 'Applications'; + + interceptURL('GET', `/api/v1/teams/**`, 'getTeams'); + interceptURL( + 'GET', + `/api/v1/users?fields=teams%2Croles&limit=25&team=${appName}`, + 'teamUsers' + ); + + cy.get('[data-testid="search-bar-container"]').type(appName); + cy.get(`[data-row-key="${appName}"]`).contains(appName).click(); + verifyResponseStatusCode('@getTeams', 200); + verifyResponseStatusCode('@teamUsers', 200); + + interceptURL('GET', '/api/v1/users?*isBot=false*', 'getUsers'); + cy.get('[data-testid="add-new-user"]').click(); + verifyResponseStatusCode('@getUsers', 200); + interceptURL( + 'GET', + `api/v1/search/query?q=*&index=user_search_index*`, + 'searchOwner' + ); + cy.get( + '[data-testid="selectable-list"] [data-testid="search-bar-container"]' + ).type(userName); + verifyResponseStatusCode('@searchOwner', 200); + interceptURL('PATCH', `/api/v1/**`, 'patchOwner'); + cy.get(`.ant-popover [title="${userName}"]`).click(); + cy.get('[data-testid="selectable-list-update-btn"]').click(); + verifyResponseStatusCode('@patchOwner', 200); + }); + + // Create a new user_2 + cy.request({ + method: 'POST', + url: `/api/v1/users/signup`, + headers: { Authorization: `Bearer ${token}` }, + body: CREDENTIALS_2, + }).then((response) => { + createdUserId_2 = response.body.id; }); }); }); @@ -803,158 +570,84 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { }); it('Create new glossary flow should work properly', () => { - createGlossary(NEW_GLOSSARY, true); - createGlossary(NEW_GLOSSARY_1, false); + createGlossary(GLOSSARY_1, true); + createGlossary(GLOSSARY_2, false); + createGlossary(GLOSSARY_3, false); + verifyGlossaryDetails(GLOSSARY_1); + verifyGlossaryDetails(GLOSSARY_2); + verifyGlossaryDetails(GLOSSARY_3); }); - it('Assign Owner', () => { + it('Glossary Owner Flow', () => { cy.get('[data-testid="glossary-left-panel"]') - .contains(NEW_GLOSSARY.name) + .contains(GLOSSARY_1.name) .click(); - checkDisplayName(NEW_GLOSSARY.name); + checkDisplayName(GLOSSARY_1.name); addOwner(userName, GLOSSARY_OWNER_LINK_TEST_ID); - }); - - it('Update Owner', () => { - cy.get('[data-testid="glossary-left-panel"]') - .contains(NEW_GLOSSARY.name) - .click(); - - checkDisplayName(NEW_GLOSSARY.name); + cy.reload(); addOwner('Alex Pollard', GLOSSARY_OWNER_LINK_TEST_ID); - }); - - it('Remove Owner', () => { - cy.get('[data-testid="glossary-left-panel"]') - .contains(NEW_GLOSSARY.name) - .click(); - - checkDisplayName(NEW_GLOSSARY.name); + cy.reload(); removeOwner('Alex Pollard', GLOSSARY_OWNER_LINK_TEST_ID); }); - it('Verify and Remove Tags from Glossary', () => { - cy.get('[data-testid="glossary-left-panel"]') - .contains(NEW_GLOSSARY.name) - .click(); - - checkDisplayName(NEW_GLOSSARY.name); - // Verify Tags which is added at the time of creating glossary - cy.get('[data-testid="tags-container"]') - .contains('Personal') - .should('be.visible'); - - // Remove Tag - cy.get('[data-testid="tags-container"] [data-testid="edit-button"]') - .scrollIntoView() - .click(); + it('Create glossary term should work properly', () => { + createGlossaryTerms(GLOSSARY_1); + createGlossaryTerms(GLOSSARY_2); + createGlossaryTerms(GLOSSARY_3); - cy.get('[data-testid="remove-tags"]').should('be.visible').click(); - interceptURL('PATCH', '/api/v1/glossaries/*', 'updateGlossary'); - cy.get('[data-testid="saveAssociatedTag"]').scrollIntoView().click(); - verifyResponseStatusCode('@updateGlossary', 200); - cy.get('[data-testid="add-tag"]').should('be.visible'); + verifyStatusFilterInExplore('Approved'); + verifyStatusFilterInExplore('Draft'); }); - it('Verify added glossary details', () => { - cy.get('[data-testid="glossary-left-panel"]') - .contains(NEW_GLOSSARY.name) - .click(); - - checkDisplayName(NEW_GLOSSARY.name); - - cy.get('[data-testid="viewer-container"]') - .invoke('text') - .then((text) => { - expect(text).to.contain(NEW_GLOSSARY.description); - }); - - cy.get(`[data-testid="glossary-reviewer-name"]`) - .invoke('text') - .then((text) => { - expect(text).to.contain(userName); - }); - - // Verify Product glossary details - cy.get('.ant-menu-item').contains(NEW_GLOSSARY_1.name).click(); - - cy.get('[data-testid="glossary-left-panel"]') - .contains(NEW_GLOSSARY_1.name) - .should('be.visible') - .scrollIntoView(); - - selectActiveGlossary(NEW_GLOSSARY_1.name); + it('Updating data of glossary should work properly', () => { + selectActiveGlossary(GLOSSARY_1.name); - checkDisplayName(NEW_GLOSSARY_1.name); - cy.get('[data-testid="viewer-container"]') - .invoke('text') - .then((text) => { - expect(text).to.contain(NEW_GLOSSARY_1.description); - }); - }); + // Updating owner + addOwner(userName2, GLOSSARY_OWNER_LINK_TEST_ID); + + // Updating Reviewer + const reviewers = GLOSSARY_1.reviewers.map((reviewer) => reviewer.name); + addOwnerInGlossary( + [...reviewers, userName], + 'edit-reviewer-button', + 'glossary-reviewer-name', + false + ); - it('Create glossary term should work properly', () => { - const terms = Object.values(NEW_GLOSSARY_TERMS); - selectActiveGlossary(NEW_GLOSSARY.name); - terms.forEach((term, index) => { - createGlossaryTerm(term, NEW_GLOSSARY, 'Draft', true, index === 0); - verifyGlossaryTermDataInTable(term, 'Draft'); - }); + // updating tags + removeTags(GLOSSARY_1.tag, EntityType.Glossary); + assignTags('PII.None', EntityType.Glossary); - // Glossary term for Product glossary - selectActiveGlossary(NEW_GLOSSARY_1.name); + // updating description + updateDescription('Updated description', true); - const ProductTerms = Object.values(NEW_GLOSSARY_1_TERMS); - ProductTerms.forEach((term) => { - createGlossaryTerm(term, NEW_GLOSSARY_1, 'Approved', false, false); - verifyGlossaryTermDataInTable(term, 'Approved'); - }); - verifyStatusFilterInExplore('Approved'); - verifyStatusFilterInExplore('Draft'); + voteGlossary(true); }); - it('Approval Workflow for Glossary Term', () => { + it('Team Approval Workflow for Glossary Term', () => { cy.logout(); - cy.login(CREDENTIALS.email, CREDENTIALS.password); approveGlossaryTermWorkflow({ - glossary: NEW_GLOSSARY, - glossaryTerm: NEW_GLOSSARY_TERMS.term_1, + glossary: GLOSSARY_2, + glossaryTerm: GLOSSARY_2.terms[0], }); approveGlossaryTermWorkflow({ - glossary: NEW_GLOSSARY, - glossaryTerm: NEW_GLOSSARY_TERMS.term_2, + glossary: GLOSSARY_2, + glossaryTerm: GLOSSARY_2.terms[1], }); cy.logout(); Cypress.session.clearAllSavedSessions(); cy.login(); }); - it('Updating data of glossary should work properly', () => { - cy.get('[data-testid="glossary-left-panel"]') - .contains(NEW_GLOSSARY.name) - .click(); - - checkDisplayName(NEW_GLOSSARY.name); - - // Updating owner - addOwner(userName, GLOSSARY_OWNER_LINK_TEST_ID); - - // updating tags - updateTags(false); - - // updating description - updateDescription('Updated description', true); - - voteGlossary(true); - }); - it('Update glossary term', () => { const uSynonyms = ['pick up', 'take', 'obtain']; const newRef = { name: 'take', url: 'https://take.com' }; - const term2 = NEW_GLOSSARY_TERMS.term_2.name; - const { name, fullyQualifiedName } = NEW_GLOSSARY_1_TERMS.term_1; + const term2 = GLOSSARY_3.terms[1].name; + const { name, fullyQualifiedName } = GLOSSARY_1.terms[0]; + const { name: newTermName, fullyQualifiedName: newTermFqn } = + GLOSSARY_1.terms[1]; // visit glossary page interceptURL( @@ -964,7 +657,7 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { ); interceptURL('GET', `/api/v1/permissions/glossary/*`, 'permissions'); - cy.get('.ant-menu-item').contains(NEW_GLOSSARY_1.name).click(); + cy.get('.ant-menu-item').contains(GLOSSARY_1.name).click(); verifyMultipleResponseStatusCode(['@glossaryTerm', '@permissions'], 200); // visit glossary term page @@ -987,13 +680,50 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { // updating description updateDescription('Updated description', false); + // Updating Reviewer + addOwnerInGlossary( + [userName], + 'edit-reviewer-button', + 'glossary-reviewer-name', + false + ); + // updating voting for glossary term voteGlossary(); + + goToGlossaryPage(); + cy.get('.ant-menu-item').contains(GLOSSARY_1.name).click(); + visitGlossaryTermPage(newTermName, newTermFqn); + + // Updating Reviewer + addOwnerInGlossary( + [userName], + 'edit-reviewer-button', + 'glossary-reviewer-name', + false + ); + }); + + it('User Approval Workflow for Glossary Term', () => { + cy.logout(); + cy.login(CREDENTIALS.email, CREDENTIALS.password); + approveGlossaryTermWorkflow({ + glossary: GLOSSARY_1, + glossaryTerm: GLOSSARY_1.terms[0], + }); + + approveGlossaryTermWorkflow({ + glossary: GLOSSARY_1, + glossaryTerm: GLOSSARY_1.terms[1], + }); + cy.logout(); + Cypress.session.clearAllSavedSessions(); + cy.login(); }); it('Request Tags workflow for Glossary', function () { cy.get('[data-testid="glossary-left-panel"]') - .contains(NEW_GLOSSARY_1.name) + .contains(GLOSSARY_1.name) .click(); interceptURL( @@ -1006,10 +736,18 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { cy.get('[data-testid="request-entity-tags"]').should('exist').click(); - // check assignees for task which will be owner of the glossary term + // check assignees for task which will be reviewer of the glossary term cy.get( '[data-testid="select-assignee"] > .ant-select-selector > .ant-select-selection-overflow' - ).should('contain', 'admin'); + ).within(() => { + for (const reviewer of [...GLOSSARY_1.reviewers, { name: userName }]) { + cy.contains(reviewer.name); + } + }); + + cy.get( + '[data-testid="select-assignee"] > .ant-select-selector > .ant-select-selection-overflow' + ).should('not.contain', userName2); cy.get('[data-testid="tag-selector"]') .click() @@ -1026,6 +764,44 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { cy.get('[data-testid="submit-tag-request"]').click(); verifyResponseStatusCode('@taskCreated', 201); + // Owner should not be able to accept the tag suggestion when reviewer is assigned + cy.logout(); + cy.login(CREDENTIALS_2.email, CREDENTIALS_2.password); + + goToGlossaryPage(); + + cy.get('[data-testid="glossary-left-panel"]') + .contains(GLOSSARY_1.name) + .click(); + + cy.get('[data-testid="activity_feed"]').click(); + + cy.get('[data-testid="global-setting-left-panel"]') + .contains('Tasks') + .click(); + + // accept the tag suggestion button should not be present + cy.get('[data-testid="task-cta-buttons"]').should( + 'not.contain', + 'Accept Suggestion' + ); + + // Reviewer only should accepts the tag suggestion + cy.logout(); + cy.login(CREDENTIALS.email, CREDENTIALS.password); + + goToGlossaryPage(); + + cy.get('[data-testid="glossary-left-panel"]') + .contains(GLOSSARY_1.name) + .click(); + + cy.get('[data-testid="activity_feed"]').click(); + + cy.get('[data-testid="global-setting-left-panel"]') + .contains('Tasks') + .click(); + // Accept the tag suggestion which is created cy.get('.ant-btn-compact-first-item').contains('Accept Suggestion').click(); @@ -1034,36 +810,148 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { cy.reload(); cy.get('[data-testid="glossary-left-panel"]') - .contains(NEW_GLOSSARY_1.name) + .contains(GLOSSARY_1.name) .click(); - checkDisplayName(NEW_GLOSSARY_1.name); + checkDisplayName(GLOSSARY_1.name); - // Verify Tags which is added at the time of creating glossary - cy.get('[data-testid="tags-container"]') - .contains('Personal') - .should('be.visible'); + cy.logout(); + Cypress.session.clearAllSavedSessions(); + cy.login(); + }); + + it('Request Tags workflow for Glossary and reviewer as Team', function () { + cy.get('[data-testid="glossary-left-panel"]') + .contains(GLOSSARY_2.name) + .click(); + + interceptURL( + 'GET', + `/api/v1/search/query?q=*%20AND%20disabled:false&index=tag_search_index*`, + 'suggestTag' + ); + interceptURL('POST', '/api/v1/feed', 'taskCreated'); + interceptURL('PUT', '/api/v1/feed/tasks/*/resolve', 'taskResolve'); + + cy.get('[data-testid="request-entity-tags"]').should('exist').click(); + + // check assignees for task which will be Owner of the glossary term which is Team + cy.get( + '[data-testid="select-assignee"] > .ant-select-selector > .ant-select-selection-overflow' + ).within(() => { + for (const reviewer of GLOSSARY_2.reviewers) { + cy.contains(reviewer.name); + } + }); + + cy.get( + '[data-testid="select-assignee"] > .ant-select-selector > .ant-select-selection-overflow' + ).should('not.contain', GLOSSARY_2.owner); + + cy.get('[data-testid="tag-selector"]') + .click() + .type('{backspace}') + .type('{backspace}') + .type('Personal'); + + verifyResponseStatusCode('@suggestTag', 200); + cy.get( + '.ant-select-dropdown [data-testid="tag-PersonalData.Personal"]' + ).click(); + cy.clickOutside(); + + cy.get('[data-testid="submit-tag-request"]').click(); + verifyResponseStatusCode('@taskCreated', 201); + + // Reviewer should accepts the tag suggestion which belongs to the Team + cy.logout(); + cy.login(CREDENTIALS.email, CREDENTIALS.password); + + goToGlossaryPage(); + + cy.get('[data-testid="glossary-left-panel"]') + .contains(GLOSSARY_2.name) + .click(); + + cy.get('[data-testid="activity_feed"]').click(); + + cy.get('[data-testid="global-setting-left-panel"]') + .contains('Tasks') + .click(); + + // Accept the tag suggestion which is created + cy.get('.ant-btn-compact-first-item').contains('Accept Suggestion').click(); + + verifyResponseStatusCode('@taskResolve', 200); + + cy.reload(); + + cy.get('[data-testid="glossary-left-panel"]') + .contains(GLOSSARY_2.name) + .click(); + + checkDisplayName(GLOSSARY_2.name); + + cy.logout(); + Cypress.session.clearAllSavedSessions(); + cy.login(); + }); + + it('Request Description workflow for Glossary', function () { + cy.get('[data-testid="glossary-left-panel"]') + .contains(GLOSSARY_3.name) + .click(); + + interceptURL( + 'GET', + `/api/v1/search/query?q=*%20AND%20disabled:false&index=tag_search_index*`, + 'suggestTag' + ); + interceptURL('POST', '/api/v1/feed', 'taskCreated'); + interceptURL('PUT', '/api/v1/feed/tasks/*/resolve', 'taskResolve'); + + cy.get('[data-testid="request-description"]').should('exist').click(); + + // check assignees for task which will be owner of the glossary since it has no reviewer + cy.get( + '[data-testid="select-assignee"] > .ant-select-selector > .ant-select-selection-overflow' + ).should('contain', GLOSSARY_3.owner); + + cy.get(descriptionBox).should('be.visible').as('description'); + cy.get('@description').clear(); + cy.get('@description').type(GLOSSARY_3.newDescription); + + cy.get('[data-testid="submit-btn"]').click(); + verifyResponseStatusCode('@taskCreated', 201); + + // Accept the tag suggestion which is created + cy.get('.ant-btn-compact-first-item').contains('Accept Suggestion').click(); + + verifyResponseStatusCode('@taskResolve', 200); + + cy.reload(); + + cy.get('[data-testid="glossary-left-panel"]') + .contains(GLOSSARY_3.name) + .click(); + + checkDisplayName(GLOSSARY_3.name); }); it('Assets Tab should work properly', () => { - selectActiveGlossary(NEW_GLOSSARY.name); - const glossary = NEW_GLOSSARY.name; - const term1 = NEW_GLOSSARY_TERMS.term_1.name; - const term2 = NEW_GLOSSARY_TERMS.term_2.name; + const glossary1 = GLOSSARY_1.name; + const term1 = GLOSSARY_1.terms[0]; + const term2 = GLOSSARY_1.terms[1]; - const glossary1 = NEW_GLOSSARY_1.name; - const term3 = NEW_GLOSSARY_1_TERMS.term_1.name; - const term4 = NEW_GLOSSARY_1_TERMS.term_2.name; + const glossary2 = GLOSSARY_2.name; + const term3 = GLOSSARY_2.terms[0]; + const term4 = GLOSSARY_2.terms[1]; const entity = SEARCH_ENTITY_TABLE.table_3; - cy.get('.ant-menu-item').contains(NEW_GLOSSARY_1.name).click(); + selectActiveGlossary(glossary2); - goToAssetsTab( - NEW_GLOSSARY_1_TERMS.term_1.name, - NEW_GLOSSARY_1_TERMS.term_1.fullyQualifiedName, - true - ); + goToAssetsTab(term3.name, term3.fullyQualifiedName, true); cy.contains('Adding a new Asset is easy, just give it a spin!').should( 'be.visible' ); @@ -1099,13 +987,17 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { cy.get(`${parentPath} [data-testid="add-tag"]`).click(); // Select 1st term - cy.get('[data-testid="tag-selector"] #tagsForm_tags').click().type(term1); - cy.get(`[data-testid="tag-${glossary}.${term1}"]`).click(); - cy.get('[data-testid="tag-selector"]').should('contain', term1); + cy.get('[data-testid="tag-selector"] #tagsForm_tags') + .click() + .type(term1.name); + cy.get(`[data-testid="tag-${glossary1}.${term1.name}"]`).click(); + cy.get('[data-testid="tag-selector"]').should('contain', term1.name); // Select 2nd term - cy.get('[data-testid="tag-selector"] #tagsForm_tags').click().type(term2); - cy.get(`[data-testid="tag-${glossary}.${term2}"]`).click(); - cy.get('[data-testid="tag-selector"]').should('contain', term2); + cy.get('[data-testid="tag-selector"] #tagsForm_tags') + .click() + .type(term2.name); + cy.get(`[data-testid="tag-${glossary1}.${term2.name}"]`).click(); + cy.get('[data-testid="tag-selector"]').should('contain', term2.name); interceptURL('GET', '/api/v1/tags', 'tags'); interceptURL('PATCH', '/api/v1/tables/*', 'saveTag'); @@ -1115,7 +1007,7 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { cy.get('[data-testid="saveAssociatedTag"]').scrollIntoView().click(); verifyResponseStatusCode('@saveTag', 400); toastNotification( - `Tag labels ${glossary}.${term2} and ${glossary}.${term1} are mutually exclusive and can't be assigned together` + `Tag labels ${glossary1}.${term2.name} and ${glossary1}.${term1.name} are mutually exclusive and can't be assigned together` ); // Add non mutually exclusive tags @@ -1124,13 +1016,17 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { ).click(); // Select 1st term - cy.get('[data-testid="tag-selector"] #tagsForm_tags').click().type(term3); + cy.get('[data-testid="tag-selector"] #tagsForm_tags') + .click() + .type(term3.name); - cy.get(`[data-testid="tag-${glossary1}.${term3}"]`).click(); - cy.get('[data-testid="tag-selector"]').should('contain', term3); + cy.get(`[data-testid="tag-${glossary2}.${term3.name}"]`).click(); + cy.get('[data-testid="tag-selector"]').should('contain', term3.name); // Select 2nd term - cy.get('[data-testid="tag-selector"] #tagsForm_tags').click().type(term4); - cy.get(`[data-testid="tag-${glossary1}.${term4}"]`).click(); + cy.get('[data-testid="tag-selector"] #tagsForm_tags') + .click() + .type(term4.name); + cy.get(`[data-testid="tag-${glossary2}.${term4.name}"]`).click(); cy.clickOutside(); cy.get('[data-testid="saveAssociatedTag"]').scrollIntoView().click(); verifyResponseStatusCode('@saveTag', 200); @@ -1138,8 +1034,8 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { '[data-testid="entity-right-panel"] [data-testid="glossary-container"]' ) .scrollIntoView() - .should('contain', term3) - .should('contain', term4); + .should('contain', term3.name) + .should('contain', term4.name); cy.get( '[data-testid="entity-right-panel"] [data-testid="glossary-container"] [data-testid="icon"]' @@ -1151,13 +1047,13 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { cy.get(firstColumn).scrollIntoView(); cy.get(firstColumn).click(); - cy.get('[data-testid="tag-selector"]').click().type(term3); + cy.get('[data-testid="tag-selector"]').click().type(term3.name); cy.get( - `.ant-select-dropdown [data-testid="tag-${glossary1}.${term3}"]` + `.ant-select-dropdown [data-testid="tag-${glossary2}.${term3.name}"]` ).click(); cy.get('[data-testid="tag-selector"] > .ant-select-selector').contains( - term3 + term3.name ); cy.clickOutside(); cy.get('[data-testid="saveAssociatedTag"]').scrollIntoView().click(); @@ -1165,20 +1061,16 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { '[data-testid="glossary-tags-0"] > [data-testid="tags-wrapper"] > [data-testid="glossary-container"]' ) .scrollIntoView() - .should('contain', term3); + .should('contain', term3.name); cy.get( '[data-testid="glossary-tags-0"] > [data-testid="tags-wrapper"] > [data-testid="glossary-container"] [data-testid="icon"]' ).should('be.visible'); goToGlossaryPage(); - cy.get('.ant-menu-item').contains(NEW_GLOSSARY_1.name).click(); + cy.get('.ant-menu-item').contains(glossary2).click(); - goToAssetsTab( - NEW_GLOSSARY_1_TERMS.term_1.name, - NEW_GLOSSARY_1_TERMS.term_1.fullyQualifiedName, - false - ); + goToAssetsTab(term3.name, term3.fullyQualifiedName, false); cy.get('[data-testid="entity-header-display-name"]') .contains(entity.term) @@ -1186,31 +1078,21 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { }); it('Add asset to glossary term using asset modal', () => { - createGlossary(CYPRESS_ASSETS_GLOSSARY, false); - const terms = Object.values(CYPRESS_ASSETS_GLOSSARY_TERMS); - selectActiveGlossary(CYPRESS_ASSETS_GLOSSARY.name); - terms.forEach((term) => - createGlossaryTerm(term, CYPRESS_ASSETS_GLOSSARY, 'Approved', true, false) - ); - - terms.forEach((term) => { - addAssetToGlossaryTerm(term, CYPRESS_ASSETS_GLOSSARY); - }); + const term = GLOSSARY_3.terms[0]; + addAssetToGlossaryTerm(term, GLOSSARY_3); }); it('Remove asset from glossary term using asset modal', () => { - const terms = Object.values(CYPRESS_ASSETS_GLOSSARY_TERMS); - terms.forEach((term) => { - removeAssetsFromGlossaryTerm(term, CYPRESS_ASSETS_GLOSSARY); - }); + const term = GLOSSARY_3.terms[0]; + removeAssetsFromGlossaryTerm(term, GLOSSARY_3); }); it('Remove Glossary term from entity should work properly', () => { - const glossaryName = NEW_GLOSSARY_1.name; - const { name, fullyQualifiedName } = NEW_GLOSSARY_1_TERMS.term_1; + const glossaryName = GLOSSARY_2.name; + const { name, fullyQualifiedName } = GLOSSARY_2.terms[0]; const entity = SEARCH_ENTITY_TABLE.table_3; - selectActiveGlossary(NEW_GLOSSARY_1.name); + selectActiveGlossary(glossaryName); interceptURL('GET', '/api/v1/search/query*', 'assetTab'); // go assets tab @@ -1267,7 +1149,7 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { goToGlossaryPage(); - selectActiveGlossary(NEW_GLOSSARY_1.name); + selectActiveGlossary(glossaryName); goToAssetsTab(name, fullyQualifiedName); cy.contains('Adding a new Asset is easy, just give it a spin!').should( @@ -1276,20 +1158,7 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { }); it('Tags and entity summary columns should be sorted based on current Term Page', () => { - createGlossary(CYPRESS_ASSETS_GLOSSARY_1, false); - selectActiveGlossary(CYPRESS_ASSETS_GLOSSARY_1.name); - - const terms = Object.values(CYPRESS_ASSETS_GLOSSARY_TERMS_1); - terms.forEach((term) => - createGlossaryTerm( - term, - CYPRESS_ASSETS_GLOSSARY_1, - 'Approved', - true, - false - ) - ); - + const terms = GLOSSARY_3.terms; const entityTable = SEARCH_ENTITY_TABLE.table_1; visitEntityDetailsPage({ @@ -1305,7 +1174,7 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { }); goToGlossaryPage(); - selectActiveGlossary(CYPRESS_ASSETS_GLOSSARY_1.name); + selectActiveGlossary(GLOSSARY_3.name); goToAssetsTab(terms[0].name, terms[0].fullyQualifiedName, true); checkSummaryListItemSorting({ @@ -1322,9 +1191,9 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { 'fetchGlossaryTermData' ); - const parentTerm = CYPRESS_ASSETS_GLOSSARY_TERMS.term_1; - const childTerm = CYPRESS_ASSETS_GLOSSARY_TERMS.term_2; - selectActiveGlossary(CYPRESS_ASSETS_GLOSSARY.name); + const parentTerm = GLOSSARY_3.terms[0]; + const childTerm = GLOSSARY_3.terms[1]; + selectActiveGlossary(GLOSSARY_3.name); cy.get('[data-testid="expand-collapse-all-button"]').click(); visitGlossaryTermPage(childTerm.name, childTerm.fullyQualifiedName, true); @@ -1364,67 +1233,61 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { goToGlossaryPage(); - const newTermHierarchy = `${Cypress.$.escapeSelector( - CYPRESS_ASSETS_GLOSSARY.name - )}.${parentTerm.name}.${childTerm.name}`; - selectActiveGlossary(CYPRESS_ASSETS_GLOSSARY.name); + const newTermHierarchy = `${Cypress.$.escapeSelector(GLOSSARY_3.name)}.${ + parentTerm.name + }.${childTerm.name}`; + selectActiveGlossary(GLOSSARY_3.name); cy.get('[data-testid="expand-collapse-all-button"]').click(); // verify the term is moved under the parent term cy.get(`[data-row-key='${newTermHierarchy}']`).should('be.visible'); // re-dropping the term to the root level dragAndDropElement( - `${CYPRESS_ASSETS_GLOSSARY.name}.${parentTerm.name}.${childTerm.name}`, + `${GLOSSARY_3.name}.${parentTerm.name}.${childTerm.name}`, '.ant-table-thead > tr', true ); - confirmationDragAndDropGlossary( - childTerm.name, - CYPRESS_ASSETS_GLOSSARY.name, - true - ); + confirmationDragAndDropGlossary(childTerm.name, GLOSSARY_3.name, true); }); it('Drag and Drop should work properly for glossary term', () => { - selectActiveGlossary(NEW_GLOSSARY.name); + const { fullyQualifiedName: term1Fqn, name: term1Name } = + GLOSSARY_1.terms[0]; + const { fullyQualifiedName: term2Fqn, name: term2Name } = + GLOSSARY_1.terms[1]; - dragAndDropElement( - NEW_GLOSSARY_TERMS.term_2.fullyQualifiedName, - NEW_GLOSSARY_TERMS.term_1.fullyQualifiedName - ); + selectActiveGlossary(GLOSSARY_1.name); + dragAndDropElement(term2Fqn, term1Fqn); - confirmationDragAndDropGlossary( - NEW_GLOSSARY_TERMS.term_2.name, - NEW_GLOSSARY_TERMS.term_1.name - ); + confirmationDragAndDropGlossary(term2Name, term1Name); // clicking on the expand icon to view the child term cy.get( `[data-row-key=${Cypress.$.escapeSelector( - NEW_GLOSSARY_TERMS.term_1.fullyQualifiedName + term1Fqn )}] [data-testid="expand-icon"] > svg` ).click(); cy.get( `.ant-table-row-level-1[data-row-key="${Cypress.$.escapeSelector( - NEW_GLOSSARY_TERMS.term_1.fullyQualifiedName - )}.${NEW_GLOSSARY_TERMS.term_2.name}"]` + term1Fqn + )}.${term2Name}"]` ).should('be.visible'); }); it('Drag and Drop should work properly for glossary term at table level', () => { - selectActiveGlossary(NEW_GLOSSARY.name); + selectActiveGlossary(GLOSSARY_1.name); cy.get('[data-testid="expand-collapse-all-button"]').click(); dragAndDropElement( - `${NEW_GLOSSARY_TERMS.term_1.fullyQualifiedName}.${NEW_GLOSSARY_TERMS.term_2.name}`, + `${GLOSSARY_1.terms[0].fullyQualifiedName}.${GLOSSARY_1.terms[1].name}`, '.ant-table-thead > tr', true ); confirmationDragAndDropGlossary( - NEW_GLOSSARY_TERMS.term_2.name, - NEW_GLOSSARY.name, + GLOSSARY_1.terms[1].name, + GLOSSARY_1.name, true ); @@ -1432,29 +1295,19 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { cy.get('[data-testid="expand-collapse-all-button"]').click(); cy.get( `.ant-table-row-level-0[data-row-key="${Cypress.$.escapeSelector( - NEW_GLOSSARY_TERMS.term_2.fullyQualifiedName + GLOSSARY_1.terms[1].fullyQualifiedName )}"]` ).should('be.visible'); }); it('Delete glossary term should work properly', () => { - const terms = Object.values(NEW_GLOSSARY_TERMS); - selectActiveGlossary(NEW_GLOSSARY.name); - terms.forEach(deleteGlossaryTerm); - - // Glossary term for Product glossary - selectActiveGlossary(NEW_GLOSSARY_1.name); - Object.values(NEW_GLOSSARY_1_TERMS).forEach(deleteGlossaryTerm); + selectActiveGlossary(GLOSSARY_2.name); + GLOSSARY_2.terms.forEach(deleteGlossaryTerm); }); it('Delete glossary should work properly', () => { verifyResponseStatusCode('@fetchGlossaries', 200); - [ - NEW_GLOSSARY.name, - NEW_GLOSSARY_1.name, - CYPRESS_ASSETS_GLOSSARY.name, - CYPRESS_ASSETS_GLOSSARY_1.name, - ].forEach((glossary) => { + [GLOSSARY_1.name, GLOSSARY_2.name, GLOSSARY_3.name].forEach((glossary) => { deleteGlossary(glossary); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/GlossaryVersionPage.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/GlossaryVersionPage.spec.ts index 38516eef8a11..5d2975378099 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/GlossaryVersionPage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/GlossaryVersionPage.spec.ts @@ -13,7 +13,7 @@ import { interceptURL, verifyResponseStatusCode } from '../../common/common'; import { - addReviewer, + addOwnerInGlossary, removeReviewer, visitGlossaryPage, } from '../../common/GlossaryUtils'; @@ -245,7 +245,12 @@ describe( removeOwner(data.user.displayName, GLOSSARY_OWNER_LINK_TEST_ID); - addReviewer(data.reviewer.displayName, 'glossaries'); + addOwnerInGlossary( + [data.reviewer.displayName], + 'Add', + 'glossary-reviewer-name', + false + ); // Adding manual wait as the backend is now performing batch operations, // which causes a delay in reflecting changes @@ -397,7 +402,12 @@ describe( removeOwner(data.user.displayName, GLOSSARY_OWNER_LINK_TEST_ID); - addReviewer(data.reviewer.displayName, 'glossaryTerms'); + addOwnerInGlossary( + [data.reviewer.displayName], + 'Add', + 'glossary-reviewer-name', + false + ); interceptURL( 'GET', diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Teams.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Teams.spec.ts index f9e7713d0719..2cb6263556f5 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Teams.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Teams.spec.ts @@ -145,8 +145,8 @@ describe('Teams flow should work properly', { tags: 'Settings' }, () => { .find(`[title="${TEAM_DETAILS.username}"]`) .click(); cy.get('[data-testid="selectable-list"]') - .find(`[title="${TEAM_DETAILS.username}"] input[type='checkbox']`) - .should('be.checked'); + .find(`[title="${TEAM_DETAILS.username}"]`) + .should('have.class', 'active'); cy.get('[data-testid="selectable-list-update-btn"]').click(); verifyResponseStatusCode('@updateTeam', 200); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx index 19f7632afb9e..fd38d1600002 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/ActivityFeedTab/ActivityFeedTab.component.tsx @@ -81,6 +81,7 @@ export const ActivityFeedTab = ({ columns, entityType, refetchFeed, + hasGlossaryReviewer, entityFeedTotalCount, isForFeedTab = true, onUpdateFeedCount, @@ -510,6 +511,7 @@ export const ActivityFeedTab = ({ ) : ( void; onFeedUpdate: () => void; onUpdateEntityDetails?: () => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestSuite/TestSuiteList/TestSuites.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestSuite/TestSuiteList/TestSuites.component.tsx index 12c5a4a2dda6..c7e69552880b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestSuite/TestSuiteList/TestSuites.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestSuite/TestSuiteList/TestSuites.component.tsx @@ -285,7 +285,9 @@ export const TestSuites = ({ summaryPanel }: { summaryPanel: ReactNode }) => { + onUpdate={(updatedUser) => + handleOwnerSelect(updatedUser as EntityReference) + }> onUpdate(updatedUser as EntityReference)} /> )}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/SelectableList/SelectableList.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/SelectableList/SelectableList.component.tsx index fac0248e4b28..07033e52428f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/SelectableList/SelectableList.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/SelectableList/SelectableList.component.tsx @@ -10,8 +10,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { CheckOutlined } from '@ant-design/icons'; import Icon from '@ant-design/icons/lib/components/Icon'; -import { Button, Checkbox, List, Space, Tooltip } from 'antd'; +import { Button, List, Space, Tooltip } from 'antd'; +import classNames from 'classnames'; import { cloneDeep, isEmpty } from 'lodash'; import VirtualList from 'rc-virtual-list'; import React, { UIEventHandler, useCallback, useEffect, useState } from 'react'; @@ -67,11 +69,13 @@ export const SelectableList = ({ selectedItems, onUpdate, onCancel, + onChange, searchPlaceholder, customTagRenderer, searchBarDataTestId, removeIconTooltipLabel, emptyPlaceholderText, + height = ADD_USER_CONTAINER_HEIGHT, }: SelectableListProps) => { const [uniqueOptions, setUniqueOptions] = useState([]); const [searchText, setSearchText] = useState(''); @@ -157,8 +161,7 @@ export const SelectableList = ({ async (e) => { if ( // If user reachs to end of container fetch more options - e.currentTarget.scrollHeight - e.currentTarget.scrollTop === - ADD_USER_CONTAINER_HEIGHT && + e.currentTarget.scrollHeight - e.currentTarget.scrollTop === height && // If there are other options available which can be determine form the cursor value pagingInfo.after && // If we have all the options already we don't need to fetch more @@ -179,7 +182,7 @@ export const SelectableList = ({ const handleUpdate = useCallback( async (updateItems: EntityReference[]) => { setUpdating(true); - await onUpdate(updateItems); + await onUpdate?.(updateItems); setUpdating(false); }, [setUpdating, onUpdate] @@ -196,6 +199,10 @@ export const SelectableList = ({ newItemsMap?.set(id, item); } + const newSelectedItems = [...newItemsMap.values()]; + // Call onChange with the new selected items + onChange?.(newSelectedItems); + return newItemsMap; }); } else { @@ -213,6 +220,7 @@ export const SelectableList = ({ const handleClearAllClick = () => { setSelectedItemInternal(new Map()); + onChange?.([]); }; return ( @@ -223,6 +231,7 @@ export const SelectableList = ({ multiSelect && (