From 631a3f78440333af5a144bd479c6b5645cf9c16c Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Sun, 5 May 2024 11:20:53 -0700 Subject: [PATCH 001/117] Limits --- conf/openmetadata.yaml | 5 ++ .../service/OpenMetadataApplication.java | 30 +++++++++- .../OpenMetadataApplicationConfig.java | 4 ++ .../service/limits/DefaultLimits.java | 33 +++++++++++ .../openmetadata/service/limits/Limits.java | 19 +++++++ .../service/resources/CollectionRegistry.java | 47 +++++++++------- .../resources/limits/LimitsResource.java | 56 +++++++++++++++++++ .../service/util/OpenMetadataOperations.java | 2 +- .../configuration/limitsConfiguration.json | 27 +++++++++ .../json/schema/system/limitsResponse.json | 20 +++++++ 10 files changed, 221 insertions(+), 22 deletions(-) create mode 100644 openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java create mode 100644 openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java create mode 100644 openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java create mode 100644 openmetadata-spec/src/main/resources/json/schema/configuration/limitsConfiguration.json create mode 100644 openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json diff --git a/conf/openmetadata.yaml b/conf/openmetadata.yaml index 20e1e89fac73..e3328a02127e 100644 --- a/conf/openmetadata.yaml +++ b/conf/openmetadata.yaml @@ -367,6 +367,11 @@ email: password: ${SMTP_SERVER_PWD:-""} transportationStrategy: ${SMTP_SERVER_STRATEGY:-"SMTP_TLS"} +limits: + enable: ${LIMITS_ENABLED:-false} + className: ${LIMITS_CLASS_NAME:-"org.openmetadata.service.limits.DefaultLimits"} + limitsConfigFile: ${LIMITS_CONFIG_FILE:-""} + web: uriPath: ${WEB_CONF_URI_PATH:-"/api"} hsts: diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java index bb30c776b45c..e76a88b6dc8f 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java @@ -65,6 +65,7 @@ import org.openmetadata.schema.api.security.AuthenticationConfiguration; import org.openmetadata.schema.api.security.AuthorizerConfiguration; import org.openmetadata.schema.api.security.ClientType; +import org.openmetadata.schema.configuration.LimitsConfiguration; import org.openmetadata.schema.services.connections.metadata.AuthProvider; import org.openmetadata.service.apps.ApplicationHandler; import org.openmetadata.service.apps.scheduler.AppScheduler; @@ -84,6 +85,8 @@ import org.openmetadata.service.jdbi3.MigrationDAO; import org.openmetadata.service.jdbi3.locator.ConnectionAwareAnnotationSqlLocator; import org.openmetadata.service.jdbi3.locator.ConnectionType; +import org.openmetadata.service.limits.DefaultLimits; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.migration.Migration; import org.openmetadata.service.migration.MigrationValidationClient; import org.openmetadata.service.migration.api.MigrationWorkflow; @@ -131,6 +134,7 @@ public class OpenMetadataApplication extends Application { private Authorizer authorizer; private AuthenticatorHandler authenticatorHandler; + private Limits limits; protected Jdbi jdbi; @@ -201,6 +205,9 @@ public void run(OpenMetadataApplicationConfig catalogConfig, Environment environ // Register Authenticator registerAuthenticator(catalogConfig); + // Register Limits + registerLimits(catalogConfig); + // Unregister dropwizard default exception mappers ((DefaultServerFactory) catalogConfig.getServerFactory()) .setRegisterDefaultExceptionMappers(false); @@ -524,6 +531,27 @@ private void registerAuthenticator(OpenMetadataApplicationConfig catalogConfig) } } + private void registerLimits( + OpenMetadataApplicationConfig serverConfig) + throws NoSuchMethodException, + ClassNotFoundException, + IllegalAccessException, + InvocationTargetException, + InstantiationException { + LimitsConfiguration limitsConfiguration = serverConfig.getLimitsConfiguration(); + if (limitsConfiguration != null) { + limits = + Class.forName(limitsConfiguration.getClassName()) + .asSubclass(Limits.class) + .getConstructor() + .newInstance(); + } else { + LOG.info("Limits config not set, setting DefaultLimits"); + limits = new DefaultLimits(); + } + limits.init(limitsConfiguration, jdbi); + } + private void registerEventFilter( OpenMetadataApplicationConfig catalogConfig, Environment environment) { if (catalogConfig.getEventHandlerConfiguration() != null) { @@ -550,7 +578,7 @@ private void registerResources( OpenMetadataApplicationConfig config, Environment environment, Jdbi jdbi) { CollectionRegistry.initialize(); CollectionRegistry.getInstance() - .registerResources(jdbi, environment, config, authorizer, authenticatorHandler); + .registerResources(jdbi, environment, config, authorizer, authenticatorHandler, limits); environment.jersey().register(new JsonPatchProvider()); OMErrorPageHandler eph = new OMErrorPageHandler(config.getWebConfiguration()); eph.addErrorPage(Response.Status.NOT_FOUND.getStatusCode(), "/"); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplicationConfig.java b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplicationConfig.java index 47deae5398f9..a1be9b9587d6 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplicationConfig.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplicationConfig.java @@ -30,6 +30,7 @@ import org.openmetadata.schema.api.security.AuthenticationConfiguration; import org.openmetadata.schema.api.security.AuthorizerConfiguration; import org.openmetadata.schema.api.security.jwt.JWTTokenConfiguration; +import org.openmetadata.schema.configuration.LimitsConfiguration; import org.openmetadata.schema.email.SmtpSettings; import org.openmetadata.schema.security.secrets.SecretsManagerConfiguration; import org.openmetadata.schema.service.configuration.elasticsearch.ElasticSearchConfiguration; @@ -101,6 +102,9 @@ public class OpenMetadataApplicationConfig extends Configuration { @JsonProperty("applications") private AppsPrivateConfiguration appsPrivateConfiguration; + @JsonProperty("limits") + private LimitsConfiguration limitsConfiguration; + @Override public String toString() { return "catalogConfig{" diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java new file mode 100644 index 000000000000..39a1265da778 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java @@ -0,0 +1,33 @@ +package org.openmetadata.service.limits; + +import org.jdbi.v3.core.Jdbi; +import org.openmetadata.schema.configuration.LimitsConfiguration; +import org.openmetadata.schema.system.LimitsResponse; +import org.openmetadata.service.security.policyevaluator.OperationContext; +import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; + +import javax.ws.rs.core.SecurityContext; + +public class DefaultLimits implements Limits { + private LimitsConfiguration limitsConfiguration = null; + private Jdbi jdbi = null; + + @Override + public void init(LimitsConfiguration limitsConfiguration, Jdbi jdbi) { + this.limitsConfiguration = limitsConfiguration; + this.jdbi = jdbi; + } + + @Override + public void enforceLimits(SecurityContext securityContext, OperationContext operationContext, ResourceContextInterface resourceContext) { + // do not enforce limits + } + + @Override + public LimitsResponse getLimits() { + LimitsResponse limitsResponse = new LimitsResponse(); + limitsResponse.setEnable(limitsConfiguration.getEnable()); + return limitsResponse; + } + +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java new file mode 100644 index 000000000000..6d0732207c78 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java @@ -0,0 +1,19 @@ +package org.openmetadata.service.limits; + +import org.jdbi.v3.core.Jdbi; +import org.openmetadata.schema.configuration.LimitsConfiguration; +import org.openmetadata.schema.system.LimitsResponse; +import org.openmetadata.service.security.policyevaluator.OperationContext; +import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +public interface Limits { + void init(LimitsConfiguration limitsConfiguration, Jdbi jdbi); + + void enforceLimits(SecurityContext securityContext, OperationContext operationContext, + ResourceContextInterface resourceContext); + + LimitsResponse getLimits(); +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java index adddf7545d2a..79f63c3809d4 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java @@ -25,7 +25,6 @@ import java.lang.reflect.Method; import java.net.URI; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; @@ -40,6 +39,7 @@ import org.openmetadata.schema.type.CollectionDescriptor; import org.openmetadata.schema.type.CollectionInfo; import org.openmetadata.service.OpenMetadataApplicationConfig; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.security.Authorizer; import org.openmetadata.service.security.auth.AuthenticatorHandler; import org.openmetadata.service.util.ReflectionUtil; @@ -88,10 +88,6 @@ public static void initialize() { } } - public Map getCollectionMap() { - return Collections.unmodifiableMap(collectionMap); - } - /** * REST collections are described using *CollectionDescriptor.json Load all CollectionDescriptors from these files in * the classpath @@ -152,19 +148,20 @@ public void registerResources( Environment environment, OpenMetadataApplicationConfig config, Authorizer authorizer, - AuthenticatorHandler authenticatorHandler) { + AuthenticatorHandler authenticatorHandler, + Limits limits) { // Build list of ResourceDescriptors for (Map.Entry e : collectionMap.entrySet()) { CollectionDetails details = e.getValue(); String resourceClass = details.resourceClass; try { Object resource = - createResource(jdbi, resourceClass, config, authorizer, authenticatorHandler); + createResource(jdbi, resourceClass, config, authorizer, authenticatorHandler, limits); details.setResource(resource); environment.jersey().register(resource); LOG.info("Registering {} with order {}", resourceClass, details.order); } catch (Exception ex) { - LOG.warn("Failed to create resource for class {} {}", resourceClass, ex); + LOG.warn("Failed to create resource for class {} {}", resourceClass, ex.getMessage()); } } @@ -180,16 +177,16 @@ public void loadSeedData( Jdbi jdbi, OpenMetadataApplicationConfig config, Authorizer authorizer, - AuthenticatorHandler authenticatorHandler) { + AuthenticatorHandler authenticatorHandler, + Limits limits) { // Build list of ResourceDescriptors for (Map.Entry e : collectionMap.entrySet()) { CollectionDetails details = e.getValue(); String resourceClass = details.resourceClass; try { - Object resource = - createResource(jdbi, resourceClass, config, authorizer, authenticatorHandler); + createResource(jdbi, resourceClass, config, authorizer, authenticatorHandler, limits); } catch (Exception ex) { - LOG.warn("Failed to create resource for class {} {}", resourceClass, ex); + LOG.warn("Failed to create resource for class {} {}", resourceClass, ex.getMessage()); } } } @@ -236,7 +233,8 @@ private static Object createResource( String resourceClass, OpenMetadataApplicationConfig config, Authorizer authorizer, - AuthenticatorHandler authHandler) + AuthenticatorHandler authHandler, + Limits limits) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, @@ -248,19 +246,28 @@ private static Object createResource( // Create the resource identified by resourceClass try { - resource = clz.getDeclaredConstructor(Authorizer.class).newInstance(authorizer); + resource = clz.getDeclaredConstructor(Authorizer.class, Limits.class).newInstance(authorizer, limits); } catch (NoSuchMethodException e) { try { - resource = - clz.getDeclaredConstructor(Authorizer.class, AuthenticatorHandler.class) - .newInstance(authorizer, authHandler); + resource = clz.getDeclaredConstructor(Authorizer.class).newInstance(authorizer); } catch (NoSuchMethodException ex) { try { resource = - clz.getDeclaredConstructor(Jdbi.class, Authorizer.class) - .newInstance(jdbi, authorizer); + clz.getDeclaredConstructor(Authorizer.class, AuthenticatorHandler.class) + .newInstance(authorizer, authHandler); } catch (NoSuchMethodException exe) { - resource = Class.forName(resourceClass).getConstructor().newInstance(); + try { + resource = + clz.getDeclaredConstructor(Jdbi.class, Authorizer.class) + .newInstance(jdbi, authorizer); + } catch (NoSuchMethodException exec) { + try { + resource = + clz.getDeclaredConstructor(Limits.class).newInstance(limits); + } catch (NoSuchMethodException excep) { + resource = Class.forName(resourceClass).getConstructor().newInstance(); + } + } } } } catch (Exception ex) { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java new file mode 100644 index 000000000000..8d82858c1c24 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java @@ -0,0 +1,56 @@ +package org.openmetadata.service.resources.limits; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.openmetadata.schema.system.LimitsResponse; +import org.openmetadata.service.OpenMetadataApplicationConfig; +import org.openmetadata.service.limits.Limits; +import org.openmetadata.service.resources.Collection; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/v1/limits") +@Tag(name = "Limits", description = "APIs related to Limits configuration and settings.") +@Hidden +@Produces(MediaType.APPLICATION_JSON) +@Collection(name = "limits") +public class LimitsResource { + private OpenMetadataApplicationConfig openMetadataApplicationConfig; + + private Limits limits; + + public void initialize(OpenMetadataApplicationConfig config, Limits limits) { + this.openMetadataApplicationConfig = config; + this.limits = limits; + } + + @GET + @Path(("/config")) + @Operation( + operationId = "getLimitsConfiguration", + summary = "Get Limits configuration", + responses = { + @ApiResponse( + responseCode = "200", + description = "Limits configuration", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = LimitsResponse.class))) + }) + public LimitsResponse getAuthConfig() { + LimitsResponse limitsResponse = new LimitsResponse(); + if (limits != null) { + limitsResponse = limits.getLimits(); + } + return limitsResponse; + } + +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/OpenMetadataOperations.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/OpenMetadataOperations.java index 9c1d355bd760..57b992f405ba 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/util/OpenMetadataOperations.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/OpenMetadataOperations.java @@ -257,7 +257,7 @@ public Integer reIndex( CollectionRegistry.initialize(); ApplicationHandler.initialize(config); // load seed data so that repositories are initialized - CollectionRegistry.getInstance().loadSeedData(jdbi, config, null, null); + CollectionRegistry.getInstance().loadSeedData(jdbi, config, null, null, null); ApplicationHandler.initialize(config); // creates the default search index application AppScheduler.initialize(config, collectionDAO, searchRepository); diff --git a/openmetadata-spec/src/main/resources/json/schema/configuration/limitsConfiguration.json b/openmetadata-spec/src/main/resources/json/schema/configuration/limitsConfiguration.json new file mode 100644 index 000000000000..e9ee55cd7c07 --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/configuration/limitsConfiguration.json @@ -0,0 +1,27 @@ +{ + "$id": "https://open-metadata.org/schema/entity/configuration/limitsConfiguration.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FernetConfiguration", + "description": "This schema defines the Limits Configuration.", + "type": "object", + "javaType": "org.openmetadata.schema.configuration.LimitsConfiguration", + "properties": { + "className": { + "description": "Class Name for authorizer.", + "type": "string", + "default": "org.openmetadata.service.limits.DefaultLimits" + }, + "enable": { + "description": "Limits Enabled or Disabled.", + "type": "boolean", + "default": false + }, + "limitsConfigFile": { + "description": "Limits Configuration File.", + "type": "string", + "default": "limits-config.yaml" + } + }, + "required": ["enable"], + "additionalProperties": false +} \ No newline at end of file diff --git a/openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json b/openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json new file mode 100644 index 000000000000..f4d9ccb51771 --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json @@ -0,0 +1,20 @@ +{ + "$id": "https://open-metadata.org/schema/system/limitsResponse.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LimitsResponse", + "description": "Limits response schema", + "type": "object", + "javaType": "org.openmetadata.schema.system.LimitsResponse", + "properties": { + "enable": { + "description": "Limits Enabled", + "type": "boolean", + "default": false + }, + "limits": { + "description": "Limits", + "type": "object" + } + }, + "additionalProperties": false +} \ No newline at end of file From ff5b4c5b733e408284395d69716cb38ca24868ce Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Sun, 5 May 2024 17:13:17 -0700 Subject: [PATCH 002/117] Limits --- .../service/OpenMetadataApplication.java | 13 +++++----- .../service/limits/DefaultLimits.java | 21 ++++++++-------- .../openmetadata/service/limits/Limits.java | 10 ++++---- .../service/resources/CollectionRegistry.java | 7 +++--- .../resources/limits/LimitsResource.java | 24 +++++++++---------- 5 files changed, 37 insertions(+), 38 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java index e76a88b6dc8f..65d762ab2500 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java @@ -531,13 +531,12 @@ private void registerAuthenticator(OpenMetadataApplicationConfig catalogConfig) } } - private void registerLimits( - OpenMetadataApplicationConfig serverConfig) + private void registerLimits(OpenMetadataApplicationConfig serverConfig) throws NoSuchMethodException, - ClassNotFoundException, - IllegalAccessException, - InvocationTargetException, - InstantiationException { + ClassNotFoundException, + IllegalAccessException, + InvocationTargetException, + InstantiationException { LimitsConfiguration limitsConfiguration = serverConfig.getLimitsConfiguration(); if (limitsConfiguration != null) { limits = @@ -545,7 +544,7 @@ private void registerLimits( .asSubclass(Limits.class) .getConstructor() .newInstance(); - } else { + } else { LOG.info("Limits config not set, setting DefaultLimits"); limits = new DefaultLimits(); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java index 39a1265da778..4c71f6c36985 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java @@ -1,25 +1,27 @@ package org.openmetadata.service.limits; +import javax.ws.rs.core.SecurityContext; import org.jdbi.v3.core.Jdbi; import org.openmetadata.schema.configuration.LimitsConfiguration; import org.openmetadata.schema.system.LimitsResponse; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; -import javax.ws.rs.core.SecurityContext; - public class DefaultLimits implements Limits { - private LimitsConfiguration limitsConfiguration = null; - private Jdbi jdbi = null; + private LimitsConfiguration limitsConfiguration = null; + private Jdbi jdbi = null; @Override - public void init(LimitsConfiguration limitsConfiguration, Jdbi jdbi) { - this.limitsConfiguration = limitsConfiguration; - this.jdbi = jdbi; - } + public void init(LimitsConfiguration limitsConfiguration, Jdbi jdbi) { + this.limitsConfiguration = limitsConfiguration; + this.jdbi = jdbi; + } @Override - public void enforceLimits(SecurityContext securityContext, OperationContext operationContext, ResourceContextInterface resourceContext) { + public void enforceLimits( + SecurityContext securityContext, + OperationContext operationContext, + ResourceContextInterface resourceContext) { // do not enforce limits } @@ -29,5 +31,4 @@ public LimitsResponse getLimits() { limitsResponse.setEnable(limitsConfiguration.getEnable()); return limitsResponse; } - } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java index 6d0732207c78..995a11b00c6a 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java @@ -1,19 +1,19 @@ package org.openmetadata.service.limits; +import javax.ws.rs.core.SecurityContext; import org.jdbi.v3.core.Jdbi; import org.openmetadata.schema.configuration.LimitsConfiguration; import org.openmetadata.schema.system.LimitsResponse; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; - public interface Limits { void init(LimitsConfiguration limitsConfiguration, Jdbi jdbi); - void enforceLimits(SecurityContext securityContext, OperationContext operationContext, - ResourceContextInterface resourceContext); + void enforceLimits( + SecurityContext securityContext, + OperationContext operationContext, + ResourceContextInterface resourceContext); LimitsResponse getLimits(); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java index 79f63c3809d4..e70bc5ff5609 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java @@ -246,7 +246,9 @@ private static Object createResource( // Create the resource identified by resourceClass try { - resource = clz.getDeclaredConstructor(Authorizer.class, Limits.class).newInstance(authorizer, limits); + resource = + clz.getDeclaredConstructor(Authorizer.class, Limits.class) + .newInstance(authorizer, limits); } catch (NoSuchMethodException e) { try { resource = clz.getDeclaredConstructor(Authorizer.class).newInstance(authorizer); @@ -262,8 +264,7 @@ private static Object createResource( .newInstance(jdbi, authorizer); } catch (NoSuchMethodException exec) { try { - resource = - clz.getDeclaredConstructor(Limits.class).newInstance(limits); + resource = clz.getDeclaredConstructor(Limits.class).newInstance(limits); } catch (NoSuchMethodException excep) { resource = Class.forName(resourceClass).getConstructor().newInstance(); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java index 8d82858c1c24..21d5fc5bcb4f 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java @@ -6,15 +6,14 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import org.openmetadata.schema.system.LimitsResponse; -import org.openmetadata.service.OpenMetadataApplicationConfig; -import org.openmetadata.service.limits.Limits; -import org.openmetadata.service.resources.Collection; - import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; +import org.openmetadata.schema.system.LimitsResponse; +import org.openmetadata.service.OpenMetadataApplicationConfig; +import org.openmetadata.service.limits.Limits; +import org.openmetadata.service.resources.Collection; @Path("/v1/limits") @Tag(name = "Limits", description = "APIs related to Limits configuration and settings.") @@ -37,13 +36,13 @@ public void initialize(OpenMetadataApplicationConfig config, Limits limits) { operationId = "getLimitsConfiguration", summary = "Get Limits configuration", responses = { - @ApiResponse( - responseCode = "200", - description = "Limits configuration", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = LimitsResponse.class))) + @ApiResponse( + responseCode = "200", + description = "Limits configuration", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = LimitsResponse.class))) }) public LimitsResponse getAuthConfig() { LimitsResponse limitsResponse = new LimitsResponse(); @@ -52,5 +51,4 @@ public LimitsResponse getAuthConfig() { } return limitsResponse; } - } From efca50b0ff93cae4f0cacc919049592265e1049e Mon Sep 17 00:00:00 2001 From: mohitdeuex Date: Mon, 6 May 2024 17:19:13 +0530 Subject: [PATCH 003/117] - Mismatched Types --- .../openmetadata/service/resources/CollectionRegistry.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java index e70bc5ff5609..054a14f28ef0 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java @@ -247,8 +247,8 @@ private static Object createResource( // Create the resource identified by resourceClass try { resource = - clz.getDeclaredConstructor(Authorizer.class, Limits.class) - .newInstance(authorizer, limits); + clz.getDeclaredConstructor(OpenMetadataApplicationConfig.class, Limits.class) + .newInstance(config, limits); } catch (NoSuchMethodException e) { try { resource = clz.getDeclaredConstructor(Authorizer.class).newInstance(authorizer); From 31fe69e01c78f5300c63ab7884d7452282ac03d7 Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Mon, 6 May 2024 20:58:00 -0700 Subject: [PATCH 004/117] Update Limits config response --- .../service/limits/DefaultLimits.java | 10 +++++----- .../openmetadata/service/limits/Limits.java | 4 ++-- .../resources/limits/LimitsResource.java | 20 ++++++++----------- .../json/schema/system/limitsResponse.json | 4 ++-- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java index 4c71f6c36985..d7c1a0a03e5f 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java @@ -3,7 +3,7 @@ import javax.ws.rs.core.SecurityContext; import org.jdbi.v3.core.Jdbi; import org.openmetadata.schema.configuration.LimitsConfiguration; -import org.openmetadata.schema.system.LimitsResponse; +import org.openmetadata.schema.system.LimitsConfig; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; @@ -26,9 +26,9 @@ public void enforceLimits( } @Override - public LimitsResponse getLimits() { - LimitsResponse limitsResponse = new LimitsResponse(); - limitsResponse.setEnable(limitsConfiguration.getEnable()); - return limitsResponse; + public LimitsConfig getLimitsConfig() { + LimitsConfig limitsConfig = new LimitsConfig(); + limitsConfig.setEnable(limitsConfiguration.getEnable()); + return limitsConfig; } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java index 995a11b00c6a..7e90ea1ff35f 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java @@ -3,7 +3,7 @@ import javax.ws.rs.core.SecurityContext; import org.jdbi.v3.core.Jdbi; import org.openmetadata.schema.configuration.LimitsConfiguration; -import org.openmetadata.schema.system.LimitsResponse; +import org.openmetadata.schema.system.LimitsConfig; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; @@ -15,5 +15,5 @@ void enforceLimits( OperationContext operationContext, ResourceContextInterface resourceContext); - LimitsResponse getLimits(); + LimitsConfig getLimitsConfig(); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java index 21d5fc5bcb4f..ea467a3f3a73 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java @@ -10,8 +10,7 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import org.openmetadata.schema.system.LimitsResponse; -import org.openmetadata.service.OpenMetadataApplicationConfig; +import org.openmetadata.schema.system.LimitsConfig; import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; @@ -21,12 +20,9 @@ @Produces(MediaType.APPLICATION_JSON) @Collection(name = "limits") public class LimitsResource { - private OpenMetadataApplicationConfig openMetadataApplicationConfig; + private final Limits limits; - private Limits limits; - - public void initialize(OpenMetadataApplicationConfig config, Limits limits) { - this.openMetadataApplicationConfig = config; + public LimitsResource(Limits limits) { this.limits = limits; } @@ -42,13 +38,13 @@ public void initialize(OpenMetadataApplicationConfig config, Limits limits) { content = @Content( mediaType = "application/json", - schema = @Schema(implementation = LimitsResponse.class))) + schema = @Schema(implementation = LimitsConfig.class))) }) - public LimitsResponse getAuthConfig() { - LimitsResponse limitsResponse = new LimitsResponse(); + public LimitsConfig getAuthConfig() { + LimitsConfig limitsConfig = new LimitsConfig(); if (limits != null) { - limitsResponse = limits.getLimits(); + limitsConfig = limits.getLimitsConfig(); } - return limitsResponse; + return limitsConfig; } } diff --git a/openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json b/openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json index f4d9ccb51771..60a907458be9 100644 --- a/openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json +++ b/openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json @@ -2,9 +2,9 @@ "$id": "https://open-metadata.org/schema/system/limitsResponse.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "LimitsResponse", - "description": "Limits response schema", + "description": "Limits Config schema", "type": "object", - "javaType": "org.openmetadata.schema.system.LimitsResponse", + "javaType": "org.openmetadata.schema.system.LimitsConfig", "properties": { "enable": { "description": "Limits Enabled", From b400f0f7c2b263d5bb84abb51bcb349530badf55 Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Tue, 7 May 2024 22:51:34 -0700 Subject: [PATCH 005/117] Update Limits feature response --- .../service/OpenMetadataApplication.java | 2 +- .../service/limits/DefaultLimits.java | 13 +++++++-- .../openmetadata/service/limits/Limits.java | 7 +++-- .../resources/limits/LimitsResource.java | 28 ++++++++++++++++++- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java index 65d762ab2500..b94d38ac67da 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java @@ -548,7 +548,7 @@ private void registerLimits(OpenMetadataApplicationConfig serverConfig) LOG.info("Limits config not set, setting DefaultLimits"); limits = new DefaultLimits(); } - limits.init(limitsConfiguration, jdbi); + limits.init(serverConfig, jdbi); } private void registerEventFilter( diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java index d7c1a0a03e5f..f9a406d505fe 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java @@ -1,19 +1,23 @@ package org.openmetadata.service.limits; +import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import org.jdbi.v3.core.Jdbi; import org.openmetadata.schema.configuration.LimitsConfiguration; import org.openmetadata.schema.system.LimitsConfig; +import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; public class DefaultLimits implements Limits { + private OpenMetadataApplicationConfig serverConfig = null; private LimitsConfiguration limitsConfiguration = null; private Jdbi jdbi = null; @Override - public void init(LimitsConfiguration limitsConfiguration, Jdbi jdbi) { - this.limitsConfiguration = limitsConfiguration; + public void init(OpenMetadataApplicationConfig serverConfig, Jdbi jdbi) { + this.serverConfig = serverConfig; + this.limitsConfiguration = serverConfig.getLimitsConfiguration(); this.jdbi = jdbi; } @@ -31,4 +35,9 @@ public LimitsConfig getLimitsConfig() { limitsConfig.setEnable(limitsConfiguration.getEnable()); return limitsConfig; } + + @Override + public Response getLimitsForaFeature(String name) { + return Response.ok().build(); + } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java index 7e90ea1ff35f..955873bd702d 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java @@ -1,14 +1,15 @@ package org.openmetadata.service.limits; +import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import org.jdbi.v3.core.Jdbi; -import org.openmetadata.schema.configuration.LimitsConfiguration; import org.openmetadata.schema.system.LimitsConfig; +import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; public interface Limits { - void init(LimitsConfiguration limitsConfiguration, Jdbi jdbi); + void init(OpenMetadataApplicationConfig serverConfig, Jdbi jdbi); void enforceLimits( SecurityContext securityContext, @@ -16,4 +17,6 @@ void enforceLimits( ResourceContextInterface resourceContext); LimitsConfig getLimitsConfig(); + + Response getLimitsForaFeature(String name); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java index ea467a3f3a73..e692697b21f4 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java @@ -2,15 +2,22 @@ import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import javax.ws.rs.GET; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriInfo; import org.openmetadata.schema.system.LimitsConfig; +import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; @@ -21,9 +28,28 @@ @Collection(name = "limits") public class LimitsResource { private final Limits limits; + private final OpenMetadataApplicationConfig config; - public LimitsResource(Limits limits) { + public LimitsResource(OpenMetadataApplicationConfig config, Limits limits) { this.limits = limits; + this.config = config; + } + + @GET + @Path("/features/{name}") + @Operation( + operationId = "getLimitsForaFeature", + summary = "Get Limits configuration for a feature", + responses = { + @ApiResponse(responseCode = "200", description = "Limits configuration for a feature") + }) + public Response getLimitsForaFeature( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "Name of the Feature", schema = @Schema(type = "string")) + @PathParam("name") + String name) { + return limits.getLimitsForaFeature(name); } @GET From 3ce92fd51767dff4108051b631de1d861e0d70bc Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Sun, 5 May 2024 11:20:53 -0700 Subject: [PATCH 006/117] Limits --- conf/openmetadata.yaml | 5 ++ .../service/OpenMetadataApplication.java | 30 +++++++++- .../OpenMetadataApplicationConfig.java | 4 ++ .../service/limits/DefaultLimits.java | 33 +++++++++++ .../openmetadata/service/limits/Limits.java | 19 +++++++ .../service/resources/CollectionRegistry.java | 45 ++++++++------- .../resources/limits/LimitsResource.java | 56 +++++++++++++++++++ .../service/util/OpenMetadataOperations.java | 2 +- .../configuration/limitsConfiguration.json | 27 +++++++++ .../json/schema/system/limitsResponse.json | 20 +++++++ 10 files changed, 220 insertions(+), 21 deletions(-) create mode 100644 openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java create mode 100644 openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java create mode 100644 openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java create mode 100644 openmetadata-spec/src/main/resources/json/schema/configuration/limitsConfiguration.json create mode 100644 openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json diff --git a/conf/openmetadata.yaml b/conf/openmetadata.yaml index 20e1e89fac73..e3328a02127e 100644 --- a/conf/openmetadata.yaml +++ b/conf/openmetadata.yaml @@ -367,6 +367,11 @@ email: password: ${SMTP_SERVER_PWD:-""} transportationStrategy: ${SMTP_SERVER_STRATEGY:-"SMTP_TLS"} +limits: + enable: ${LIMITS_ENABLED:-false} + className: ${LIMITS_CLASS_NAME:-"org.openmetadata.service.limits.DefaultLimits"} + limitsConfigFile: ${LIMITS_CONFIG_FILE:-""} + web: uriPath: ${WEB_CONF_URI_PATH:-"/api"} hsts: diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java index bb30c776b45c..e76a88b6dc8f 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java @@ -65,6 +65,7 @@ import org.openmetadata.schema.api.security.AuthenticationConfiguration; import org.openmetadata.schema.api.security.AuthorizerConfiguration; import org.openmetadata.schema.api.security.ClientType; +import org.openmetadata.schema.configuration.LimitsConfiguration; import org.openmetadata.schema.services.connections.metadata.AuthProvider; import org.openmetadata.service.apps.ApplicationHandler; import org.openmetadata.service.apps.scheduler.AppScheduler; @@ -84,6 +85,8 @@ import org.openmetadata.service.jdbi3.MigrationDAO; import org.openmetadata.service.jdbi3.locator.ConnectionAwareAnnotationSqlLocator; import org.openmetadata.service.jdbi3.locator.ConnectionType; +import org.openmetadata.service.limits.DefaultLimits; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.migration.Migration; import org.openmetadata.service.migration.MigrationValidationClient; import org.openmetadata.service.migration.api.MigrationWorkflow; @@ -131,6 +134,7 @@ public class OpenMetadataApplication extends Application { private Authorizer authorizer; private AuthenticatorHandler authenticatorHandler; + private Limits limits; protected Jdbi jdbi; @@ -201,6 +205,9 @@ public void run(OpenMetadataApplicationConfig catalogConfig, Environment environ // Register Authenticator registerAuthenticator(catalogConfig); + // Register Limits + registerLimits(catalogConfig); + // Unregister dropwizard default exception mappers ((DefaultServerFactory) catalogConfig.getServerFactory()) .setRegisterDefaultExceptionMappers(false); @@ -524,6 +531,27 @@ private void registerAuthenticator(OpenMetadataApplicationConfig catalogConfig) } } + private void registerLimits( + OpenMetadataApplicationConfig serverConfig) + throws NoSuchMethodException, + ClassNotFoundException, + IllegalAccessException, + InvocationTargetException, + InstantiationException { + LimitsConfiguration limitsConfiguration = serverConfig.getLimitsConfiguration(); + if (limitsConfiguration != null) { + limits = + Class.forName(limitsConfiguration.getClassName()) + .asSubclass(Limits.class) + .getConstructor() + .newInstance(); + } else { + LOG.info("Limits config not set, setting DefaultLimits"); + limits = new DefaultLimits(); + } + limits.init(limitsConfiguration, jdbi); + } + private void registerEventFilter( OpenMetadataApplicationConfig catalogConfig, Environment environment) { if (catalogConfig.getEventHandlerConfiguration() != null) { @@ -550,7 +578,7 @@ private void registerResources( OpenMetadataApplicationConfig config, Environment environment, Jdbi jdbi) { CollectionRegistry.initialize(); CollectionRegistry.getInstance() - .registerResources(jdbi, environment, config, authorizer, authenticatorHandler); + .registerResources(jdbi, environment, config, authorizer, authenticatorHandler, limits); environment.jersey().register(new JsonPatchProvider()); OMErrorPageHandler eph = new OMErrorPageHandler(config.getWebConfiguration()); eph.addErrorPage(Response.Status.NOT_FOUND.getStatusCode(), "/"); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplicationConfig.java b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplicationConfig.java index 47deae5398f9..a1be9b9587d6 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplicationConfig.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplicationConfig.java @@ -30,6 +30,7 @@ import org.openmetadata.schema.api.security.AuthenticationConfiguration; import org.openmetadata.schema.api.security.AuthorizerConfiguration; import org.openmetadata.schema.api.security.jwt.JWTTokenConfiguration; +import org.openmetadata.schema.configuration.LimitsConfiguration; import org.openmetadata.schema.email.SmtpSettings; import org.openmetadata.schema.security.secrets.SecretsManagerConfiguration; import org.openmetadata.schema.service.configuration.elasticsearch.ElasticSearchConfiguration; @@ -101,6 +102,9 @@ public class OpenMetadataApplicationConfig extends Configuration { @JsonProperty("applications") private AppsPrivateConfiguration appsPrivateConfiguration; + @JsonProperty("limits") + private LimitsConfiguration limitsConfiguration; + @Override public String toString() { return "catalogConfig{" diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java new file mode 100644 index 000000000000..39a1265da778 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java @@ -0,0 +1,33 @@ +package org.openmetadata.service.limits; + +import org.jdbi.v3.core.Jdbi; +import org.openmetadata.schema.configuration.LimitsConfiguration; +import org.openmetadata.schema.system.LimitsResponse; +import org.openmetadata.service.security.policyevaluator.OperationContext; +import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; + +import javax.ws.rs.core.SecurityContext; + +public class DefaultLimits implements Limits { + private LimitsConfiguration limitsConfiguration = null; + private Jdbi jdbi = null; + + @Override + public void init(LimitsConfiguration limitsConfiguration, Jdbi jdbi) { + this.limitsConfiguration = limitsConfiguration; + this.jdbi = jdbi; + } + + @Override + public void enforceLimits(SecurityContext securityContext, OperationContext operationContext, ResourceContextInterface resourceContext) { + // do not enforce limits + } + + @Override + public LimitsResponse getLimits() { + LimitsResponse limitsResponse = new LimitsResponse(); + limitsResponse.setEnable(limitsConfiguration.getEnable()); + return limitsResponse; + } + +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java new file mode 100644 index 000000000000..6d0732207c78 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java @@ -0,0 +1,19 @@ +package org.openmetadata.service.limits; + +import org.jdbi.v3.core.Jdbi; +import org.openmetadata.schema.configuration.LimitsConfiguration; +import org.openmetadata.schema.system.LimitsResponse; +import org.openmetadata.service.security.policyevaluator.OperationContext; +import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +public interface Limits { + void init(LimitsConfiguration limitsConfiguration, Jdbi jdbi); + + void enforceLimits(SecurityContext securityContext, OperationContext operationContext, + ResourceContextInterface resourceContext); + + LimitsResponse getLimits(); +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java index aefab4259b5f..916a1ab265b4 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java @@ -25,7 +25,6 @@ import java.lang.reflect.Method; import java.net.URI; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; @@ -40,6 +39,7 @@ import org.openmetadata.schema.type.CollectionDescriptor; import org.openmetadata.schema.type.CollectionInfo; import org.openmetadata.service.OpenMetadataApplicationConfig; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.security.Authorizer; import org.openmetadata.service.security.auth.AuthenticatorHandler; import org.openmetadata.service.util.ReflectionUtil; @@ -88,10 +88,6 @@ public static void initialize() { } } - public Map getCollectionMap() { - return Collections.unmodifiableMap(collectionMap); - } - /** * REST collections are described using *CollectionDescriptor.json Load all CollectionDescriptors from these files in * the classpath @@ -152,19 +148,20 @@ public void registerResources( Environment environment, OpenMetadataApplicationConfig config, Authorizer authorizer, - AuthenticatorHandler authenticatorHandler) { + AuthenticatorHandler authenticatorHandler, + Limits limits) { // Build list of ResourceDescriptors for (Map.Entry e : collectionMap.entrySet()) { CollectionDetails details = e.getValue(); String resourceClass = details.resourceClass; try { Object resource = - createResource(jdbi, resourceClass, config, authorizer, authenticatorHandler); + createResource(jdbi, resourceClass, config, authorizer, authenticatorHandler, limits); details.setResource(resource); environment.jersey().register(resource); LOG.info("Registering {} with order {}", resourceClass, details.order); } catch (Exception ex) { - LOG.warn("Failed to create resource for class {} {}", resourceClass, ex); + LOG.warn("Failed to create resource for class {} {}", resourceClass, ex.getMessage()); } } @@ -181,15 +178,15 @@ public void loadSeedData( OpenMetadataApplicationConfig config, Authorizer authorizer, AuthenticatorHandler authenticatorHandler, - boolean isOperations) { + Limits limits, + boolean isOperations) { // Build list of ResourceDescriptors for (Map.Entry e : collectionMap.entrySet()) { CollectionDetails details = e.getValue(); if (!isOperations || (isOperations && details.requiredForOps)) { String resourceClass = details.resourceClass; try { - Object resource = - createResource(jdbi, resourceClass, config, authorizer, authenticatorHandler); + createResource(jdbi, resourceClass, config, authorizer, authenticatorHandler, limits); } catch (Exception ex) { LOG.warn("Failed to create resource for class {} {}", resourceClass, ex); } @@ -241,7 +238,8 @@ private static Object createResource( String resourceClass, OpenMetadataApplicationConfig config, Authorizer authorizer, - AuthenticatorHandler authHandler) + AuthenticatorHandler authHandler, + Limits limits) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, @@ -253,19 +251,28 @@ private static Object createResource( // Create the resource identified by resourceClass try { - resource = clz.getDeclaredConstructor(Authorizer.class).newInstance(authorizer); + resource = clz.getDeclaredConstructor(Authorizer.class, Limits.class).newInstance(authorizer, limits); } catch (NoSuchMethodException e) { try { - resource = - clz.getDeclaredConstructor(Authorizer.class, AuthenticatorHandler.class) - .newInstance(authorizer, authHandler); + resource = clz.getDeclaredConstructor(Authorizer.class).newInstance(authorizer); } catch (NoSuchMethodException ex) { try { resource = - clz.getDeclaredConstructor(Jdbi.class, Authorizer.class) - .newInstance(jdbi, authorizer); + clz.getDeclaredConstructor(Authorizer.class, AuthenticatorHandler.class) + .newInstance(authorizer, authHandler); } catch (NoSuchMethodException exe) { - resource = Class.forName(resourceClass).getConstructor().newInstance(); + try { + resource = + clz.getDeclaredConstructor(Jdbi.class, Authorizer.class) + .newInstance(jdbi, authorizer); + } catch (NoSuchMethodException exec) { + try { + resource = + clz.getDeclaredConstructor(Limits.class).newInstance(limits); + } catch (NoSuchMethodException excep) { + resource = Class.forName(resourceClass).getConstructor().newInstance(); + } + } } } } catch (Exception ex) { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java new file mode 100644 index 000000000000..8d82858c1c24 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java @@ -0,0 +1,56 @@ +package org.openmetadata.service.resources.limits; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.openmetadata.schema.system.LimitsResponse; +import org.openmetadata.service.OpenMetadataApplicationConfig; +import org.openmetadata.service.limits.Limits; +import org.openmetadata.service.resources.Collection; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/v1/limits") +@Tag(name = "Limits", description = "APIs related to Limits configuration and settings.") +@Hidden +@Produces(MediaType.APPLICATION_JSON) +@Collection(name = "limits") +public class LimitsResource { + private OpenMetadataApplicationConfig openMetadataApplicationConfig; + + private Limits limits; + + public void initialize(OpenMetadataApplicationConfig config, Limits limits) { + this.openMetadataApplicationConfig = config; + this.limits = limits; + } + + @GET + @Path(("/config")) + @Operation( + operationId = "getLimitsConfiguration", + summary = "Get Limits configuration", + responses = { + @ApiResponse( + responseCode = "200", + description = "Limits configuration", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = LimitsResponse.class))) + }) + public LimitsResponse getAuthConfig() { + LimitsResponse limitsResponse = new LimitsResponse(); + if (limits != null) { + limitsResponse = limits.getLimits(); + } + return limitsResponse; + } + +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/OpenMetadataOperations.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/OpenMetadataOperations.java index 32744eeff5c1..bc8a1af31f72 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/util/OpenMetadataOperations.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/OpenMetadataOperations.java @@ -254,7 +254,7 @@ public Integer reIndex( CollectionRegistry.initialize(); ApplicationHandler.initialize(config); // load seed data so that repositories are initialized - CollectionRegistry.getInstance().loadSeedData(jdbi, config, null, null, true); + CollectionRegistry.getInstance().loadSeedData(jdbi, config, null, null, null, true); ApplicationHandler.initialize(config); // creates the default search index application AppScheduler.initialize(config, collectionDAO, searchRepository); diff --git a/openmetadata-spec/src/main/resources/json/schema/configuration/limitsConfiguration.json b/openmetadata-spec/src/main/resources/json/schema/configuration/limitsConfiguration.json new file mode 100644 index 000000000000..e9ee55cd7c07 --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/configuration/limitsConfiguration.json @@ -0,0 +1,27 @@ +{ + "$id": "https://open-metadata.org/schema/entity/configuration/limitsConfiguration.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FernetConfiguration", + "description": "This schema defines the Limits Configuration.", + "type": "object", + "javaType": "org.openmetadata.schema.configuration.LimitsConfiguration", + "properties": { + "className": { + "description": "Class Name for authorizer.", + "type": "string", + "default": "org.openmetadata.service.limits.DefaultLimits" + }, + "enable": { + "description": "Limits Enabled or Disabled.", + "type": "boolean", + "default": false + }, + "limitsConfigFile": { + "description": "Limits Configuration File.", + "type": "string", + "default": "limits-config.yaml" + } + }, + "required": ["enable"], + "additionalProperties": false +} \ No newline at end of file diff --git a/openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json b/openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json new file mode 100644 index 000000000000..f4d9ccb51771 --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json @@ -0,0 +1,20 @@ +{ + "$id": "https://open-metadata.org/schema/system/limitsResponse.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "LimitsResponse", + "description": "Limits response schema", + "type": "object", + "javaType": "org.openmetadata.schema.system.LimitsResponse", + "properties": { + "enable": { + "description": "Limits Enabled", + "type": "boolean", + "default": false + }, + "limits": { + "description": "Limits", + "type": "object" + } + }, + "additionalProperties": false +} \ No newline at end of file From 94dd285fb90b436e8f962e3fdd4be8961f26e21d Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Sun, 5 May 2024 17:13:17 -0700 Subject: [PATCH 007/117] Limits --- .../service/OpenMetadataApplication.java | 13 +++++----- .../service/limits/DefaultLimits.java | 21 ++++++++-------- .../openmetadata/service/limits/Limits.java | 10 ++++---- .../service/resources/CollectionRegistry.java | 7 +++--- .../resources/limits/LimitsResource.java | 24 +++++++++---------- 5 files changed, 37 insertions(+), 38 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java index e76a88b6dc8f..65d762ab2500 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java @@ -531,13 +531,12 @@ private void registerAuthenticator(OpenMetadataApplicationConfig catalogConfig) } } - private void registerLimits( - OpenMetadataApplicationConfig serverConfig) + private void registerLimits(OpenMetadataApplicationConfig serverConfig) throws NoSuchMethodException, - ClassNotFoundException, - IllegalAccessException, - InvocationTargetException, - InstantiationException { + ClassNotFoundException, + IllegalAccessException, + InvocationTargetException, + InstantiationException { LimitsConfiguration limitsConfiguration = serverConfig.getLimitsConfiguration(); if (limitsConfiguration != null) { limits = @@ -545,7 +544,7 @@ private void registerLimits( .asSubclass(Limits.class) .getConstructor() .newInstance(); - } else { + } else { LOG.info("Limits config not set, setting DefaultLimits"); limits = new DefaultLimits(); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java index 39a1265da778..4c71f6c36985 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java @@ -1,25 +1,27 @@ package org.openmetadata.service.limits; +import javax.ws.rs.core.SecurityContext; import org.jdbi.v3.core.Jdbi; import org.openmetadata.schema.configuration.LimitsConfiguration; import org.openmetadata.schema.system.LimitsResponse; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; -import javax.ws.rs.core.SecurityContext; - public class DefaultLimits implements Limits { - private LimitsConfiguration limitsConfiguration = null; - private Jdbi jdbi = null; + private LimitsConfiguration limitsConfiguration = null; + private Jdbi jdbi = null; @Override - public void init(LimitsConfiguration limitsConfiguration, Jdbi jdbi) { - this.limitsConfiguration = limitsConfiguration; - this.jdbi = jdbi; - } + public void init(LimitsConfiguration limitsConfiguration, Jdbi jdbi) { + this.limitsConfiguration = limitsConfiguration; + this.jdbi = jdbi; + } @Override - public void enforceLimits(SecurityContext securityContext, OperationContext operationContext, ResourceContextInterface resourceContext) { + public void enforceLimits( + SecurityContext securityContext, + OperationContext operationContext, + ResourceContextInterface resourceContext) { // do not enforce limits } @@ -29,5 +31,4 @@ public LimitsResponse getLimits() { limitsResponse.setEnable(limitsConfiguration.getEnable()); return limitsResponse; } - } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java index 6d0732207c78..995a11b00c6a 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java @@ -1,19 +1,19 @@ package org.openmetadata.service.limits; +import javax.ws.rs.core.SecurityContext; import org.jdbi.v3.core.Jdbi; import org.openmetadata.schema.configuration.LimitsConfiguration; import org.openmetadata.schema.system.LimitsResponse; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; - public interface Limits { void init(LimitsConfiguration limitsConfiguration, Jdbi jdbi); - void enforceLimits(SecurityContext securityContext, OperationContext operationContext, - ResourceContextInterface resourceContext); + void enforceLimits( + SecurityContext securityContext, + OperationContext operationContext, + ResourceContextInterface resourceContext); LimitsResponse getLimits(); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java index 916a1ab265b4..75b67fc3a9a6 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java @@ -251,7 +251,9 @@ private static Object createResource( // Create the resource identified by resourceClass try { - resource = clz.getDeclaredConstructor(Authorizer.class, Limits.class).newInstance(authorizer, limits); + resource = + clz.getDeclaredConstructor(Authorizer.class, Limits.class) + .newInstance(authorizer, limits); } catch (NoSuchMethodException e) { try { resource = clz.getDeclaredConstructor(Authorizer.class).newInstance(authorizer); @@ -267,8 +269,7 @@ private static Object createResource( .newInstance(jdbi, authorizer); } catch (NoSuchMethodException exec) { try { - resource = - clz.getDeclaredConstructor(Limits.class).newInstance(limits); + resource = clz.getDeclaredConstructor(Limits.class).newInstance(limits); } catch (NoSuchMethodException excep) { resource = Class.forName(resourceClass).getConstructor().newInstance(); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java index 8d82858c1c24..21d5fc5bcb4f 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java @@ -6,15 +6,14 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import org.openmetadata.schema.system.LimitsResponse; -import org.openmetadata.service.OpenMetadataApplicationConfig; -import org.openmetadata.service.limits.Limits; -import org.openmetadata.service.resources.Collection; - import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; +import org.openmetadata.schema.system.LimitsResponse; +import org.openmetadata.service.OpenMetadataApplicationConfig; +import org.openmetadata.service.limits.Limits; +import org.openmetadata.service.resources.Collection; @Path("/v1/limits") @Tag(name = "Limits", description = "APIs related to Limits configuration and settings.") @@ -37,13 +36,13 @@ public void initialize(OpenMetadataApplicationConfig config, Limits limits) { operationId = "getLimitsConfiguration", summary = "Get Limits configuration", responses = { - @ApiResponse( - responseCode = "200", - description = "Limits configuration", - content = - @Content( - mediaType = "application/json", - schema = @Schema(implementation = LimitsResponse.class))) + @ApiResponse( + responseCode = "200", + description = "Limits configuration", + content = + @Content( + mediaType = "application/json", + schema = @Schema(implementation = LimitsResponse.class))) }) public LimitsResponse getAuthConfig() { LimitsResponse limitsResponse = new LimitsResponse(); @@ -52,5 +51,4 @@ public LimitsResponse getAuthConfig() { } return limitsResponse; } - } From debf326a525f3b778363982c825180de5cbd73b3 Mon Sep 17 00:00:00 2001 From: mohitdeuex Date: Mon, 6 May 2024 17:19:13 +0530 Subject: [PATCH 008/117] - Mismatched Types --- .../openmetadata/service/resources/CollectionRegistry.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java index 75b67fc3a9a6..015584acf4a4 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java @@ -252,8 +252,8 @@ private static Object createResource( // Create the resource identified by resourceClass try { resource = - clz.getDeclaredConstructor(Authorizer.class, Limits.class) - .newInstance(authorizer, limits); + clz.getDeclaredConstructor(OpenMetadataApplicationConfig.class, Limits.class) + .newInstance(config, limits); } catch (NoSuchMethodException e) { try { resource = clz.getDeclaredConstructor(Authorizer.class).newInstance(authorizer); From a05ae303e66e6a7d4880a6d76b6977f7cc7ed3f9 Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Mon, 6 May 2024 20:58:00 -0700 Subject: [PATCH 009/117] Update Limits config response --- .../service/limits/DefaultLimits.java | 10 +++++----- .../openmetadata/service/limits/Limits.java | 4 ++-- .../resources/limits/LimitsResource.java | 20 ++++++++----------- .../json/schema/system/limitsResponse.json | 4 ++-- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java index 4c71f6c36985..d7c1a0a03e5f 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java @@ -3,7 +3,7 @@ import javax.ws.rs.core.SecurityContext; import org.jdbi.v3.core.Jdbi; import org.openmetadata.schema.configuration.LimitsConfiguration; -import org.openmetadata.schema.system.LimitsResponse; +import org.openmetadata.schema.system.LimitsConfig; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; @@ -26,9 +26,9 @@ public void enforceLimits( } @Override - public LimitsResponse getLimits() { - LimitsResponse limitsResponse = new LimitsResponse(); - limitsResponse.setEnable(limitsConfiguration.getEnable()); - return limitsResponse; + public LimitsConfig getLimitsConfig() { + LimitsConfig limitsConfig = new LimitsConfig(); + limitsConfig.setEnable(limitsConfiguration.getEnable()); + return limitsConfig; } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java index 995a11b00c6a..7e90ea1ff35f 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java @@ -3,7 +3,7 @@ import javax.ws.rs.core.SecurityContext; import org.jdbi.v3.core.Jdbi; import org.openmetadata.schema.configuration.LimitsConfiguration; -import org.openmetadata.schema.system.LimitsResponse; +import org.openmetadata.schema.system.LimitsConfig; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; @@ -15,5 +15,5 @@ void enforceLimits( OperationContext operationContext, ResourceContextInterface resourceContext); - LimitsResponse getLimits(); + LimitsConfig getLimitsConfig(); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java index 21d5fc5bcb4f..ea467a3f3a73 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java @@ -10,8 +10,7 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import org.openmetadata.schema.system.LimitsResponse; -import org.openmetadata.service.OpenMetadataApplicationConfig; +import org.openmetadata.schema.system.LimitsConfig; import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; @@ -21,12 +20,9 @@ @Produces(MediaType.APPLICATION_JSON) @Collection(name = "limits") public class LimitsResource { - private OpenMetadataApplicationConfig openMetadataApplicationConfig; + private final Limits limits; - private Limits limits; - - public void initialize(OpenMetadataApplicationConfig config, Limits limits) { - this.openMetadataApplicationConfig = config; + public LimitsResource(Limits limits) { this.limits = limits; } @@ -42,13 +38,13 @@ public void initialize(OpenMetadataApplicationConfig config, Limits limits) { content = @Content( mediaType = "application/json", - schema = @Schema(implementation = LimitsResponse.class))) + schema = @Schema(implementation = LimitsConfig.class))) }) - public LimitsResponse getAuthConfig() { - LimitsResponse limitsResponse = new LimitsResponse(); + public LimitsConfig getAuthConfig() { + LimitsConfig limitsConfig = new LimitsConfig(); if (limits != null) { - limitsResponse = limits.getLimits(); + limitsConfig = limits.getLimitsConfig(); } - return limitsResponse; + return limitsConfig; } } diff --git a/openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json b/openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json index f4d9ccb51771..60a907458be9 100644 --- a/openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json +++ b/openmetadata-spec/src/main/resources/json/schema/system/limitsResponse.json @@ -2,9 +2,9 @@ "$id": "https://open-metadata.org/schema/system/limitsResponse.json", "$schema": "http://json-schema.org/draft-07/schema#", "title": "LimitsResponse", - "description": "Limits response schema", + "description": "Limits Config schema", "type": "object", - "javaType": "org.openmetadata.schema.system.LimitsResponse", + "javaType": "org.openmetadata.schema.system.LimitsConfig", "properties": { "enable": { "description": "Limits Enabled", From 885114fdd3b014e8c034deaaa6400ee49dfed595 Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Tue, 7 May 2024 22:51:34 -0700 Subject: [PATCH 010/117] Update Limits feature response --- .../service/OpenMetadataApplication.java | 2 +- .../service/limits/DefaultLimits.java | 13 +++++++-- .../openmetadata/service/limits/Limits.java | 7 +++-- .../resources/limits/LimitsResource.java | 28 ++++++++++++++++++- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java index 65d762ab2500..b94d38ac67da 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java @@ -548,7 +548,7 @@ private void registerLimits(OpenMetadataApplicationConfig serverConfig) LOG.info("Limits config not set, setting DefaultLimits"); limits = new DefaultLimits(); } - limits.init(limitsConfiguration, jdbi); + limits.init(serverConfig, jdbi); } private void registerEventFilter( diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java index d7c1a0a03e5f..f9a406d505fe 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java @@ -1,19 +1,23 @@ package org.openmetadata.service.limits; +import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import org.jdbi.v3.core.Jdbi; import org.openmetadata.schema.configuration.LimitsConfiguration; import org.openmetadata.schema.system.LimitsConfig; +import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; public class DefaultLimits implements Limits { + private OpenMetadataApplicationConfig serverConfig = null; private LimitsConfiguration limitsConfiguration = null; private Jdbi jdbi = null; @Override - public void init(LimitsConfiguration limitsConfiguration, Jdbi jdbi) { - this.limitsConfiguration = limitsConfiguration; + public void init(OpenMetadataApplicationConfig serverConfig, Jdbi jdbi) { + this.serverConfig = serverConfig; + this.limitsConfiguration = serverConfig.getLimitsConfiguration(); this.jdbi = jdbi; } @@ -31,4 +35,9 @@ public LimitsConfig getLimitsConfig() { limitsConfig.setEnable(limitsConfiguration.getEnable()); return limitsConfig; } + + @Override + public Response getLimitsForaFeature(String name) { + return Response.ok().build(); + } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java index 7e90ea1ff35f..955873bd702d 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java @@ -1,14 +1,15 @@ package org.openmetadata.service.limits; +import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import org.jdbi.v3.core.Jdbi; -import org.openmetadata.schema.configuration.LimitsConfiguration; import org.openmetadata.schema.system.LimitsConfig; +import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; public interface Limits { - void init(LimitsConfiguration limitsConfiguration, Jdbi jdbi); + void init(OpenMetadataApplicationConfig serverConfig, Jdbi jdbi); void enforceLimits( SecurityContext securityContext, @@ -16,4 +17,6 @@ void enforceLimits( ResourceContextInterface resourceContext); LimitsConfig getLimitsConfig(); + + Response getLimitsForaFeature(String name); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java index ea467a3f3a73..e692697b21f4 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/limits/LimitsResource.java @@ -2,15 +2,22 @@ import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import javax.ws.rs.GET; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriInfo; import org.openmetadata.schema.system.LimitsConfig; +import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; @@ -21,9 +28,28 @@ @Collection(name = "limits") public class LimitsResource { private final Limits limits; + private final OpenMetadataApplicationConfig config; - public LimitsResource(Limits limits) { + public LimitsResource(OpenMetadataApplicationConfig config, Limits limits) { this.limits = limits; + this.config = config; + } + + @GET + @Path("/features/{name}") + @Operation( + operationId = "getLimitsForaFeature", + summary = "Get Limits configuration for a feature", + responses = { + @ApiResponse(responseCode = "200", description = "Limits configuration for a feature") + }) + public Response getLimitsForaFeature( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "Name of the Feature", schema = @Schema(type = "string")) + @PathParam("name") + String name) { + return limits.getLimitsForaFeature(name); } @GET From 0e69b91297911cbd622e1bb26af55bd0fde27abc Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Sat, 11 May 2024 12:15:27 -0700 Subject: [PATCH 011/117] Limits: add entity resource enforcer --- .../service/resources/CollectionRegistry.java | 25 ++++++++++++------- .../service/resources/EntityResource.java | 7 +++++- .../analytics/WebAnalyticEventResource.java | 5 ++-- .../apps/AppMarketPlaceResource.java | 5 ++-- .../service/resources/apps/AppResource.java | 5 ++-- .../automations/WorkflowResource.java | 5 ++-- .../service/resources/bots/BotResource.java | 5 ++-- .../resources/charts/ChartResource.java | 5 ++-- .../dashboards/DashboardResource.java | 5 ++-- .../resources/databases/DatabaseResource.java | 5 ++-- .../databases/DatabaseSchemaResource.java | 5 ++-- .../databases/StoredProcedureResource.java | 5 ++-- .../resources/databases/TableResource.java | 5 ++-- .../datainsight/DataInsightChartResource.java | 5 ++-- .../DashboardDataModelResource.java | 5 ++-- .../resources/docstore/DocStoreResource.java | 5 ++-- .../domains/DataProductResource.java | 5 ++-- .../resources/domains/DomainResource.java | 5 ++-- .../resources/dqtests/TestCaseResource.java | 5 ++-- .../dqtests/TestDefinitionResource.java | 5 ++-- .../resources/dqtests/TestSuiteResource.java | 5 ++-- .../EventSubscriptionResource.java | 5 ++-- .../resources/glossary/GlossaryResource.java | 5 ++-- .../glossary/GlossaryTermResource.java | 5 ++-- .../service/resources/kpi/KpiResource.java | 5 ++-- .../resources/metrics/MetricsResource.java | 5 ++-- .../resources/mlmodels/MlModelResource.java | 5 ++-- .../resources/pipelines/PipelineResource.java | 5 ++-- .../resources/policies/PolicyResource.java | 5 ++-- .../resources/query/QueryResource.java | 5 ++-- .../resources/reports/ReportResource.java | 5 ++-- .../searchindex/SearchIndexResource.java | 5 ++-- .../services/ServiceEntityResource.java | 5 ++-- .../TestConnectionDefinitionResource.java | 5 ++-- .../dashboard/DashboardServiceResource.java | 5 ++-- .../database/DatabaseServiceResource.java | 5 ++-- .../IngestionPipelineResource.java | 5 ++-- .../messaging/MessagingServiceResource.java | 5 ++-- .../metadata/MetadataServiceResource.java | 5 ++-- .../mlmodel/MlModelServiceResource.java | 5 ++-- .../pipeline/PipelineServiceResource.java | 5 ++-- .../searchIndexes/SearchServiceResource.java | 5 ++-- .../storage/StorageServiceResource.java | 5 ++-- .../resources/storages/ContainerResource.java | 5 ++-- .../tags/ClassificationResource.java | 5 ++-- .../service/resources/tags/TagResource.java | 5 ++-- .../resources/teams/PersonaResource.java | 5 ++-- .../service/resources/teams/RoleResource.java | 5 ++-- .../service/resources/teams/TeamResource.java | 5 ++-- .../service/resources/teams/UserResource.java | 6 +++-- .../resources/topics/TopicResource.java | 5 ++-- .../service/resources/types/TypeResource.java | 5 ++-- 52 files changed, 173 insertions(+), 110 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java index 015584acf4a4..c1c0b4455554 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java @@ -256,22 +256,29 @@ private static Object createResource( .newInstance(config, limits); } catch (NoSuchMethodException e) { try { - resource = clz.getDeclaredConstructor(Authorizer.class).newInstance(authorizer); + resource = + clz.getDeclaredConstructor(Authorizer.class, Limits.class) + .newInstance(authorizer, limits); } catch (NoSuchMethodException ex) { try { - resource = - clz.getDeclaredConstructor(Authorizer.class, AuthenticatorHandler.class) - .newInstance(authorizer, authHandler); + resource = clz.getDeclaredConstructor(Authorizer.class).newInstance(authorizer); } catch (NoSuchMethodException exe) { try { resource = - clz.getDeclaredConstructor(Jdbi.class, Authorizer.class) - .newInstance(jdbi, authorizer); + clz.getDeclaredConstructor( + Authorizer.class, Limits.class, AuthenticatorHandler.class) + .newInstance(authorizer, limits, authHandler); } catch (NoSuchMethodException exec) { try { - resource = clz.getDeclaredConstructor(Limits.class).newInstance(limits); - } catch (NoSuchMethodException excep) { - resource = Class.forName(resourceClass).getConstructor().newInstance(); + resource = + clz.getDeclaredConstructor(Jdbi.class, Authorizer.class) + .newInstance(jdbi, authorizer); + } catch (NoSuchMethodException execp) { + try { + resource = clz.getDeclaredConstructor(Limits.class).newInstance(limits); + } catch (NoSuchMethodException except) { + resource = Class.forName(resourceClass).getConstructor().newInstance(); + } } } } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java index c29caa58645f..0a93ac8e5b2f 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java @@ -31,6 +31,7 @@ import org.openmetadata.service.exception.CatalogExceptionMessage; import org.openmetadata.service.jdbi3.EntityRepository; import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.search.SearchListFilter; import org.openmetadata.service.search.SearchSortFilter; import org.openmetadata.service.security.Authorizer; @@ -53,14 +54,16 @@ public abstract class EntityResource allowedFields; @Getter protected final K repository; protected final Authorizer authorizer; + protected final Limits limits; protected final Map fieldsToViewOperations = new HashMap<>(); - protected EntityResource(String entityType, Authorizer authorizer) { + protected EntityResource(String entityType, Authorizer authorizer, Limits limits) { this.entityType = entityType; this.repository = (K) Entity.getEntityRepository(entityType); this.entityClass = (Class) Entity.getEntityClassFromType(entityType); allowedFields = repository.getAllowedFields(); this.authorizer = authorizer; + this.limits = limits; addViewOperation( "owner,followers,votes,tags,extension,domain,dataProducts,experts", VIEW_BASIC); Entity.registerResourcePermissions(entityType, getEntitySpecificOperations()); @@ -259,6 +262,7 @@ public Response create(UriInfo uriInfo, SecurityContext securityContext, T entit OperationContext operationContext = new OperationContext(entityType, CREATE); CreateResourceContext createResourceContext = new CreateResourceContext<>(entityType, entity); + limits.enforceLimits(securityContext, operationContext, createResourceContext); authorizer.authorize(securityContext, operationContext, createResourceContext); entity = addHref(uriInfo, repository.create(uriInfo, entity)); return Response.created(entity.getHref()).entity(entity).build(); @@ -274,6 +278,7 @@ public Response createOrUpdate(UriInfo uriInfo, SecurityContext securityContext, if (operation == CREATE) { CreateResourceContext createResourceContext = new CreateResourceContext<>(entityType, entity); + limits.enforceLimits(securityContext, operationContext, createResourceContext); authorizer.authorize(securityContext, operationContext, createResourceContext); entity = addHref(uriInfo, repository.create(uriInfo, entity)); return new PutResponse<>(Response.Status.CREATED, entity, ENTITY_CREATED).toResponse(); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/analytics/WebAnalyticEventResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/analytics/WebAnalyticEventResource.java index 474226cdcf98..88955594c867 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/analytics/WebAnalyticEventResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/analytics/WebAnalyticEventResource.java @@ -47,6 +47,7 @@ import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.WebAnalyticEventRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -65,8 +66,8 @@ public class WebAnalyticEventResource public static final String COLLECTION_PATH = WebAnalyticEventRepository.COLLECTION_PATH; static final String FIELDS = "owner"; - public WebAnalyticEventResource(Authorizer authorizer) { - super(Entity.WEB_ANALYTIC_EVENT, authorizer); + public WebAnalyticEventResource(Authorizer authorizer, Limits limits) { + super(Entity.WEB_ANALYTIC_EVENT, authorizer, limits); } public static class WebAnalyticEventList extends ResultList { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppMarketPlaceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppMarketPlaceResource.java index c38896febe3a..b6aab57c2330 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppMarketPlaceResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppMarketPlaceResource.java @@ -51,6 +51,7 @@ import org.openmetadata.service.clients.pipeline.PipelineServiceClientFactory; import org.openmetadata.service.jdbi3.AppMarketPlaceRepository; import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -96,8 +97,8 @@ public void initialize(OpenMetadataApplicationConfig config) { } } - public AppMarketPlaceResource(Authorizer authorizer) { - super(Entity.APP_MARKET_PLACE_DEF, authorizer); + public AppMarketPlaceResource(Authorizer authorizer, Limits limits) { + super(Entity.APP_MARKET_PLACE_DEF, authorizer, limits); } public static class AppMarketPlaceDefinitionList extends ResultList { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppResource.java index db8e526bebec..b430e3f2a131 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/apps/AppResource.java @@ -72,6 +72,7 @@ import org.openmetadata.service.jdbi3.CollectionDAO; import org.openmetadata.service.jdbi3.IngestionPipelineRepository; import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.search.SearchRepository; @@ -162,8 +163,8 @@ private App getAppForInit(String appName) { } } - public AppResource(Authorizer authorizer) { - super(Entity.APPLICATION, authorizer); + public AppResource(Authorizer authorizer, Limits limits) { + super(Entity.APPLICATION, authorizer, limits); } public static class AppList extends ResultList { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java index c0fe53447296..38afcd7da137 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/automations/WorkflowResource.java @@ -57,6 +57,7 @@ import org.openmetadata.service.jdbi3.EntityRepository; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.WorkflowRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.secrets.SecretsManager; @@ -86,8 +87,8 @@ public class WorkflowResource extends EntityResource { public static final String COLLECTION_PATH = "/v1/bots/"; - public BotResource(Authorizer authorizer) { - super(Entity.BOT, authorizer); + public BotResource(Authorizer authorizer, Limits limits) { + super(Entity.BOT, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/charts/ChartResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/charts/ChartResource.java index e8cd0e20727e..0fbb168c3e0e 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/charts/ChartResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/charts/ChartResource.java @@ -57,6 +57,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.ChartRepository; import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -82,8 +83,8 @@ public Chart addHref(UriInfo uriInfo, Chart chart) { return chart; } - public ChartResource(Authorizer authorizer) { - super(Entity.CHART, authorizer); + public ChartResource(Authorizer authorizer, Limits limits) { + super(Entity.CHART, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dashboards/DashboardResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dashboards/DashboardResource.java index fb1ab9669f82..1d7796af14ac 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dashboards/DashboardResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dashboards/DashboardResource.java @@ -57,6 +57,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.DashboardRepository; import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -85,8 +86,8 @@ public Dashboard addHref(UriInfo uriInfo, Dashboard dashboard) { return dashboard; } - public DashboardResource(Authorizer authorizer) { - super(Entity.DASHBOARD, authorizer); + public DashboardResource(Authorizer authorizer, Limits limits) { + super(Entity.DASHBOARD, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseResource.java index f8f4f48ade54..0147feceea99 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseResource.java @@ -61,6 +61,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.DatabaseRepository; import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -95,8 +96,8 @@ protected List getEntitySpecificOperations() { return listOf(MetadataOperation.VIEW_USAGE, MetadataOperation.EDIT_USAGE); } - public DatabaseResource(Authorizer authorizer) { - super(Entity.DATABASE, authorizer); + public DatabaseResource(Authorizer authorizer, Limits limits) { + super(Entity.DATABASE, authorizer, limits); } public static class DatabaseList extends ResultList { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseSchemaResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseSchemaResource.java index 94a305256d75..4df331474dab 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseSchemaResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/DatabaseSchemaResource.java @@ -56,6 +56,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.DatabaseSchemaRepository; import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -84,8 +85,8 @@ public DatabaseSchema addHref(UriInfo uriInfo, DatabaseSchema schema) { return schema; } - public DatabaseSchemaResource(Authorizer authorizer) { - super(Entity.DATABASE_SCHEMA, authorizer); + public DatabaseSchemaResource(Authorizer authorizer, Limits limits) { + super(Entity.DATABASE_SCHEMA, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/StoredProcedureResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/StoredProcedureResource.java index de48da0bc470..bfcb2c07a67f 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/StoredProcedureResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/StoredProcedureResource.java @@ -27,6 +27,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.StoredProcedureRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -54,8 +55,8 @@ public StoredProcedure addHref(UriInfo uriInfo, StoredProcedure storedProcedure) return storedProcedure; } - public StoredProcedureResource(Authorizer authorizer) { - super(Entity.STORED_PROCEDURE, authorizer); + public StoredProcedureResource(Authorizer authorizer, Limits limits) { + super(Entity.STORED_PROCEDURE, authorizer, limits); } public static class StoredProcedureList extends ResultList { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/TableResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/TableResource.java index f5a0efafa367..cad01d211f61 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/TableResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/TableResource.java @@ -70,6 +70,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.TableRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -101,8 +102,8 @@ public Table addHref(UriInfo uriInfo, Table table) { return table; } - public TableResource(Authorizer authorizer) { - super(Entity.TABLE, authorizer); + public TableResource(Authorizer authorizer, Limits limits) { + super(Entity.TABLE, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/datainsight/DataInsightChartResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/datainsight/DataInsightChartResource.java index f35cbc7fab44..693c79030ed3 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/datainsight/DataInsightChartResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/datainsight/DataInsightChartResource.java @@ -48,6 +48,7 @@ import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.jdbi3.DataInsightChartRepository; import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.search.SearchRepository; @@ -69,8 +70,8 @@ public class DataInsightChartResource public static final String FIELDS = "owner"; private final SearchRepository searchRepository; - public DataInsightChartResource(Authorizer authorizer) { - super(Entity.DATA_INSIGHT_CHART, authorizer); + public DataInsightChartResource(Authorizer authorizer, Limits limits) { + super(Entity.DATA_INSIGHT_CHART, authorizer, limits); searchRepository = Entity.getSearchRepository(); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/datamodels/DashboardDataModelResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/datamodels/DashboardDataModelResource.java index 2fdf435fe128..94506cd2caef 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/datamodels/DashboardDataModelResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/datamodels/DashboardDataModelResource.java @@ -53,6 +53,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.DashboardDataModelRepository; import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.resources.databases.DatabaseUtil; @@ -80,8 +81,8 @@ public DashboardDataModel addHref(UriInfo uriInfo, DashboardDataModel dashboardD return dashboardDataModel; } - public DashboardDataModelResource(Authorizer authorizer) { - super(Entity.DASHBOARD_DATA_MODEL, authorizer); + public DashboardDataModelResource(Authorizer authorizer, Limits limits) { + super(Entity.DASHBOARD_DATA_MODEL, authorizer, limits); } public static class DashboardDataModelList extends ResultList { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/docstore/DocStoreResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/docstore/DocStoreResource.java index 3675e8724310..1adf1baaad24 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/docstore/DocStoreResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/docstore/DocStoreResource.java @@ -57,6 +57,7 @@ import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.jdbi3.DocumentRepository; import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -83,8 +84,8 @@ protected List getEntitySpecificOperations() { return listOf(MetadataOperation.EDIT_ALL); } - public DocStoreResource(Authorizer authorizer) { - super(Entity.DOCUMENT, authorizer); + public DocStoreResource(Authorizer authorizer, Limits limits) { + super(Entity.DOCUMENT, authorizer, limits); } public static class DocumentList extends ResultList { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DataProductResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DataProductResource.java index 703d393c774c..715665712608 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DataProductResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/domains/DataProductResource.java @@ -60,6 +60,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.DataProductRepository; import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -80,8 +81,8 @@ public class DataProductResource extends EntityResource { public static final String COLLECTION_PATH = "/v1/domains/"; static final String FIELDS = "children,owner,experts"; - public DomainResource(Authorizer authorizer) { - super(Entity.DOMAIN, authorizer); + public DomainResource(Authorizer authorizer, Limits limits) { + super(Entity.DOMAIN, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java index d8756dc3d97c..ee386a051e50 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java @@ -54,6 +54,7 @@ import org.openmetadata.service.jdbi3.Filter; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.TestCaseRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.resources.feeds.MessageParser.EntityLink; @@ -97,8 +98,8 @@ public TestCase addHref(UriInfo uriInfo, TestCase test) { return test; } - public TestCaseResource(Authorizer authorizer) { - super(Entity.TEST_CASE, authorizer); + public TestCaseResource(Authorizer authorizer, Limits limits) { + super(Entity.TEST_CASE, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestDefinitionResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestDefinitionResource.java index ece4ab5c9fa5..801c4959c73c 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestDefinitionResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestDefinitionResource.java @@ -45,6 +45,7 @@ import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.TestDefinitionRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -65,8 +66,8 @@ public class TestDefinitionResource public static final String COLLECTION_PATH = "/v1/dataQuality/testDefinitions"; static final String FIELDS = "owner"; - public TestDefinitionResource(Authorizer authorizer) { - super(Entity.TEST_DEFINITION, authorizer); + public TestDefinitionResource(Authorizer authorizer, Limits limits) { + super(Entity.TEST_DEFINITION, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java index 55e7d8132399..ac855b1e2ad6 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestSuiteResource.java @@ -49,6 +49,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.TestSuiteRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.search.SearchListFilter; @@ -78,8 +79,8 @@ public class TestSuiteResource extends EntityResource public static final String COLLECTION_PATH = "/v1/metrics/"; static final String FIELDS = "owner,usageSummary,domain"; - public MetricsResource(Authorizer authorizer) { - super(Entity.METRICS, authorizer); + public MetricsResource(Authorizer authorizer, Limits limits) { + super(Entity.METRICS, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/mlmodels/MlModelResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/mlmodels/MlModelResource.java index 7b1a0cee34c2..9202cd44b43f 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/mlmodels/MlModelResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/mlmodels/MlModelResource.java @@ -57,6 +57,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.MlModelRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -83,8 +84,8 @@ public MlModel addHref(UriInfo uriInfo, MlModel mlmodel) { return mlmodel; } - public MlModelResource(Authorizer authorizer) { - super(Entity.MLMODEL, authorizer); + public MlModelResource(Authorizer authorizer, Limits limits) { + super(Entity.MLMODEL, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/pipelines/PipelineResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/pipelines/PipelineResource.java index 090163531a81..3c6f8d6d1fd9 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/pipelines/PipelineResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/pipelines/PipelineResource.java @@ -59,6 +59,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.PipelineRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.resources.dqtests.TestCaseResource; @@ -86,8 +87,8 @@ public Pipeline addHref(UriInfo uriInfo, Pipeline pipeline) { return pipeline; } - public PipelineResource(Authorizer authorizer) { - super(Entity.PIPELINE, authorizer); + public PipelineResource(Authorizer authorizer, Limits limits) { + super(Entity.PIPELINE, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/policies/PolicyResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/policies/PolicyResource.java index 4b7906d9f97e..ed0fea47c062 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/policies/PolicyResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/policies/PolicyResource.java @@ -62,6 +62,7 @@ import org.openmetadata.service.ResourceRegistry; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.PolicyRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.CollectionRegistry; import org.openmetadata.service.resources.EntityResource; @@ -92,8 +93,8 @@ public Policy addHref(UriInfo uriInfo, Policy policy) { return policy; } - public PolicyResource(Authorizer authorizer) { - super(Entity.POLICY, authorizer); + public PolicyResource(Authorizer authorizer, Limits limits) { + super(Entity.POLICY, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/query/QueryResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/query/QueryResource.java index 28b38d068fb4..9910d8010b15 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/query/QueryResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/query/QueryResource.java @@ -46,6 +46,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.QueryRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -66,8 +67,8 @@ public class QueryResource extends EntityResource { public static final String COLLECTION_PATH = "v1/queries/"; static final String FIELDS = "owner,followers,users,votes,tags,queryUsedIn"; - public QueryResource(Authorizer authorizer) { - super(Entity.QUERY, authorizer); + public QueryResource(Authorizer authorizer, Limits limits) { + super(Entity.QUERY, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/reports/ReportResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/reports/ReportResource.java index c05b131905c2..66a34fbb96bb 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/reports/ReportResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/reports/ReportResource.java @@ -46,6 +46,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.ReportRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -65,8 +66,8 @@ public class ReportResource extends EntityResource { public static final String COLLECTION_PATH = "/v1/reports/"; static final String FIELDS = "owner,usageSummary"; - public ReportResource(Authorizer authorizer) { - super(Entity.REPORT, authorizer); + public ReportResource(Authorizer authorizer, Limits limits) { + super(Entity.REPORT, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/searchindex/SearchIndexResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/searchindex/SearchIndexResource.java index 39a64cd0cc91..71aa489be684 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/searchindex/SearchIndexResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/searchindex/SearchIndexResource.java @@ -58,6 +58,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.SearchIndexRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -84,8 +85,8 @@ public SearchIndex addHref(UriInfo uriInfo, SearchIndex searchIndex) { return searchIndex; } - public SearchIndexResource(Authorizer authorizer) { - super(Entity.SEARCH_INDEX, authorizer); + public SearchIndexResource(Authorizer authorizer, Limits limits) { + super(Entity.SEARCH_INDEX, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ServiceEntityResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ServiceEntityResource.java index 8b8bc2859415..7b3af7900a06 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ServiceEntityResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ServiceEntityResource.java @@ -28,6 +28,7 @@ import org.openmetadata.service.exception.InvalidServiceConnectionException; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.ServiceEntityRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.secrets.SecretsManager; import org.openmetadata.service.secrets.SecretsManagerFactory; @@ -47,8 +48,8 @@ public abstract class ServiceEntityResource< private final ServiceType serviceType; protected ServiceEntityResource( - String entityType, Authorizer authorizer, ServiceType serviceType) { - super(entityType, authorizer); + String entityType, Authorizer authorizer, Limits limits, ServiceType serviceType) { + super(entityType, authorizer, limits); this.serviceType = serviceType; serviceEntityRepository = (ServiceEntityRepository) Entity.getServiceEntityRepository(serviceType); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/connections/TestConnectionDefinitionResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/connections/TestConnectionDefinitionResource.java index 6c2b186d6e4d..ffde23cdf670 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/connections/TestConnectionDefinitionResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/connections/TestConnectionDefinitionResource.java @@ -33,6 +33,7 @@ import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.TestConnectionDefinitionRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -53,8 +54,8 @@ public class TestConnectionDefinitionResource public static final String COLLECTION_PATH = "/v1/services/testConnectionDefinitions"; static final String FIELDS = "owner"; - public TestConnectionDefinitionResource(Authorizer authorizer) { - super(Entity.TEST_CONNECTION_DEFINITION, authorizer); + public TestConnectionDefinitionResource(Authorizer authorizer, Limits limits) { + super(Entity.TEST_CONNECTION_DEFINITION, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/dashboard/DashboardServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/dashboard/DashboardServiceResource.java index a7182ad233b9..b1fc9e4c5830 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/dashboard/DashboardServiceResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/dashboard/DashboardServiceResource.java @@ -57,6 +57,7 @@ import org.openmetadata.schema.type.MetadataOperation; import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.DashboardServiceRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.services.ServiceEntityResource; import org.openmetadata.service.security.Authorizer; @@ -75,8 +76,8 @@ public class DashboardServiceResource public static final String COLLECTION_PATH = "v1/services/dashboardServices"; static final String FIELDS = "owner,domain"; - public DashboardServiceResource(Authorizer authorizer) { - super(Entity.DASHBOARD_SERVICE, authorizer, ServiceType.DASHBOARD); + public DashboardServiceResource(Authorizer authorizer, Limits limits) { + super(Entity.DASHBOARD_SERVICE, authorizer, limits, ServiceType.DASHBOARD); } public static class DashboardServiceList extends ResultList { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/database/DatabaseServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/database/DatabaseServiceResource.java index 38d9d7e06c85..8458a0fd3801 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/database/DatabaseServiceResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/database/DatabaseServiceResource.java @@ -59,6 +59,7 @@ import org.openmetadata.schema.type.csv.CsvImportResult; import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.DatabaseServiceRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.services.ServiceEntityResource; import org.openmetadata.service.security.Authorizer; @@ -94,8 +95,8 @@ protected List getEntitySpecificOperations() { return null; } - public DatabaseServiceResource(Authorizer authorizer) { - super(Entity.DATABASE_SERVICE, authorizer, ServiceType.DATABASE); + public DatabaseServiceResource(Authorizer authorizer, Limits limits) { + super(Entity.DATABASE_SERVICE, authorizer, limits, ServiceType.DATABASE); } public static class DatabaseServiceList extends ResultList { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java index 5b8a6057c4b8..2e83dc875688 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java @@ -71,6 +71,7 @@ import org.openmetadata.service.clients.pipeline.PipelineServiceClientFactory; import org.openmetadata.service.jdbi3.IngestionPipelineRepository; import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.secrets.SecretsManager; @@ -107,8 +108,8 @@ public IngestionPipeline addHref(UriInfo uriInfo, IngestionPipeline ingestionPip return ingestionPipeline; } - public IngestionPipelineResource(Authorizer authorizer) { - super(Entity.INGESTION_PIPELINE, authorizer); + public IngestionPipelineResource(Authorizer authorizer, Limits limits) { + super(Entity.INGESTION_PIPELINE, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/messaging/MessagingServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/messaging/MessagingServiceResource.java index 98faacc45bcb..7c134cfbd6ac 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/messaging/MessagingServiceResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/messaging/MessagingServiceResource.java @@ -57,6 +57,7 @@ import org.openmetadata.schema.type.MetadataOperation; import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.MessagingServiceRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.services.ServiceEntityResource; import org.openmetadata.service.security.Authorizer; @@ -75,8 +76,8 @@ public class MessagingServiceResource public static final String COLLECTION_PATH = "v1/services/messagingServices/"; public static final String FIELDS = "owner,domain"; - public MessagingServiceResource(Authorizer authorizer) { - super(Entity.MESSAGING_SERVICE, authorizer, ServiceType.MESSAGING); + public MessagingServiceResource(Authorizer authorizer, Limits limits) { + super(Entity.MESSAGING_SERVICE, authorizer, limits, ServiceType.MESSAGING); } public static class MessagingServiceList extends ResultList { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/metadata/MetadataServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/metadata/MetadataServiceResource.java index d4862d58bb4e..cb1b01b0e26d 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/metadata/MetadataServiceResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/metadata/MetadataServiceResource.java @@ -56,6 +56,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.jdbi3.MetadataServiceRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.services.ServiceEntityResource; import org.openmetadata.service.security.Authorizer; @@ -119,8 +120,8 @@ public MetadataService addHref(UriInfo uriInfo, MetadataService service) { return service; } - public MetadataServiceResource(Authorizer authorizer) { - super(Entity.METADATA_SERVICE, authorizer, ServiceType.METADATA); + public MetadataServiceResource(Authorizer authorizer, Limits limits) { + super(Entity.METADATA_SERVICE, authorizer, limits, ServiceType.METADATA); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/mlmodel/MlModelServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/mlmodel/MlModelServiceResource.java index 561868842bc8..17efd15dd7ae 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/mlmodel/MlModelServiceResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/mlmodel/MlModelServiceResource.java @@ -59,6 +59,7 @@ import org.openmetadata.schema.type.MlModelConnection; import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.MlModelServiceRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.services.ServiceEntityResource; import org.openmetadata.service.security.Authorizer; @@ -83,8 +84,8 @@ public MlModelService addHref(UriInfo uriInfo, MlModelService service) { return service; } - public MlModelServiceResource(Authorizer authorizer) { - super(Entity.MLMODEL_SERVICE, authorizer, ServiceType.ML_MODEL); + public MlModelServiceResource(Authorizer authorizer, Limits limits) { + super(Entity.MLMODEL_SERVICE, authorizer, limits, ServiceType.ML_MODEL); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/pipeline/PipelineServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/pipeline/PipelineServiceResource.java index d61af347d4fc..cc347cff0dec 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/pipeline/PipelineServiceResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/pipeline/PipelineServiceResource.java @@ -57,6 +57,7 @@ import org.openmetadata.schema.type.PipelineConnection; import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.PipelineServiceRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.services.ServiceEntityResource; import org.openmetadata.service.security.Authorizer; @@ -81,8 +82,8 @@ public PipelineService addHref(UriInfo uriInfo, PipelineService service) { return service; } - public PipelineServiceResource(Authorizer authorizer) { - super(Entity.PIPELINE_SERVICE, authorizer, ServiceType.PIPELINE); + public PipelineServiceResource(Authorizer authorizer, Limits limits) { + super(Entity.PIPELINE_SERVICE, authorizer, limits, ServiceType.PIPELINE); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/searchIndexes/SearchServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/searchIndexes/SearchServiceResource.java index 020ccdb6eb51..85c8557e3841 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/searchIndexes/SearchServiceResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/searchIndexes/SearchServiceResource.java @@ -45,6 +45,7 @@ import org.openmetadata.schema.utils.EntityInterfaceUtil; import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.SearchServiceRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.services.ServiceEntityResource; import org.openmetadata.service.security.Authorizer; @@ -72,8 +73,8 @@ public SearchService addHref(UriInfo uriInfo, SearchService service) { return service; } - public SearchServiceResource(Authorizer authorizer) { - super(Entity.SEARCH_SERVICE, authorizer, ServiceType.SEARCH); + public SearchServiceResource(Authorizer authorizer, Limits limits) { + super(Entity.SEARCH_SERVICE, authorizer, limits, ServiceType.SEARCH); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/storage/StorageServiceResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/storage/StorageServiceResource.java index 59536e943b43..bb11cd83f103 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/storage/StorageServiceResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/storage/StorageServiceResource.java @@ -45,6 +45,7 @@ import org.openmetadata.schema.type.StorageConnection; import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.StorageServiceRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.services.ServiceEntityResource; import org.openmetadata.service.security.Authorizer; @@ -72,8 +73,8 @@ public StorageService addHref(UriInfo uriInfo, StorageService service) { return service; } - public StorageServiceResource(Authorizer authorizer) { - super(Entity.STORAGE_SERVICE, authorizer, ServiceType.STORAGE); + public StorageServiceResource(Authorizer authorizer, Limits limits) { + super(Entity.STORAGE_SERVICE, authorizer, limits, ServiceType.STORAGE); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/storages/ContainerResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/storages/ContainerResource.java index 636f037b6632..69a5f914fe20 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/storages/ContainerResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/storages/ContainerResource.java @@ -42,6 +42,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.ContainerRepository; import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -70,8 +71,8 @@ public Container addHref(UriInfo uriInfo, Container container) { return container; } - public ContainerResource(Authorizer authorizer) { - super(Entity.CONTAINER, authorizer); + public ContainerResource(Authorizer authorizer, Limits limits) { + super(Entity.CONTAINER, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/ClassificationResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/ClassificationResource.java index a84bee8a20a5..ccee98f4de26 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/ClassificationResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/ClassificationResource.java @@ -55,6 +55,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.ClassificationRepository; import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -84,8 +85,8 @@ static class ClassificationList extends ResultList { /* Required for serde */ } - public ClassificationResource(Authorizer authorizer) { - super(Entity.CLASSIFICATION, authorizer); + public ClassificationResource(Authorizer authorizer, Limits limits) { + super(Entity.CLASSIFICATION, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/TagResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/TagResource.java index b90515a708a9..208c158bea98 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/TagResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/tags/TagResource.java @@ -65,6 +65,7 @@ import org.openmetadata.service.jdbi3.EntityRepository; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.TagRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -96,8 +97,8 @@ static class TagList extends ResultList { /* Required for serde */ } - public TagResource(Authorizer authorizer) { - super(Entity.TAG, authorizer); + public TagResource(Authorizer authorizer, Limits limits) { + super(Entity.TAG, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/PersonaResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/PersonaResource.java index cdde012864a9..7f9db2ee7beb 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/PersonaResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/PersonaResource.java @@ -42,6 +42,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.PersonaRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -69,8 +70,8 @@ public Persona addHref(UriInfo uriInfo, Persona persona) { return persona; } - public PersonaResource(Authorizer authorizer) { - super(Entity.PERSONA, authorizer); + public PersonaResource(Authorizer authorizer, Limits limits) { + super(Entity.PERSONA, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/RoleResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/RoleResource.java index 120464499903..9b5631c01707 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/RoleResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/RoleResource.java @@ -59,6 +59,7 @@ import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.RoleRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -92,8 +93,8 @@ public Role addHref(UriInfo uriInfo, Role role) { return role; } - public RoleResource(Authorizer authorizer) { - super(Entity.ROLE, authorizer); + public RoleResource(Authorizer authorizer, Limits limits) { + super(Entity.ROLE, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/TeamResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/TeamResource.java index 9c9d3abfac59..07fdfdf4fdbf 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/TeamResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/TeamResource.java @@ -67,6 +67,7 @@ import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.TeamRepository; import org.openmetadata.service.jdbi3.TeamRepository.TeamCsv; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -103,8 +104,8 @@ public Team addHref(UriInfo uriInfo, Team team) { return team; } - public TeamResource(Authorizer authorizer) { - super(Entity.TEAM, authorizer); + public TeamResource(Authorizer authorizer, Limits limits) { + super(Entity.TEAM, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java index 6a3b305e5589..2e98bdf4a79e 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java @@ -123,6 +123,7 @@ import org.openmetadata.service.jdbi3.TokenRepository; import org.openmetadata.service.jdbi3.UserRepository; import org.openmetadata.service.jdbi3.UserRepository.UserCsv; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.secrets.SecretsManager; @@ -182,8 +183,9 @@ public User addHref(UriInfo uriInfo, User user) { return user; } - public UserResource(Authorizer authorizer, AuthenticatorHandler authenticatorHandler) { - super(Entity.USER, authorizer); + public UserResource( + Authorizer authorizer, Limits limits, AuthenticatorHandler authenticatorHandler) { + super(Entity.USER, authorizer, limits); jwtTokenGenerator = JWTTokenGenerator.getInstance(); allowedFields.remove(USER_PROTECTED_FIELDS); tokenRepository = Entity.getTokenRepository(); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/topics/TopicResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/topics/TopicResource.java index 6da016a85107..25c690445d36 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/topics/TopicResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/topics/TopicResource.java @@ -58,6 +58,7 @@ import org.openmetadata.service.Entity; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.TopicRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -85,8 +86,8 @@ public Topic addHref(UriInfo uriInfo, Topic topic) { return topic; } - public TopicResource(Authorizer authorizer) { - super(Entity.TOPIC, authorizer); + public TopicResource(Authorizer authorizer, Limits limits) { + super(Entity.TOPIC, authorizer, limits); } @Override diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java index d9fd31a42da5..becbd478d591 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java @@ -59,6 +59,7 @@ import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.TypeRepository; +import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; @@ -88,8 +89,8 @@ public Type addHref(UriInfo uriInfo, Type type) { return type; } - public TypeResource(Authorizer authorizer) { - super(Entity.TYPE, authorizer); + public TypeResource(Authorizer authorizer, Limits limits) { + super(Entity.TYPE, authorizer, limits); } @Override From 2dbc0ec11640f3bce100bba6074f9733e0e9c07e Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Sat, 11 May 2024 13:08:46 -0700 Subject: [PATCH 012/117] Limits: fix rebase --- .../openmetadata/service/resources/CollectionRegistry.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java index 262da6592fd7..8b5ff387d665 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/CollectionRegistry.java @@ -179,14 +179,14 @@ public void loadSeedData( Authorizer authorizer, AuthenticatorHandler authenticatorHandler, Limits limits, - boolean isOperations) { + boolean isOperations) { // Build list of ResourceDescriptors for (Map.Entry e : collectionMap.entrySet()) { CollectionDetails details = e.getValue(); if (!isOperations || (isOperations && details.requiredForOps)) { String resourceClass = details.resourceClass; try { - createResource(jdbi, resourceClass, config, authorizer, authenticatorHandler, limits); + createResource(jdbi, resourceClass, config, authorizer, authenticatorHandler, limits); } catch (Exception ex) { LOG.warn("Failed to create resource for class {} {}", resourceClass, ex); } @@ -256,6 +256,7 @@ private static Object createResource( .newInstance(config, limits); } catch (NoSuchMethodException e) { try { + resource = clz.getDeclaredConstructor(Authorizer.class, Limits.class) .newInstance(authorizer, limits); } catch (NoSuchMethodException ex) { From 546b510e4cd7c7057cc3551c81ecd6512d54612e Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Sun, 12 May 2024 11:56:25 -0700 Subject: [PATCH 013/117] update limits enforcement --- .../service/exception/LimitsException.java | 12 ++++++++++++ .../openmetadata/service/limits/DefaultLimits.java | 5 +---- .../java/org/openmetadata/service/limits/Limits.java | 6 +----- .../service/resources/EntityResource.java | 4 ++-- .../service/resources/teams/UserResource.java | 2 ++ 5 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 openmetadata-service/src/main/java/org/openmetadata/service/exception/LimitsException.java diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/exception/LimitsException.java b/openmetadata-service/src/main/java/org/openmetadata/service/exception/LimitsException.java new file mode 100644 index 000000000000..17a757a556ea --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/exception/LimitsException.java @@ -0,0 +1,12 @@ +package org.openmetadata.service.exception; + +import javax.ws.rs.core.Response; +import org.openmetadata.sdk.exception.WebServiceException; + +public class LimitsException extends WebServiceException { + private static final String ERROR_TYPE = "LIMITS_EXCEPTION"; + + public LimitsException(String message) { + super(Response.Status.TOO_MANY_REQUESTS, ERROR_TYPE, message); + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java index f9a406d505fe..fe77a9f58c00 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java @@ -6,7 +6,6 @@ import org.openmetadata.schema.configuration.LimitsConfiguration; import org.openmetadata.schema.system.LimitsConfig; import org.openmetadata.service.OpenMetadataApplicationConfig; -import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; public class DefaultLimits implements Limits { @@ -23,9 +22,7 @@ public void init(OpenMetadataApplicationConfig serverConfig, Jdbi jdbi) { @Override public void enforceLimits( - SecurityContext securityContext, - OperationContext operationContext, - ResourceContextInterface resourceContext) { + SecurityContext securityContext, ResourceContextInterface resourceContext) { // do not enforce limits } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java index 955873bd702d..6d522d7f3ef7 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java @@ -5,16 +5,12 @@ import org.jdbi.v3.core.Jdbi; import org.openmetadata.schema.system.LimitsConfig; import org.openmetadata.service.OpenMetadataApplicationConfig; -import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; public interface Limits { void init(OpenMetadataApplicationConfig serverConfig, Jdbi jdbi); - void enforceLimits( - SecurityContext securityContext, - OperationContext operationContext, - ResourceContextInterface resourceContext); + void enforceLimits(SecurityContext securityContext, ResourceContextInterface resourceContext); LimitsConfig getLimitsConfig(); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java index 0a93ac8e5b2f..322d07e6f399 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java @@ -262,7 +262,7 @@ public Response create(UriInfo uriInfo, SecurityContext securityContext, T entit OperationContext operationContext = new OperationContext(entityType, CREATE); CreateResourceContext createResourceContext = new CreateResourceContext<>(entityType, entity); - limits.enforceLimits(securityContext, operationContext, createResourceContext); + limits.enforceLimits(securityContext, createResourceContext); authorizer.authorize(securityContext, operationContext, createResourceContext); entity = addHref(uriInfo, repository.create(uriInfo, entity)); return Response.created(entity.getHref()).entity(entity).build(); @@ -278,7 +278,7 @@ public Response createOrUpdate(UriInfo uriInfo, SecurityContext securityContext, if (operation == CREATE) { CreateResourceContext createResourceContext = new CreateResourceContext<>(entityType, entity); - limits.enforceLimits(securityContext, operationContext, createResourceContext); + limits.enforceLimits(securityContext, createResourceContext); authorizer.authorize(securityContext, operationContext, createResourceContext); entity = addHref(uriInfo, repository.create(uriInfo, entity)); return new PutResponse<>(Response.Status.CREATED, entity, ENTITY_CREATED).toResponse(); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java index 2e98bdf4a79e..7e21c90b38b1 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java @@ -550,6 +550,8 @@ public Response createUser( @Context ContainerRequestContext containerRequestContext, @Valid CreateUser create) { User user = getUser(securityContext.getUserPrincipal().getName(), create); + ResourceContext resourceContext = getResourceContextByName(user.getFullyQualifiedName()); + limits.enforceLimits(securityContext, resourceContext); if (Boolean.TRUE.equals(create.getIsAdmin())) { authorizer.authorizeAdmin(securityContext); } From 7ff5ab3cd0c9a93f18cc77a8c9141b4afee176fa Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Mon, 20 May 2024 14:55:10 -0700 Subject: [PATCH 014/117] Add OperationContext to limits --- .../openmetadata/service/limits/DefaultLimits.java | 3 ++- .../java/org/openmetadata/service/limits/Limits.java | 3 ++- .../service/resources/EntityResource.java | 4 ++-- .../ingestionpipelines/IngestionPipelineResource.java | 11 ++++++++++- .../service/resources/teams/UserResource.java | 3 ++- .../policies/accessControl/resourceDescriptor.json | 5 ++++- 6 files changed, 22 insertions(+), 7 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java index fe77a9f58c00..2d63c75c089f 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/DefaultLimits.java @@ -6,6 +6,7 @@ import org.openmetadata.schema.configuration.LimitsConfiguration; import org.openmetadata.schema.system.LimitsConfig; import org.openmetadata.service.OpenMetadataApplicationConfig; +import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; public class DefaultLimits implements Limits { @@ -22,7 +23,7 @@ public void init(OpenMetadataApplicationConfig serverConfig, Jdbi jdbi) { @Override public void enforceLimits( - SecurityContext securityContext, ResourceContextInterface resourceContext) { + SecurityContext securityContext, ResourceContextInterface resourceContext, OperationContext operationContext) { // do not enforce limits } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java index 6d522d7f3ef7..9aca33f23add 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/limits/Limits.java @@ -5,12 +5,13 @@ import org.jdbi.v3.core.Jdbi; import org.openmetadata.schema.system.LimitsConfig; import org.openmetadata.service.OpenMetadataApplicationConfig; +import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.security.policyevaluator.ResourceContextInterface; public interface Limits { void init(OpenMetadataApplicationConfig serverConfig, Jdbi jdbi); - void enforceLimits(SecurityContext securityContext, ResourceContextInterface resourceContext); + void enforceLimits(SecurityContext securityContext, ResourceContextInterface resourceContext, OperationContext operationContext); LimitsConfig getLimitsConfig(); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java index 322d07e6f399..a944ab365106 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/EntityResource.java @@ -262,7 +262,7 @@ public Response create(UriInfo uriInfo, SecurityContext securityContext, T entit OperationContext operationContext = new OperationContext(entityType, CREATE); CreateResourceContext createResourceContext = new CreateResourceContext<>(entityType, entity); - limits.enforceLimits(securityContext, createResourceContext); + limits.enforceLimits(securityContext, createResourceContext, operationContext); authorizer.authorize(securityContext, operationContext, createResourceContext); entity = addHref(uriInfo, repository.create(uriInfo, entity)); return Response.created(entity.getHref()).entity(entity).build(); @@ -278,7 +278,7 @@ public Response createOrUpdate(UriInfo uriInfo, SecurityContext securityContext, if (operation == CREATE) { CreateResourceContext createResourceContext = new CreateResourceContext<>(entityType, entity); - limits.enforceLimits(securityContext, createResourceContext); + limits.enforceLimits(securityContext, createResourceContext, operationContext); authorizer.authorize(securityContext, operationContext, createResourceContext); entity = addHref(uriInfo, repository.create(uriInfo, entity)); return new PutResponse<>(Response.Status.CREATED, entity, ENTITY_CREATED).toResponse(); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java index 2e83dc875688..c2f83f97139b 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/services/ingestionpipelines/IngestionPipelineResource.java @@ -79,6 +79,7 @@ import org.openmetadata.service.secrets.masker.EntityMaskerFactory; import org.openmetadata.service.security.AuthorizationException; import org.openmetadata.service.security.Authorizer; +import org.openmetadata.service.security.policyevaluator.CreateResourceContext; import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.util.EntityUtil.Fields; import org.openmetadata.service.util.OpenMetadataConnectionBuilder; @@ -490,7 +491,7 @@ public Response createOrUpdate( @Path("/deploy/{id}") @Operation( summary = "Deploy an ingestion pipeline run", - description = "Trigger a ingestion pipeline run by Id.", + description = "Deploy a ingestion pipeline run by Id.", responses = { @ApiResponse( responseCode = "200", @@ -923,6 +924,10 @@ private PipelineServiceClientResponse deployPipelineInternal( UUID id, UriInfo uriInfo, SecurityContext securityContext) { Fields fields = getFields(FIELD_OWNER); IngestionPipeline ingestionPipeline = repository.get(uriInfo, id, fields); + CreateResourceContext createResourceContext = + new CreateResourceContext<>(entityType, ingestionPipeline); + OperationContext operationContext = new OperationContext(entityType, MetadataOperation.DEPLOY); + limits.enforceLimits(securityContext, createResourceContext, operationContext); decryptOrNullify(securityContext, ingestionPipeline, true); ServiceEntityInterface service = Entity.getEntity(ingestionPipeline.getService(), "", Include.NON_DELETED); @@ -938,6 +943,10 @@ public PipelineServiceClientResponse triggerPipelineInternal( UUID id, UriInfo uriInfo, SecurityContext securityContext, String botName) { Fields fields = getFields(FIELD_OWNER); IngestionPipeline ingestionPipeline = repository.get(uriInfo, id, fields); + CreateResourceContext createResourceContext = + new CreateResourceContext<>(entityType, ingestionPipeline); + OperationContext operationContext = new OperationContext(entityType, MetadataOperation.TRIGGER); + limits.enforceLimits(securityContext, createResourceContext, operationContext); if (CommonUtil.nullOrEmpty(botName)) { // Use Default Ingestion Bot ingestionPipeline.setOpenMetadataServerConnection( diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java index 0e4f31d42163..de28a58cafa8 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java @@ -551,7 +551,8 @@ public Response createUser( @Valid CreateUser create) { User user = getUser(securityContext.getUserPrincipal().getName(), create); ResourceContext resourceContext = getResourceContextByName(user.getFullyQualifiedName()); - limits.enforceLimits(securityContext, resourceContext); + OperationContext operationContext = new OperationContext(entityType, MetadataOperation.CREATE); + limits.enforceLimits(securityContext, resourceContext, operationContext); if (Boolean.TRUE.equals(create.getIsAdmin())) { authorizer.authorizeAdmin(securityContext); } diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/policies/accessControl/resourceDescriptor.json b/openmetadata-spec/src/main/resources/json/schema/entity/policies/accessControl/resourceDescriptor.json index a5fc6160a4c2..48a6daf92d91 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/policies/accessControl/resourceDescriptor.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/policies/accessControl/resourceDescriptor.json @@ -44,7 +44,10 @@ "EditLifeCycle", "EditKnowledgePanel", "EditPage", - "DeleteTestCaseFailedRowsSample" + "DeleteTestCaseFailedRowsSample", + "Deploy", + "Trigger", + "Kill" ] } }, From 3a378b4d29ca69443e03629473b5f28f0dc98c3f Mon Sep 17 00:00:00 2001 From: Pablo Takara Date: Fri, 10 May 2024 11:46:26 +0200 Subject: [PATCH 015/117] 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 016/117] 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 017/117] 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 018/117] 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 019/117] 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 020/117] 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 021/117] 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 022/117] 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, - })}> +
- + + + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/QualityTab/QualityTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/QualityTab/QualityTab.test.tsx index 1e81b386f81e..4d2a220fea34 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/QualityTab/QualityTab.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/QualityTab/QualityTab.test.tsx @@ -12,6 +12,7 @@ */ import { act, fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; +import LimitWrapper from '../../../../../hoc/LimitWrapper'; import { MOCK_TABLE } from '../../../../../mocks/TableData.mock'; import { useTableProfiler } from '../TableProfilerProvider'; import { QualityTab } from './QualityTab.component'; @@ -103,6 +104,15 @@ jest.mock('../../DataQualityTab/DataQualityTab', () => { .mockImplementation(() =>
DataQualityTab.component
); }); +jest.mock('../../../../../hoc/LimitWrapper', () => { + return jest.fn().mockImplementation(({ children }) => ( +
+

LimitWrapper

+ {children} +
+ )); +}); + describe('QualityTab', () => { it('should render QualityTab', async () => { await act(async () => { @@ -160,6 +170,19 @@ describe('QualityTab', () => { ).toBeInTheDocument(); }); + it('should call limitWrapper', async () => { + await act(async () => { + render(); + fireEvent.click(await screen.findByTestId('profiler-add-table-test-btn')); + }); + + expect(LimitWrapper).toHaveBeenCalledWith( + expect.objectContaining({ resource: 'dataQuality' }), + {} + ); + expect(await screen.findByText('LimitWrapper')).toBeInTheDocument(); + }); + it('should not render the Add button if editTest is false', async () => { (useTableProfiler as jest.Mock).mockReturnValue({ ...mockUseTableProfiler, @@ -204,10 +227,10 @@ describe('QualityTab', () => { }); expect( - await screen.getByRole('tab', { name: 'label.test-case-plural' }) + await screen.findByRole('tab', { name: 'label.test-case-plural' }) ).toHaveAttribute('aria-selected', 'true'); expect( - await screen.getByRole('tab', { name: 'label.pipeline' }) + await screen.findByRole('tab', { name: 'label.pipeline' }) ).toHaveAttribute('aria-selected', 'false'); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/table-profiler.less b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/table-profiler.less index 9f56af2cea20..4ef2c13cfd2e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/table-profiler.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/Profiler/TableProfiler/table-profiler.less @@ -11,7 +11,7 @@ * limitations under the License. */ -@import url('../../../../styles/variables.less'); +@import (reference) url('../../../../styles/variables.less'); @succesColor: #28a745; @failedColor: #cb2431; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureVersion/StoredProcedureVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureVersion/StoredProcedureVersion.component.tsx index 43a7e414dc84..67dbcd1f5947 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureVersion/StoredProcedureVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/StoredProcedureVersion/StoredProcedureVersion.component.tsx @@ -205,6 +205,7 @@ const StoredProcedureVersion = ({ = ({ {
{domainPageRender}
{ +export const VersionButton = forwardRef< + HTMLDivElement, + EntityVersionButtonProps +>(({ version, onVersionSelect, selected, isMajorVersion, className }, ref) => { const { t } = useTranslation(); const { @@ -55,7 +58,11 @@ export const VersionButton = ({ return (
onVersionSelect(toString(versionNumber))}>
); -}; +}); const EntityVersionTimeLine: React.FC = ({ versionList = {} as EntityHistory, currentVersion, versionHandler, onBack, + entityType, }) => { const { t } = useTranslation(); - const versions = useMemo( - () => - versionList.versions?.map((v, i) => { - const currV = JSON.parse(v); - - const majorVersionChecks = () => { - return isMajorVersion( - parseFloat(currV?.changeDescription?.previousVersion) - .toFixed(1) - .toString(), - parseFloat(currV?.version).toFixed(1).toString() - ); - }; - - return ( - - {i === 0 ? ( -
-
- -
+ const { resourceLimit, getResourceLimit } = useLimitStore(); + + useEffect(() => { + entityType && getResourceLimit(entityType); + }, [entityType]); + + const { configuredLimit: { maxVersions } = { maxVersions: -1 } } = + resourceLimit[entityType ?? ''] ?? {}; + + const versions = useMemo(() => { + const maxAllowed = maxVersions ?? -1; + let versions = versionList.versions ?? []; + + let hiddenVersions = []; + + if (maxAllowed > 0) { + versions = versionList.versions?.slice(0, maxAllowed) ?? []; + hiddenVersions = versionList.versions?.slice(maxAllowed) ?? []; + } + + return ( +
+ {versions.length ? ( +
+
+ +
+
+ ) : null} + + {versions?.map((v) => { + return renderVersionButton(v, currentVersion, versionHandler); + })} + {hiddenVersions?.length > 0 ? ( + <> + +
+ {hiddenVersions.map((v) => + renderVersionButton(v, currentVersion, versionHandler) + )}
- ) : null} - - - ); - }), - [versionList, currentVersion, versionHandler] - ); +
+
+ + Unlock all of your version history + + + Upgrade to paid plan for access to all of your version history. + + + +
+ + ) : null} +
+ ); + }, [versionList, currentVersion, versionHandler]); return ( void; onBack: () => void; + entityType?: EntityType; }; export type EntityVersionButtonProps = { @@ -31,4 +33,5 @@ export type EntityVersionButtonProps = { onVersionSelect: (v: string) => void; selected: boolean; isMajorVersion: boolean; + className?: string; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityVersionTimeLine/entity-version-timeline.less b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityVersionTimeLine/entity-version-timeline.less index cf9614ce5a77..3718ad6181fa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityVersionTimeLine/entity-version-timeline.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/EntityVersionTimeLine/entity-version-timeline.less @@ -10,8 +10,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -.version-timestamp::before { - content: '\2022'; - margin-right: 6px; - margin-left: 4px; + +@import (reference) url('./../../../styles/variables.less'); + +.versions-list-container { + .version-pricing-reached { + background: #f3f7fd; + border: 1px solid #349dea; + border-radius: 10px; + text-align: center; + padding: @padding-md; + position: absolute; + bottom: 0; + box-sizing: border-box; + + button { + margin-top: 30px; + } + } + + .version-hidden { + mask-image: linear-gradient( + to bottom, + rgba(0, 0, 0, 0.9), + rgba(0, 0, 0, 0) + ); + filter: blur(2px); + + .timeline-content { + pointer-events: none; + cursor: not-allowed; + user-select: none; + } + } } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.component.tsx index 9f0b5a005fb0..b600741ae556 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.component.tsx @@ -12,12 +12,10 @@ */ import { - ExclamationCircleOutlined, SortAscendingOutlined, SortDescendingOutlined, } from '@ant-design/icons'; import { - Alert, Button, Col, Layout, @@ -47,7 +45,6 @@ import { TAG_FQN_KEY, } from '../../constants/explore.constants'; import { ERROR_PLACEHOLDER_TYPE, SORT_ORDER } from '../../enums/common.enum'; -import { useApplicationStore } from '../../hooks/useApplicationStore'; import { QueryFieldInterface } from '../../pages/ExplorePage/ExplorePage.interface'; import { getDropDownItems } from '../../utils/AdvancedSearchUtils'; import { Transi18next } from '../../utils/CommonUtils'; @@ -55,6 +52,7 @@ import { highlightEntityNameAndDescription } from '../../utils/EntityUtils'; import { getSelectedValuesFromQuickFilter } from '../../utils/Explore.utils'; import { getApplicationDetailsPath } from '../../utils/RouterUtils'; import searchClassBase from '../../utils/SearchClassBase'; +import InlineAlert from '../common/InlineAlert/InlineAlert'; import Loader from '../common/Loader/Loader'; import { ExploreProps, @@ -66,47 +64,6 @@ import SearchedData from '../SearchedData/SearchedData'; import { SearchedDataProps } from '../SearchedData/SearchedData.interface'; import './exploreV1.less'; -const IndexNotFoundBanner = () => { - const { theme } = useApplicationStore(); - const { t } = useTranslation(); - - return ( - - -
- - {t('server.indexing-error')} - - - - } - values={{ - settings: t('label.search-index-setting-plural'), - }} - /> - -
-
- } - type="error" - /> - ); -}; - const ExploreV1: React.FC = ({ aggregations, activeTabKey, @@ -299,175 +256,190 @@ const ExploreV1: React.FC = ({ return (
-
- {tabItems.length > 0 && ( - - - - {t('label.data-asset-plural')} - - { - if (info && info.key !== activeTabKey) { - onChangeSearchIndex(info.key as ExploreSearchIndex); - setShowSummaryPanel(false); - } - }} - /> - - - - - - - toggleModal(true)} - onChangeShowDeleted={onChangeShowDeleted} - onFieldValueSelect={handleQuickFiltersValueSelect} + {tabItems.length > 0 && ( + + + + {t('label.data-asset-plural')} + + { + if (info && info.key !== activeTabKey) { + onChangeSearchIndex(info.key as ExploreSearchIndex); + setShowSummaryPanel(false); + } + }} + /> + + + + + + + toggleModal(true)} + onChangeShowDeleted={onChangeShowDeleted} + onFieldValueSelect={handleQuickFiltersValueSelect} + /> + + + + - - - - - - {t('label.deleted')} - - - {(quickFilters || sqlQuery) && ( - clearFilters()}> - {t('label.clear-entity', { - entity: '', - })} - - )} - + + {t('label.deleted')} + + + {(quickFilters || sqlQuery) && ( toggleModal(true)}> - {t('label.advanced-entity', { + data-testid="clear-filters" + onClick={() => clearFilters()}> + {t('label.clear-entity', { entity: '', })} - - - - - - {isElasticSearchIssue ? ( - - - - ) : ( - <> - )} - {sqlQuery && ( - - toggleModal(true)} - /> - )} - - - - filterOption.key === TAG_FQN_KEY - )?.value ?? [] - ).map((tagFQN) => tagFQN.key), // finding the tags filter from SelectedQuickFilters and creating the array of selected Tags FQN - }, - ['description', 'displayName'] - )} - /> - ) - } - rightPanelWidth={400}> - - - {!loading && !isElasticSearchIssue ? ( - toggleModal(true)}> + {t('label.advanced-entity', { + entity: '', + })} + + + - ) : ( - <> - )} - {loading ? : <>} + + + {isElasticSearchIssue ? ( + + + } + values={{ + settings: t('label.search-index-setting-plural'), + }} + /> + } + heading={t('server.indexing-error')} + type="error" + /> + + ) : ( + <> + )} + {sqlQuery && ( + + toggleModal(true)} + /> + + )} - - - - )} -
+ + + filterOption.key === TAG_FQN_KEY + )?.value ?? [] + ).map((tagFQN) => tagFQN.key), // finding the tags filter from SelectedQuickFilters and creating the array of selected Tags FQN + }, + ['description', 'displayName'] + )} + /> + ) + } + rightPanelWidth={400}> + + + {!loading && !isElasticSearchIssue ? ( + + ) : ( + <> + )} + {loading ? : <>} + + + + + + )} {searchQueryParam && tabItems.length === 0 && !loading && ( { return ( - + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryVersion/GlossaryVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryVersion/GlossaryVersion.component.tsx index cbdafdb4d9a4..31bc8575ddec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryVersion/GlossaryVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryVersion/GlossaryVersion.component.tsx @@ -14,6 +14,7 @@ import { AxiosError } from 'axios'; import { toString } from 'lodash'; import React, { useEffect, useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; +import { EntityType } from '../../../enums/entity.enum'; import { Glossary } from '../../../generated/entity/data/glossary'; import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm'; import { EntityHistory } from '../../../generated/type/entityHistory'; @@ -119,6 +120,7 @@ const GlossaryVersion = ({ isGlossary = false }: GlossaryVersionProps) => {
= ({
= ({ = ({ feature.name === 'application' + ) ?? {}; const fetchPipelineDetails = useCallback(async () => { setIsLoading(true); @@ -134,7 +142,7 @@ const AppSchedule = ({ return ['Day']; } - return undefined; + return getScheduleOptionsFromSchedules(pipelineSchedules ?? []); }, [appData.name, appData.appType]); useEffect(() => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.test.tsx index c8ae8a89523f..109e8a1428fa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.test.tsx @@ -20,6 +20,7 @@ import React from 'react'; import { AppType } from '../../../../generated/entity/applications/app'; import { EntityReference } from '../../../../generated/tests/testSuite'; import { mockApplicationData } from '../../../../mocks/rests/applicationAPI.mock'; +import { getScheduleOptionsFromSchedules } from '../../../../utils/ScheduleUtils'; import AppSchedule from './AppSchedule.component'; const mockGetIngestionPipelineByFqn = jest.fn().mockResolvedValue({ @@ -98,6 +99,24 @@ const mockProps3 = { }, }; +jest.mock('../../../../context/LimitsProvider/useLimitsStore', () => ({ + useLimitStore: jest.fn().mockReturnValue({ + config: { + limits: { + config: { + featureLimits: [ + { name: 'application', pipelineSchedules: ['daily', 'weekly'] }, + ], + }, + }, + }, + }), +})); + +jest.mock('../../../../utils/ScheduleUtils', () => ({ + getScheduleOptionsFromSchedules: jest.fn().mockReturnValue([]), +})); + describe('AppSchedule component', () => { it('should render necessary elements for mockProps1', () => { render(); @@ -172,4 +191,23 @@ describe('AppSchedule component', () => { expect(screen.queryByText('AppRunsHistory')).not.toBeInTheDocument(); }); + + it('should call getScheduleOptionsFromSchedules with application pipelineStatus values', () => { + mockGetIngestionPipelineByFqn.mockRejectedValueOnce({}); + render( + + ); + + expect(getScheduleOptionsFromSchedules).toHaveBeenCalledWith([ + 'daily', + 'weekly', + ]); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.component.tsx index 2f0a9d1e69e0..8c25ce367b4e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.component.tsx @@ -42,6 +42,7 @@ import './bot-details.less'; import { BotsDetailProps } from './BotDetails.interfaces'; import { ReactComponent as IconBotProfile } from '../../../../assets/svg/bot-profile.svg'; +import { useLimitStore } from '../../../../context/LimitsProvider/useLimitsStore'; import { EntityType } from '../../../../enums/entity.enum'; import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1'; import AccessTokenCard from '../../Users/AccessTokenCard/AccessTokenCard.component'; @@ -59,22 +60,27 @@ const BotDetails: FC = ({ const [isDescriptionEdit, setIsDescriptionEdit] = useState(false); const [selectedRoles, setSelectedRoles] = useState>([]); const [roles, setRoles] = useState>([]); + const { getResourceLimit } = useLimitStore(); + + const [disableFields, setDisableFields] = useState([]); const { t } = useTranslation(); - const editAllPermission = useMemo( - () => botPermission.EditAll, - [botPermission] - ); - const displayNamePermission = useMemo( - () => botPermission.EditDisplayName, - [botPermission] - ); + const { editAllPermission, displayNamePermission, descriptionPermission } = + useMemo( + () => ({ + editAllPermission: botPermission.EditAll, + displayNamePermission: botPermission.EditDisplayName, + descriptionPermission: botPermission.EditDescription, + }), + [botPermission] + ); - const descriptionPermission = useMemo( - () => botPermission.EditDescription, - [botPermission] - ); + const initLimits = async () => { + const limits = await getResourceLimit('bot', false); + + setDisableFields(limits.configuredLimit.disabledFields ?? []); + }; const fetchRoles = async () => { try { @@ -219,6 +225,7 @@ const BotDetails: FC = ({ useEffect(() => { fetchRoles(); + initLimits(); }, []); useEffect(() => { @@ -257,6 +264,7 @@ const BotDetails: FC = ({ isBot botData={botData} botUserData={botUserData} + disabled={disableFields.includes('token')} revokeTokenHandlerBot={revokeTokenHandler} />
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.test.tsx index a0796267a79b..6072023b3159 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.test.tsx @@ -11,18 +11,12 @@ * limitations under the License. */ -import { - act, - findByTestId, - findByText, - fireEvent, - render, - screen, -} from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { OperationPermission } from '../../../../context/PermissionProvider/PermissionProvider.interface'; import { getAuthMechanismForBotUser } from '../../../../rest/userAPI'; +import AccessTokenCard from '../../Users/AccessTokenCard/AccessTokenCard.component'; import BotDetails from './BotDetails.component'; const revokeTokenHandler = jest.fn(); @@ -100,6 +94,10 @@ jest.mock('../../../../utils/PermissionsUtils', () => ({ checkPermission: jest.fn().mockReturnValue(true), })); +const mockGetResourceLimit = jest.fn().mockResolvedValue({ + configuredLimit: { disabledFields: [] }, +}); + jest.mock('../../../../rest/userAPI', () => { return { createUserWithPut: jest @@ -108,6 +106,7 @@ jest.mock('../../../../rest/userAPI', () => { getAuthMechanismForBotUser: jest .fn() .mockImplementation(() => Promise.resolve(mockAuthMechanism)), + getRoles: jest.fn().mockImplementation(() => Promise.resolve({ data: [] })), }; }); @@ -136,75 +135,63 @@ jest.mock('../../../PageLayoutV1/PageLayoutV1', () => )) ); +jest.mock('../../Users/AccessTokenCard/AccessTokenCard.component', () => { + return jest.fn().mockReturnValue(<>AccessTokenCard); +}); + +jest.mock('../../../../context/LimitsProvider/useLimitsStore', () => ({ + useLimitStore: jest.fn().mockImplementation(() => ({ + getResourceLimit: mockGetResourceLimit, + })), +})); + describe('Test BotsDetail Component', () => { it('Should render all child elements', async () => { - const { container } = render(, { - wrapper: MemoryRouter, + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); }); - const breadCrumb = await findByTestId(container, 'breadcrumb'); + const breadCrumb = await screen.findByTestId('breadcrumb'); - const leftPanel = await findByTestId(container, 'left-panel'); - const rightPanel = await findByTestId(container, 'right-panel'); - const centerPanel = await findByTestId(container, 'center-panel'); + const leftPanel = await screen.findByTestId('left-panel'); + const rightPanel = await screen.findByTestId('right-panel'); expect(breadCrumb).toBeInTheDocument(); expect(leftPanel).toBeInTheDocument(); expect(rightPanel).toBeInTheDocument(); - expect(centerPanel).toBeInTheDocument(); + expect(AccessTokenCard).toHaveBeenCalledWith( + { + botData, + isBot: true, + botUserData, + disabled: false, + revokeTokenHandlerBot: mockProp.revokeTokenHandler, + }, + {} + ); }); - it('Should render token if token is present', async () => { - const { container } = render(, { - wrapper: MemoryRouter, + it('should call accessTokenCard with disabled, if limit has `token` as disabledFields', async () => { + mockGetResourceLimit.mockResolvedValueOnce({ + configuredLimit: { disabledFields: ['token'] }, + }); + (getAuthMechanismForBotUser as jest.Mock).mockImplementationOnce(() => { + return Promise.resolve(undefined); }); - const tokenElement = await findByTestId(container, 'token'); - const tokenExpiry = await findByTestId(container, 'token-expiry'); - - expect(tokenElement).toBeInTheDocument(); - expect(tokenExpiry).toBeInTheDocument(); - }); - - it('Test Revoke token flow', async () => { await act(async () => { render(, { wrapper: MemoryRouter, }); }); - const revokeButton = await screen.findByTestId('revoke-button'); - - expect(revokeButton).toBeInTheDocument(); - - fireEvent.click(revokeButton); - - // should open confirmartion before revoking token - const confirmationModal = await screen.findByTestId('confirmation-modal'); - - expect(confirmationModal).toBeInTheDocument(); - - const confirmButton = await screen.findByTestId('save-button'); - - expect(confirmButton).toBeInTheDocument(); - - fireEvent.click(confirmButton); - - // revoke token handler should get called - expect(revokeTokenHandler).toHaveBeenCalled(); - }); - - it('Should render the generate form if the authmechanism is empty', async () => { - (getAuthMechanismForBotUser as jest.Mock).mockImplementationOnce(() => { - return Promise.resolve(undefined); - }); - - const { container } = render(, { - wrapper: MemoryRouter, - }); - - const authMechanismForm = await findByText(container, 'label.om-jwt-token'); + expect(mockGetResourceLimit).toHaveBeenCalledWith('bot', false); - expect(authMechanismForm).toBeInTheDocument(); + expect(AccessTokenCard).toHaveBeenCalledWith( + expect.objectContaining({ disabled: true }), + {} + ); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.test.tsx new file mode 100644 index 000000000000..739c1eb66782 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.test.tsx @@ -0,0 +1,61 @@ +/* + * 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 { MemoryRouter } from 'react-router-dom'; +import LimitWrapper from '../../../../hoc/LimitWrapper'; +import BotListV1 from './BotListV1.component'; + +const mockHandleAddBotClick = jest.fn(); +const mockHandleShowDeleted = jest.fn(); + +const mockProps = { + showDeleted: false, + handleAddBotClick: mockHandleAddBotClick, + handleShowDeleted: mockHandleShowDeleted, +}; + +jest.mock('../../../../hoc/LimitWrapper', () => { + return jest.fn().mockImplementation(() => <>LimitWrapper); +}); + +describe('BotListV1', () => { + it('renders the component', () => { + render(, { wrapper: MemoryRouter }); + + expect(screen.getByText('label.show-deleted')).toBeInTheDocument(); + }); + + it('handles show deleted', async () => { + render(, { wrapper: MemoryRouter }); + const showDeletedSwitch = await screen.findByTestId('switch-deleted'); + fireEvent.click(showDeletedSwitch); + + expect(mockHandleShowDeleted).toHaveBeenCalledWith( + true, + expect.objectContaining({}) + ); + }); + + it('should render LimitWrapper', async () => { + render(, { wrapper: MemoryRouter }); + const addBotButton = screen.getByText('LimitWrapper'); + fireEvent.click(addBotButton); + // Add your assertions here + + expect(LimitWrapper).toHaveBeenCalledWith( + expect.objectContaining({ resource: 'bot' }), + {} + ); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index 4213b823602a..7d96694bee8c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -29,6 +29,7 @@ import { EntityType } from '../../../../enums/entity.enum'; import { Bot, ProviderType } from '../../../../generated/entity/bot'; import { Include } from '../../../../generated/type/include'; import { Paging } from '../../../../generated/type/paging'; +import LimitWrapper from '../../../../hoc/LimitWrapper'; import { useAuth } from '../../../../hooks/authHooks'; import { usePaging } from '../../../../hooks/paging/usePaging'; import { getBots } from '../../../../rest/botsAPI'; @@ -264,6 +265,7 @@ const BotListV1 = ({ @@ -273,13 +275,15 @@ const BotListV1 = ({ - + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx index bf29ce716733..5eb9d02f7258 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/AddIngestion/AddIngestion.component.tsx @@ -16,6 +16,7 @@ import { isEmpty, isUndefined, omit, trim } from 'lodash'; import React, { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { STEPS_FOR_ADD_INGESTION } from '../../../../constants/Ingestions.constant'; +import { useLimitStore } from '../../../../context/LimitsProvider/useLimitsStore'; import { LOADING_STATE } from '../../../../enums/common.enum'; import { FormSubmitType } from '../../../../enums/form.enum'; import { @@ -29,6 +30,7 @@ import { IngestionWorkflowData } from '../../../../interface/service.interface'; import { getIngestionFrequency } from '../../../../utils/CommonUtils'; import { getSuccessMessage } from '../../../../utils/IngestionUtils'; import { cleanWorkFlowData } from '../../../../utils/IngestionWorkflowUtils'; +import { getScheduleOptionsFromSchedules } from '../../../../utils/ScheduleUtils'; import { getIngestionName } from '../../../../utils/ServiceUtils'; import { generateUUID } from '../../../../utils/StringsUtils'; import SuccessScreen from '../../../common/SuccessScreen/SuccessScreen'; @@ -66,6 +68,16 @@ const AddIngestion = ({ }: AddIngestionProps) => { const { t } = useTranslation(); const { currentUser } = useApplicationStore(); + const { config: limitConfig } = useLimitStore(); + + const { pipelineSchedules } = + limitConfig?.limits.config.featureLimits.find( + (limit) => limit.name === 'ingestionPipeline' + ) ?? {}; + + const periodOptions = getScheduleOptionsFromSchedules( + pipelineSchedules ?? [] + ); // lazy initialization to initialize the data only once const [workflowData, setWorkflowData] = useState( @@ -288,7 +300,9 @@ const AddIngestion = ({ { const history = useHistory(); - const { currentUser } = useApplicationStore(); + const { currentUser, setInlineAlertDetails } = useApplicationStore(); const { fetchAirflowStatus } = useAirflowStatus(); const [showErrorMessage, setShowErrorMessage] = useState( @@ -153,21 +152,15 @@ const AddService = ({ await fetchAirflowStatus(); } catch (error) { - if ( - (error as AxiosError).response?.status === HTTP_STATUS_CODE.CONFLICT - ) { - showErrorToast( - t('server.entity-already-exist', { - entity: t('label.service'), - entityPlural: t('label.service-lowercase-plural'), - name: serviceConfig.serviceName, - }) - ); - - return; - } - - return error; + handleEntityCreationError({ + error: error as AxiosError, + entity: t('label.service'), + entityLowercase: t('label.service-lowercase'), + entityLowercasePlural: t('label.service-lowercase-plural'), + setInlineAlertDetails, + name: serviceConfig.serviceName, + defaultErrorType: 'create', + }); } finally { setSaveServiceState('initial'); } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/AddIngestionButton.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/AddIngestionButton.component.tsx index 0f193feb56d6..38bc0cce9d2d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/AddIngestionButton.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/AddIngestionButton.component.tsx @@ -11,13 +11,13 @@ * limitations under the License. */ -import { Button, Dropdown, Space } from 'antd'; -import classNames from 'classnames'; +import { Button, Dropdown } from 'antd'; import React, { useCallback, useMemo } from 'react'; import { useHistory } from 'react-router-dom'; import { ReactComponent as DropdownIcon } from '../../../../assets/svg/drop-down.svg'; import { MetadataServiceType } from '../../../../generated/api/services/createMetadataService'; import { PipelineType } from '../../../../generated/entity/services/ingestionPipelines/ingestionPipeline'; +import LimitWrapper from '../../../../hoc/LimitWrapper'; import { getIngestionButtonText, getIngestionTypes, @@ -35,7 +35,7 @@ function AddIngestionButton({ ingestionData, ingestionList, permissions, -}: AddIngestionButtonProps) { +}: Readonly) { const history = useHistory(); const isOpenMetadataService = useMemo( @@ -57,6 +57,23 @@ function AddIngestionButton({ [serviceCategory, serviceName] ); + // Check if service has at least one metadata pipeline available or not + const hasMetadata = useMemo( + () => + ingestionList.find( + (ingestion) => ingestion.pipelineType === PipelineType.Metadata + ), + [ingestionList] + ); + + const handleAddIngestionButtonClick = useCallback( + () => + hasMetadata + ? undefined + : handleAddIngestionClick(pipelineType ?? PipelineType.Metadata), + [hasMetadata, pipelineType, handleAddIngestionClick] + ); + const isDataSightIngestionExists = useMemo( () => ingestionData.some( @@ -76,48 +93,32 @@ function AddIngestionButton({ [pipelineType, supportedPipelineTypes, isOpenMetadataService, ingestionList] ); - // Check if service has at least one metadata pipeline available or not - const hasMetadata = useMemo( - () => - ingestionList.find( - (ingestion) => ingestion.pipelineType === PipelineType.Metadata - ), - [ingestionList] - ); if (types.length === 0) { return null; } return ( - { - handleAddIngestionClick(item.key as PipelineType); - }, - }} - placement="bottomRight" - trigger={['click']}> - - + + + ); } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/Ingestion.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/Ingestion.test.tsx index 710f56499ed8..f7d2924b159a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/Ingestion.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/Ingestion.test.tsx @@ -18,6 +18,7 @@ import { fireEvent, getAllByText, queryByTestId, + queryByText, render, } from '@testing-library/react'; import React from 'react'; @@ -84,6 +85,10 @@ jest.mock( } ); +jest.mock('./AddIngestionButton.component', () => { + return jest.fn().mockImplementation(() =>
AddIngestionButton
); +}); + jest.mock('./IngestionRecentRun/IngestionRecentRuns.component', () => ({ IngestionRecentRuns: jest .fn() @@ -148,9 +153,9 @@ describe('Test Ingestion page', () => { 'ingestion-container' ); const searchBox = await findByText(container, /Searchbar/i); - const addIngestionButton = await findByTestId( + const addIngestionButton = await findByText( container, - 'add-new-ingestion-button' + 'AddIngestionButton' ); const ingestionTable = await findByTestId(container, 'ingestion-table'); @@ -287,9 +292,9 @@ describe('Test Ingestion page', () => { ).toBeInTheDocument(); // on click of add ingestion - const addIngestionButton = await findByTestId( + const addIngestionButton = await findByText( container, - 'add-new-ingestion-button' + 'AddIngestionButton' ); fireEvent.click(addIngestionButton); }); @@ -460,10 +465,7 @@ describe('Test Ingestion page', () => { } ); - const addIngestionButton = queryByTestId( - container, - 'add-new-ingestion-button' - ); + const addIngestionButton = queryByText(container, 'AddIngestionButton'); const loadingButton = getAllByText(container, 'ButtonSkeleton'); @@ -501,10 +503,7 @@ describe('Test Ingestion page', () => { } ); - const addIngestionButton = queryByTestId( - container, - 'add-new-ingestion-button' - ); + const addIngestionButton = queryByText(container, 'AddIngestionButton'); expect(addIngestionButton).not.toBeInTheDocument(); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConfig/ConnectionConfigForm.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConfig/ConnectionConfigForm.tsx index 217cfc3bce6f..c67019b99f53 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConfig/ConnectionConfigForm.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/ServiceConfig/ConnectionConfigForm.tsx @@ -14,7 +14,7 @@ import Form, { IChangeEvent } from '@rjsf/core'; import validator from '@rjsf/validator-ajv8'; import { t } from 'i18next'; -import { cloneDeep, isEmpty, isNil } from 'lodash'; +import { cloneDeep, isEmpty, isNil, isUndefined } from 'lodash'; import { LoadingState } from 'Models'; import React, { Fragment, @@ -33,6 +33,7 @@ import { MessagingServiceType } from '../../../../generated/entity/services/mess import { PipelineServiceType } from '../../../../generated/entity/services/pipelineService'; import { SearchServiceType } from '../../../../generated/entity/services/searchService'; import { useAirflowStatus } from '../../../../hooks/useAirflowStatus'; +import { useApplicationStore } from '../../../../hooks/useApplicationStore'; import { ConfigData, ServicesType, @@ -42,6 +43,7 @@ import { formatFormDataForSubmit } from '../../../../utils/JSONSchemaFormUtils'; import serviceUtilClassBase from '../../../../utils/ServiceUtilClassBase'; import AirflowMessageBanner from '../../../common/AirflowMessageBanner/AirflowMessageBanner'; import FormBuilder from '../../../common/FormBuilder/FormBuilder'; +import InlineAlert from '../../../common/InlineAlert/InlineAlert'; import TestConnection from '../../../common/TestConnection/TestConnection'; interface Props { @@ -72,6 +74,7 @@ const ConnectionConfigForm: FunctionComponent = ({ const config = !isNil(data) ? ((data as ServicesType).connection?.config as ConfigData) : ({} as ConfigData); + const { inlineAlertDetails } = useApplicationStore(); const formRef = useRef>(null); @@ -222,6 +225,9 @@ const ConnectionConfigForm: FunctionComponent = ({ onValidateFormRequiredFields={handleRequiredFieldsValidation} /> )} + {!isUndefined(inlineAlertDetails) && ( + + )} ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx index 54a0646a872d..1f6961a03e56 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.test.tsx @@ -18,6 +18,7 @@ import { PIPELINE_SERVICE_PLATFORM } from '../../../constants/Services.constant' import { CursorType } from '../../../enums/pagination.enum'; import { ServiceCategory } from '../../../enums/service.enum'; import { PipelineServiceType } from '../../../generated/entity/data/pipeline'; +import LimitWrapper from '../../../hoc/LimitWrapper'; import { getServices } from '../../../rest/serviceAPI'; import { PagingHandlerParams } from '../../common/NextPrevious/NextPrevious.interface'; import Services from './Services'; @@ -263,6 +264,12 @@ jest.mock('react-router-dom', () => ({ )), })); +jest.mock('../../../hoc/LimitWrapper', () => { + return jest + .fn() + .mockImplementation(({ children }) => <>LimitWrapper{children}); +}); + describe('Services', () => { it('should render Services', async () => { await act(async () => { @@ -295,6 +302,11 @@ describe('Services', () => { }); expect(await screen.findByTestId('add-service-button')).toBeInTheDocument(); + + expect(LimitWrapper).toHaveBeenCalledWith( + expect.objectContaining({ resource: 'services' }), + {} + ); }); it('should call mock push add service skeleton loader while airflow status is not fetching', async () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx index 5181858e2f54..d000fd07851a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Services.tsx @@ -39,6 +39,7 @@ import { ServiceCategory } from '../../../enums/service.enum'; import { Operation } from '../../../generated/entity/policies/policy'; import { EntityReference } from '../../../generated/entity/type'; import { Include } from '../../../generated/type/include'; +import LimitWrapper from '../../../hoc/LimitWrapper'; import { usePaging } from '../../../hooks/paging/usePaging'; import { useAirflowStatus } from '../../../hooks/useAirflowStatus'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; @@ -461,16 +462,18 @@ const Services = ({ serviceName }: ServicesProps) => { : NO_PERMISSION_FOR_ACTION }> {addServicePermission && !isPlatFormDisabled && ( - + + + )} )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/AccessTokenCard/AccessTokenCard.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/AccessTokenCard/AccessTokenCard.component.tsx index c29de86b36ea..1236550a1d9d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/AccessTokenCard/AccessTokenCard.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/AccessTokenCard/AccessTokenCard.component.tsx @@ -11,10 +11,11 @@ * limitations under the License. */ -import { Card } from 'antd'; +import { Card, Tooltip } from 'antd'; import { AxiosError } from 'axios'; import classNames from 'classnames'; import { t } from 'i18next'; +import { noop } from 'lodash'; import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { USER_DEFAULT_AUTHENTICATION_MECHANISM } from '../../../../constants/User.constants'; import { PersonalAccessToken } from '../../../../generated/auth/personalAccessToken'; @@ -34,6 +35,7 @@ import Loader from '../../../common/Loader/Loader'; import ConfirmationModal from '../../../Modals/ConfirmationModal/ConfirmationModal'; import AuthMechanism from '../../Bot/BotDetails/AuthMechanism'; import AuthMechanismForm from '../../Bot/BotDetails/AuthMechanismForm'; +import './access-token-card.less'; import { MockProps } from './AccessTokenCard.interfaces'; const AccessTokenCard: FC = ({ @@ -41,6 +43,7 @@ const AccessTokenCard: FC = ({ botData, botUserData, revokeTokenHandlerBot, + disabled = false, }: MockProps) => { const [isUpdating, setIsUpdating] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -206,34 +209,31 @@ const AccessTokenCard: FC = ({ setIsModalOpen(false); }; - return isLoading ? ( - - ) : ( + const tokenCard = ( - <> - {isAuthMechanismEdit ? ( - setIsAuthMechanismEdit(false)} - onSave={onSave} - /> - ) : ( - setIsModalOpen(true)} - /> - )} - + {isAuthMechanismEdit ? ( + setIsAuthMechanismEdit(false)} + onSave={onSave} + /> + ) : ( + setIsModalOpen(true)} + /> + )} = ({ isLoading={isTokenRemoving} visible={isModalOpen} onCancel={() => setIsModalOpen(false)} - onConfirm={handleTokenRevoke} + onConfirm={disabled ? noop : handleTokenRevoke} /> ); + + return isLoading ? ( + + ) : disabled ? ( + {tokenCard} + ) : ( + tokenCard + ); }; export default AccessTokenCard; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/AccessTokenCard/AccessTokenCard.interfaces.ts b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/AccessTokenCard/AccessTokenCard.interfaces.ts index 58e85629065b..2c16d16546e5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/AccessTokenCard/AccessTokenCard.interfaces.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/AccessTokenCard/AccessTokenCard.interfaces.ts @@ -19,4 +19,5 @@ export type MockProps = { botData?: Bot; botUserData?: User; revokeTokenHandlerBot?: () => Promise; + disabled?: boolean; }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/AccessTokenCard/access-token-card.less b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/AccessTokenCard/access-token-card.less new file mode 100644 index 000000000000..f391ff50ece8 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/AccessTokenCard/access-token-card.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. + */ +.access-token-card { + &.disabled { + opacity: 0.5; + user-select: none; + cursor: not-allowed; + + .ant-card-body { + pointer-events: none; + } + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/CreateUser/CreateUser.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/CreateUser/CreateUser.component.tsx index e5e017f603d0..380812bf5393 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/CreateUser/CreateUser.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/CreateUser/CreateUser.component.tsx @@ -23,7 +23,7 @@ import { Switch, } from 'antd'; import { AxiosError } from 'axios'; -import { compact, isEmpty, map, trim } from 'lodash'; +import { compact, isEmpty, isUndefined, map, trim } from 'lodash'; import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ReactComponent as IconSync } from '../../../../assets/svg/ic-sync.svg'; @@ -47,6 +47,7 @@ import { handleSearchFilterOption } from '../../../../utils/CommonUtils'; import { getEntityName } from '../../../../utils/EntityUtils'; import { showErrorToast } from '../../../../utils/ToastUtils'; import CopyToClipboardButton from '../../../common/CopyToClipboardButton/CopyToClipboardButton'; +import InlineAlert from '../../../common/InlineAlert/InlineAlert'; import Loader from '../../../common/Loader/Loader'; import RichTextEditor from '../../../common/RichTextEditor/RichTextEditor'; import TeamsSelectable from '../../Team/TeamsSelectable/TeamsSelectable'; @@ -63,7 +64,7 @@ const CreateUser = ({ }: CreateUserProps) => { const { t } = useTranslation(); const [form] = Form.useForm(); - const { authConfig } = useApplicationStore(); + const { authConfig, inlineAlertDetails } = useApplicationStore(); const [isAdmin, setIsAdmin] = useState(false); const [isBot, setIsBot] = useState(forceBot); const [selectedTeams, setSelectedTeams] = useState< @@ -376,6 +377,10 @@ const CreateUser = ({ )} + {!isUndefined(inlineAlertDetails) && ( + + )} +
+ } + type={type} + onClose={onClose} + /> + ); +} + +export default InlineAlert; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/InlineAlert/inline-alert.less b/openmetadata-ui/src/main/resources/ui/src/components/common/InlineAlert/inline-alert.less new file mode 100644 index 000000000000..f08522ff0314 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/InlineAlert/inline-alert.less @@ -0,0 +1,66 @@ +/* + * 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. + */ +.inline-error-container { + border-radius: 10px; + + .alert-icon { + font-size: 16px; + height: 16px; + width: 16px; + } + + .error-icon { + color: #ff4e27; + } + + .warning-icon { + color: #ff7c50; + } + + .success-icon { + color: #43a047; + } + + .info-icon { + color: #3ca2f4; + } + + .default-icon { + color: #555f6d; + } + + &.error-alert { + border-color: #cb2531; + background-color: #fcf0f1; + } + + &.warning-alert { + border-color: #f59638; + background-color: #fff5eb; + } + + &.success-alert { + border-color: #1d7c4d; + background-color: #f4fbf7; + } + + &.info-alert { + border-color: #3062d4; + background-color: #f5f8ff; + } + + &.default-alert { + border-color: #cfd6dd; + background-color: #f5f7f9; + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/LimitBanner/LimitBanner.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/LimitBanner/LimitBanner.tsx new file mode 100644 index 000000000000..bab1da9e8856 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/LimitBanner/LimitBanner.tsx @@ -0,0 +1,63 @@ +/* + * 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. + */ +/* eslint-disable i18next/no-literal-string */ +import Icon from '@ant-design/icons'; +import { Header } from 'antd/lib/layout/layout'; +import classNames from 'classnames'; +import React from 'react'; +import { Link } from 'react-router-dom'; +import { useLimitStore } from '../../../context/LimitsProvider/useLimitsStore'; +import { ReactComponent as CloseIcon } from './../../../assets/svg/close.svg'; +import { ReactComponent as WarningIcon } from './../../../assets/svg/ic-warning-2.svg'; +import './limit-banner.less'; + +export const LimitBanner = () => { + const { bannerDetails, setBannerDetails } = useLimitStore(); + + return ( + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/LimitBanner/limit-banner.less b/openmetadata-ui/src/main/resources/ui/src/components/common/LimitBanner/limit-banner.less new file mode 100644 index 000000000000..831e1068a572 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/LimitBanner/limit-banner.less @@ -0,0 +1,49 @@ +/* + * 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'); + +.main-container { + .pricing-banner { + background-color: @yellow-5; + padding: 0 24px; + display: flex; + justify-content: space-between; + align-items: center; + line-height: inherit; + + .anticon { + color: #ffab2a; + } + + &.errored { + background-color: @red-5; + + .anticon { + color: #f44336; + } + } + + .pricing-header { + font-size: 14px; + line-height: 21px; + font-weight: 600; + } + + .pricing-subheader { + font-size: 12px; + line-height: 18px; + color: #757575; + } + } +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ManageButtonContentItem/ManageButtonContentItem.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ManageButtonContentItem/ManageButtonContentItem.component.tsx index 7b9cb2b40da4..7768c26810d1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ManageButtonContentItem/ManageButtonContentItem.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ManageButtonContentItem/ManageButtonContentItem.component.tsx @@ -12,6 +12,7 @@ */ import { Col, Row, Typography } from 'antd'; +import classNames from 'classnames'; import React from 'react'; import { MangeButtonItemLabelProps } from './ManageButtonItemLabel.interface'; @@ -21,11 +22,19 @@ export const ManageButtonItemLabel = ({ icon, description, id, + disabled, }: MangeButtonItemLabelProps) => { const Icon = icon; return ( - + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ManageButtonContentItem/ManageButtonItemLabel.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/ManageButtonContentItem/ManageButtonItemLabel.interface.ts index e2b9032607c1..a785ad8ff8bf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ManageButtonContentItem/ManageButtonItemLabel.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ManageButtonContentItem/ManageButtonItemLabel.interface.ts @@ -18,4 +18,5 @@ export interface MangeButtonItemLabelProps { icon: SvgComponent; description: string; id: string; + disabled?: boolean; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/ResizablePanels/ResizablePanels.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/ResizablePanels/ResizablePanels.tsx index 213b244ed067..a907c4cf01d4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/ResizablePanels/ResizablePanels.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/ResizablePanels/ResizablePanels.tsx @@ -32,7 +32,7 @@ const ResizablePanels: React.FC = ({ + style={{ height: 'calc(100vh - var(--ant-navbar-height))' }}> ; + disabledFields?: Array; + limits: { + softLimit: number; + hardLimit: number; + }; + }; + limitReached: boolean; + currentCount: number; + name: string; + }>; +} + +export type LimitConfig = { + enable: boolean; + limits: { + config: { + version: string; + plan: string; + installationType: string; + deployment: string; + companyName: string; + domain: string; + instances: number; + featureLimits: Array<{ + name: string; + maxVersions: number; + versionHistory: number; + limits: { + softLimit: number; + hardLimit: number; + }; + disableFields: Array; + pipelineSchedules?: Array; + }>; + }; + }; +}; + +export type BannerDetails = { + header: string; + subheader: string; + type: 'warning' | 'danger'; + softLimitExceed?: boolean; + hardLimitExceed?: boolean; +}; + +/** + * Store to manage the limits and resource limits + */ +export const useLimitStore = create<{ + config: null | LimitConfig; + resourceLimit: Record; + bannerDetails: BannerDetails | null; + getResourceLimit: ( + resource: string, + showBanner?: boolean + ) => Promise; + setConfig: (config: LimitConfig) => void; + setResourceLimit: ( + resource: string, + limit: ResourceLimit['featureLimitStatuses'][number] + ) => void; + setBannerDetails: (details: BannerDetails | null) => void; +}>()((set, get) => ({ + config: null, + resourceLimit: {}, + bannerDetails: null, + + setConfig: (config: LimitConfig) => { + set({ config }); + }, + setResourceLimit: ( + resource: string, + limit: ResourceLimit['featureLimitStatuses'][number] + ) => { + const { resourceLimit } = get(); + + set({ resourceLimit: { ...resourceLimit, [resource]: limit } }); + }, + setBannerDetails: (details: BannerDetails | null) => { + set({ bannerDetails: details }); + }, + getResourceLimit: async (resource: string, showBanner = true) => { + const { setResourceLimit, resourceLimit, setBannerDetails, config } = get(); + + let rLimit = resourceLimit[resource]; + + if (isNil(rLimit)) { + const limit = await getLimitByResource(resource); + + setResourceLimit(resource, limit.featureLimitStatuses[0]); + rLimit = limit.featureLimitStatuses[0]; + } + + if (rLimit) { + const { + configuredLimit: { limits }, + currentCount, + limitReached, + } = rLimit; + + const softLimitExceed = + (limits.hardLimit !== -1 && currentCount >= limits.softLimit) || true; + const hardLimitExceed = + limits.hardLimit !== -1 && currentCount >= limits.hardLimit; + + const plan = config?.limits.config.plan ?? 'FREE'; + + limitReached && + showBanner && + setBannerDetails({ + header: `You have reached ${ + hardLimitExceed ? '100%' : '75%' + } of ${plan} plan Limit in OpenMetadata. `, + type: hardLimitExceed ? 'danger' : 'warning', + subheader: `You have used ${currentCount} out of ${ + limits.hardLimit + } limit for resource ${capitalize(resource)}.`, + softLimitExceed, + hardLimitExceed, + }); + } + + return rLimit; + }, +})); diff --git a/openmetadata-ui/src/main/resources/ui/src/hoc/LimitWrapper.tsx b/openmetadata-ui/src/main/resources/ui/src/hoc/LimitWrapper.tsx new file mode 100644 index 000000000000..0c4f4f3baedf --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/hoc/LimitWrapper.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2022 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Skeleton, Tooltip } from 'antd'; +import classNames from 'classnames'; +import { noop } from 'lodash'; +import React, { ReactElement, useEffect, useState } from 'react'; +import { useLimitStore } from '../context/LimitsProvider/useLimitsStore'; + +interface LimitWrapperProps { + children: ReactElement; + resource: string; +} + +/** + * Component that will responsible to limit the action based on limit api response + * If limit is disabled it simply return the children + * @param resource -- resource name, required to identify the limits applicable based on name + * @param children -- children component that need to be wrapped + * @returns - Wrapped component + */ +const LimitWrapper = ({ resource, children }: LimitWrapperProps) => { + const { getResourceLimit, resourceLimit, config, setBannerDetails } = + useLimitStore(); + const [loading, setLoading] = useState(true); + + const initResourceLimit = async () => { + await getResourceLimit(resource); + + setLoading(false); + }; + + useEffect(() => { + if (resource) { + initResourceLimit(); + } + + return () => { + setBannerDetails(null); + }; + }, [resource]); + const currentLimits = resourceLimit[resource]; + + const limitReached = currentLimits?.limitReached; + + // If limit configuration is disabled or current count is -1, then return the children + if (!config?.enable || currentLimits?.currentCount === -1) { + return children; + } + + if (loading) { + return ; + } + + return limitReached ? ( + + {React.cloneElement(children, { + disabled: true, + onClick: noop, + classNames: classNames(children.props.className, 'disabled'), + })} + + ) : ( + children + ); +}; + +export default LimitWrapper; diff --git a/openmetadata-ui/src/main/resources/ui/src/hooks/useApplicationStore.ts b/openmetadata-ui/src/main/resources/ui/src/hooks/useApplicationStore.ts index a9756729fd32..6a75b033256d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/hooks/useApplicationStore.ts +++ b/openmetadata-ui/src/main/resources/ui/src/hooks/useApplicationStore.ts @@ -49,6 +49,11 @@ export const useApplicationStore = create()( oidcIdToken: '', refreshTokenKey: '', searchCriteria: '', + inlineAlertDetails: undefined, + + setInlineAlertDetails: (inlineAlertDetails) => { + set({ inlineAlertDetails }); + }, setHelperFunctionsRef: (helperFunctions: HelperFunctions) => { set({ ...helperFunctions }); diff --git a/openmetadata-ui/src/main/resources/ui/src/interface/store.interface.ts b/openmetadata-ui/src/main/resources/ui/src/interface/store.interface.ts index da54d84dc166..be56ecb01261 100644 --- a/openmetadata-ui/src/main/resources/ui/src/interface/store.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/interface/store.interface.ts @@ -16,6 +16,7 @@ import { IAuthContext, OidcUser, } from '../components/Auth/AuthProviders/AuthProvider.interface'; +import { InlineAlertProps } from '../components/common/InlineAlert/InlineAlert.interface'; import { EntityUnion, ExploreSearchIndex, @@ -53,6 +54,8 @@ export interface ApplicationStore applicationConfig?: UIThemePreference; searchCriteria: ExploreSearchIndex | ''; theme: UIThemePreference['customTheme']; + inlineAlertDetails?: InlineAlertProps; + setInlineAlertDetails: (alertDetails?: InlineAlertProps) => void; setSelectedPersona: (persona: EntityReference) => void; setApplicationConfig: (config: UIThemePreference) => void; setCurrentUser: (user: User) => void; diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json index 0442adba29d1..3847e5b1c301 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json @@ -1849,6 +1849,7 @@ "entity-fetch-error": "Fehler beim Abrufen von {{entity}}", "entity-fetch-version-error": "Fehler beim Abrufen von {{entity}} Versionen {{version}}", "entity-follow-error": "Fehler beim Folgen von {{entity}}", + "entity-limit-reached": "{{entity}} limit reached", "entity-removing-error": "Fehler beim Entfernen von {{entity}}", "entity-unfollow-error": "Fehler beim Nicht-Folgen von {{entity}}", "entity-updating-error": "Fehler beim Aktualisieren von {{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 42160b19dfd7..f866fee75206 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -1849,6 +1849,7 @@ "entity-fetch-error": "Error while fetching {{entity}}", "entity-fetch-version-error": "Error while fetching {{entity}} versions {{version}}", "entity-follow-error": "Error while following {{entity}}", + "entity-limit-reached": "{{entity}} limit reached", "entity-removing-error": "Error while removing {{entity}}", "entity-unfollow-error": "Error while unfollowing {{entity}}", "entity-updating-error": "Error while updating {{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index f3c30a51a969..84722ff374a9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -1849,6 +1849,7 @@ "entity-fetch-error": "Error al recuperar {{entity}}", "entity-fetch-version-error": "Error al recuperar {{entity}} versiones {{version}}", "entity-follow-error": "Error al seguir {{entity}}", + "entity-limit-reached": "{{entity}} limit reached", "entity-removing-error": "Error al eliminar {{entity}}", "entity-unfollow-error": "Error al dejar de seguir {{entity}}", "entity-updating-error": "Error al actualizar {{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index 622aa906141e..85fd948b4bb5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -1849,6 +1849,7 @@ "entity-fetch-error": "Une erreur est survenue lors de la récupération de {{entity}}", "entity-fetch-version-error": "Erreur lors de la récupération des versions de {{entity}} {{version}}", "entity-follow-error": "Erreur lors du suivi de {{entity}}", + "entity-limit-reached": "{{entity}} limit reached", "entity-removing-error": "Erreur lors de la suppression de {{entity}}", "entity-unfollow-error": "Erreur lors de l'arrêt de suivi de {{entity}}", "entity-updating-error": "Erreur lors de la mise à jour de {{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json index d733a878f2bc..a4ac1ae8cc61 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json @@ -1849,6 +1849,7 @@ "entity-fetch-error": "שגיאה במהלך אחזור {{entity}}", "entity-fetch-version-error": "שגיאה במהלך אחזור {{entity}} גרסה {{version}}", "entity-follow-error": "שגיאה במהלך עקיבה אחרי {{entity}}", + "entity-limit-reached": "{{entity}} limit reached", "entity-removing-error": "שגיאה במהלך הסרת {{entity}}", "entity-unfollow-error": "שגיאה במהלך ביטול עקיבה אחרי {{entity}}", "entity-updating-error": "שגיאה במהלך עדכון {{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index f205389e4315..098709d2a5fb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -1849,6 +1849,7 @@ "entity-fetch-error": "{{entity}}の取得中にエラーが発生しました", "entity-fetch-version-error": "{{entity}} バージョン {{version}}の取得中にエラーが発生しました", "entity-follow-error": "Error while following {{entity}}", + "entity-limit-reached": "{{entity}} limit reached", "entity-removing-error": "Error while removing {{entity}}", "entity-unfollow-error": "Error while unfollowing {{entity}}", "entity-updating-error": "{{entity}}の更新中にエラーが発生しました", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json index 1546c42169ec..e66df03fc8bf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json @@ -1849,6 +1849,7 @@ "entity-fetch-error": "Fout bij het ophalen van {{entity}}", "entity-fetch-version-error": "Fout bij het ophalen van {{entity}}-versies {{version}}", "entity-follow-error": "Fout bij het volgen van {{entity}}", + "entity-limit-reached": "{{entity}} limit reached", "entity-removing-error": "Fout bij het verwijderen van {{entity}}", "entity-unfollow-error": "Fout bij het ontvolgen van {{entity}}", "entity-updating-error": "Fout bij het updaten van {{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index ef7e35579a9e..e19dd7b86c3c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -1849,6 +1849,7 @@ "entity-fetch-error": "Erro ao buscar {{entity}}", "entity-fetch-version-error": "Erro ao buscar versões de {{entity}} {{version}}", "entity-follow-error": "Erro ao seguir {{entity}}", + "entity-limit-reached": "{{entity}} limit reached", "entity-removing-error": "Erro ao remover {{entity}}", "entity-unfollow-error": "Erro ao deixar de seguir {{entity}}", "entity-updating-error": "Erro ao atualizar {{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json index bbccb8acbce4..6909a6b6faf1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json @@ -1849,6 +1849,7 @@ "entity-fetch-error": "Ошибка при получении {{entity}}", "entity-fetch-version-error": "Ошибка при получении {{entity}} версий {{version}}", "entity-follow-error": "Ошибка при подписке на {{entity}}", + "entity-limit-reached": "{{entity}} limit reached", "entity-removing-error": "Ошибка при удалении {{entity}}.", "entity-unfollow-error": "Ошибка при отмене подписки на {{entity}}", "entity-updating-error": "Ошибка при обновлении {{entity}}", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index f2becbf09750..015f16ecca2c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -1849,6 +1849,7 @@ "entity-fetch-error": "获取{{entity}}时发生错误!", "entity-fetch-version-error": "获取{{entity}}版本{{version}}时发生错误!", "entity-follow-error": "关注{{entity}}时发生错误", + "entity-limit-reached": "{{entity}} limit reached", "entity-removing-error": "删除{{entity}}时发生错误", "entity-unfollow-error": "取消关注{{entity}}时发生错误", "entity-updating-error": "更新{{entity}}时发生错误", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.tsx index fa6beb7b57c0..a1b0bd1e4b1c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/AddNotificationPage/AddNotificationPage.tsx @@ -13,10 +13,11 @@ */ import { Button, Col, Form, Input, Row, Skeleton, Typography } from 'antd'; import { useForm } from 'antd/lib/form/Form'; -import { isEmpty } from 'lodash'; +import { isEmpty, isUndefined } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router-dom'; +import InlineAlert from '../../components/common/InlineAlert/InlineAlert'; import Loader from '../../components/common/Loader/Loader'; import ResizablePanels from '../../components/common/ResizablePanels/ResizablePanels'; import RichTextEditor from '../../components/common/RichTextEditor/RichTextEditor'; @@ -32,6 +33,7 @@ import { SubscriptionCategory, } from '../../generated/events/eventSubscription'; import { FilterResourceDescriptor } from '../../generated/events/filterResourceDescriptor'; +import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { createNotificationAlert, @@ -55,6 +57,7 @@ const AddNotificationPage = () => { const [form] = useForm(); const history = useHistory(); const { fqn } = useFqn(); + const { setInlineAlertDetails, inlineAlertDetails } = useApplicationStore(); const [loadingCount, setLoadingCount] = useState(0); const [entityFunctions, setEntityFunctions] = useState< @@ -153,6 +156,7 @@ const AddNotificationPage = () => { afterSaveAction: () => { history.push(getNotificationAlertDetailsPath(data.name)); }, + setInlineAlertDetails, }); } catch { // Error handling done in "handleAlertSave" @@ -274,6 +278,13 @@ const AddNotificationPage = () => { + + {!isUndefined(inlineAlertDetails) && ( + + + + )} +
- + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ClassificationVersionPage/ClassificationVersionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ClassificationVersionPage/ClassificationVersionPage.tsx index 4f80906ef552..4e00693d6add 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ClassificationVersionPage/ClassificationVersionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ClassificationVersionPage/ClassificationVersionPage.tsx @@ -27,6 +27,7 @@ import { ResourceEntity, } from '../../context/PermissionProvider/PermissionProvider.interface'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; +import { EntityType } from '../../enums/entity.enum'; import { Classification } from '../../generated/entity/classification/classification'; import { EntityHistory } from '../../generated/type/entityHistory'; import { useFqn } from '../../hooks/useFqn'; @@ -170,6 +171,7 @@ function ClassificationVersionPage() { { const history = useHistory(); const { t } = useTranslation(); + const { setInlineAlertDetails } = useApplicationStore(); const [roles, setRoles] = useState>([]); const [isLoading, setIsLoading] = useState(false); @@ -60,18 +61,6 @@ const CreateUserPage = () => { goToUserListPage(); }; - /** - * Handles error if any, while creating new user. - * @param error AxiosError or error message - * @param fallbackText fallback error message - */ - const handleSaveFailure = ( - error: AxiosError | string, - fallbackText?: string - ) => { - showErrorToast(error, fallbackText); - }; - const checkBotInUse = async (name: string) => { try { const response = await getBotByName(name); @@ -117,10 +106,17 @@ const CreateUserPage = () => { ); goToUserListPage(); } catch (error) { - handleSaveFailure( - error as AxiosError, - t('server.create-entity-error', { entity: t('label.bot') }) - ); + setInlineAlertDetails({ + type: 'error', + heading: t('label.error'), + description: getUserCreationErrorMessage({ + error: error as AxiosError, + entity: t('label.bot'), + entityLowercase: t('label.bot-lowercase'), + entityName: userData.name, + }), + onClose: () => setInlineAlertDetails(undefined), + }); } } } else { @@ -128,15 +124,17 @@ const CreateUserPage = () => { await createUser(userData); goToUserListPage(); } catch (error) { - handleSaveFailure( - getIsErrorMatch(error as AxiosError, ERROR_MESSAGE.alreadyExist) - ? t('server.email-already-exist', { - entity: t('label.user-lowercase'), - name: userData.name, - }) - : (error as AxiosError), - t('server.create-entity-error', { entity: t('label.user') }) - ); + setInlineAlertDetails({ + type: 'error', + heading: t('label.error'), + description: getUserCreationErrorMessage({ + error: error as AxiosError, + entity: t('label.user'), + entityLowercase: t('label.user-lowercase'), + entityName: userData.name, + }), + onClose: () => setInlineAlertDetails(undefined), + }); } } setIsLoading(false); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.tsx index 42fda1d1eec2..40022f1377e2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/DatabaseSchemaVersionPage/DatabaseSchemaVersionPage.tsx @@ -355,6 +355,7 @@ function DatabaseSchemaVersionPage() { { ); const settingCategoryData: SettingMenuItem | undefined = useMemo(() => { - let categoryItem = getGlobalSettingsMenuWithPermission( - permissions, - isAdminUser - ).find((item) => item.key === settingCategory); + let categoryItem = globalSettingsClassBase + .getGlobalSettingsMenuWithPermission(permissions, isAdminUser) + .find((item) => item.key === settingCategory); if (categoryItem) { categoryItem = { diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/GlobalSettingPage/GlobalSettingPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/GlobalSettingPage/GlobalSettingPage.tsx index 084fbfc51d4f..80357e067651 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/GlobalSettingPage/GlobalSettingPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/GlobalSettingPage/GlobalSettingPage.tsx @@ -24,9 +24,9 @@ import { PAGE_HEADERS } from '../../constants/PageHeaders.constant'; import { usePermissionProvider } from '../../context/PermissionProvider/PermissionProvider'; import { ERROR_PLACEHOLDER_TYPE } from '../../enums/common.enum'; import { useAuth } from '../../hooks/authHooks'; +import globalSettingsClassBase from '../../utils/GlobalSettingsClassBase'; import { getGlobalSettingMenuItem, - getGlobalSettingsMenuWithPermission, SettingMenuItem, } from '../../utils/GlobalSettingsUtils'; import { getSettingPath } from '../../utils/RouterUtils'; @@ -43,8 +43,9 @@ const GlobalSettingPage = () => { const settingItems = useMemo( () => - getGlobalSettingsMenuWithPermission(permissions, isAdminUser).filter( - (curr: SettingMenuItem) => { + globalSettingsClassBase + .getGlobalSettingsMenuWithPermission(permissions, isAdminUser) + .filter((curr: SettingMenuItem) => { const menuItem = getGlobalSettingMenuItem(curr); if (!isUndefined(menuItem.isProtected)) { @@ -56,8 +57,7 @@ const GlobalSettingPage = () => { } return false; - } - ), + }), [permissions, isAdminUser] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/NotificationListPage/NotificationListPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/NotificationListPage/NotificationListPage.test.tsx index f93acea3d0b4..0d974bfbab2e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/NotificationListPage/NotificationListPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/NotificationListPage/NotificationListPage.test.tsx @@ -14,6 +14,7 @@ import { act, render, screen } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { ROUTES } from '../../constants/constants'; +import LimitWrapper from '../../hoc/LimitWrapper'; import { getAllAlerts } from '../../rest/alertsAPI'; import NotificationListPage from './NotificationListPage'; @@ -84,6 +85,12 @@ jest.mock( } ); +jest.mock('../../hoc/LimitWrapper', () => { + return jest + .fn() + .mockImplementation(({ children }) => <>LimitWrapper{children}); +}); + describe('Notification Alerts Page Tests', () => { it('Title should be rendered', async () => { await act(async () => { @@ -148,4 +155,17 @@ describe('Notification Alerts Page Tests', () => { expect(alertNameElement).toBeInTheDocument(); }); + + it('should call LimitWrapper with resource as notifications', async () => { + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + expect(LimitWrapper).toHaveBeenCalledWith( + expect.objectContaining({ resource: 'notification' }), + {} + ); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/NotificationListPage/NotificationListPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/NotificationListPage/NotificationListPage.tsx index f2cdfb2f4e2a..d9673842f813 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/NotificationListPage/NotificationListPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/NotificationListPage/NotificationListPage.tsx @@ -41,6 +41,7 @@ import { ProviderType, } from '../../generated/events/eventSubscription'; import { Paging } from '../../generated/type/paging'; +import LimitWrapper from '../../hoc/LimitWrapper'; import { usePaging } from '../../hooks/paging/usePaging'; import { getAlertsFromName, getAllAlerts } from '../../rest/alertsAPI'; import { getEntityName } from '../../utils/EntityUtils'; @@ -219,15 +220,21 @@ const NotificationListPage = () => {
- - - +
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ObservabilityAlertsPage/ObservabilityAlertsPage.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ObservabilityAlertsPage/ObservabilityAlertsPage.test.tsx index cb8bb57c3ad9..2e59d1230c16 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ObservabilityAlertsPage/ObservabilityAlertsPage.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ObservabilityAlertsPage/ObservabilityAlertsPage.test.tsx @@ -13,6 +13,7 @@ import { act, render, screen } from '@testing-library/react'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; +import LimitWrapper from '../../hoc/LimitWrapper'; import { getAllAlerts } from '../../rest/alertsAPI'; import ObservabilityAlertsPage from './ObservabilityAlertsPage'; @@ -60,6 +61,12 @@ jest.mock('../../components/PageLayoutV1/PageLayoutV1', () => { return jest.fn().mockImplementation(({ children }) =>
{children}
); }); +jest.mock('../../hoc/LimitWrapper', () => { + return jest + .fn() + .mockImplementation(({ children }) => <>LimitWrapper{children}); +}); + describe('Observability Alerts Page Tests', () => { it('Title should be rendered', async () => { await act(async () => { @@ -124,4 +131,17 @@ describe('Observability Alerts Page Tests', () => { expect(alertNameElement).toBeInTheDocument(); }); + + it('should call LimitWrapper with resource as notifications', async () => { + await act(async () => { + render(, { + wrapper: MemoryRouter, + }); + }); + + expect(LimitWrapper).toHaveBeenCalledWith( + expect.objectContaining({ resource: 'alert' }), + {} + ); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ObservabilityAlertsPage/ObservabilityAlertsPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ObservabilityAlertsPage/ObservabilityAlertsPage.tsx index f0884d53321f..b7725dc12397 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ObservabilityAlertsPage/ObservabilityAlertsPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ObservabilityAlertsPage/ObservabilityAlertsPage.tsx @@ -35,6 +35,7 @@ import { ProviderType, } from '../../generated/events/eventSubscription'; import { Paging } from '../../generated/type/paging'; +import LimitWrapper from '../../hoc/LimitWrapper'; import { usePaging } from '../../hooks/paging/usePaging'; import { getAllAlerts } from '../../rest/alertsAPI'; import { getEntityName } from '../../utils/EntityUtils'; @@ -201,11 +202,14 @@ const ObservabilityAlertsPage = () => {
- - - +
diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/PageNotFound/page-not-found.less b/openmetadata-ui/src/main/resources/ui/src/pages/PageNotFound/page-not-found.less index f882e1bb9794..37e51e30f9a1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/PageNotFound/page-not-found.less +++ b/openmetadata-ui/src/main/resources/ui/src/pages/PageNotFound/page-not-found.less @@ -11,12 +11,11 @@ * limitations under the License. */ -// common css utils file @import (reference) url('../../styles/variables.less'); .page-not-found-container { position: relative; - height: calc(100vh - 64px); + height: 100vh; .not-found-text-image { position: absolute; diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionPage.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionPage.tsx index 8dab316ab0a8..281f400a5eba 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionPage.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ServiceVersionPage/ServiceVersionPage.tsx @@ -468,6 +468,7 @@ function ServiceVersionPage() { { }); it('Handlers in forms for change and submit should work properly', async () => { + (createUser as jest.Mock).mockImplementationOnce(() => + Promise.resolve(undefined) + ); + render(); const form = screen.getByTestId('create-user-form'); const fullNameInput = screen.getByTestId( @@ -135,12 +139,8 @@ describe('SignUp page', () => { expect(userNameInput).toHaveValue(mockChangedFormData.userName); expect(emailInput).toHaveValue(mockChangedFormData.email); - fireEvent.click(submitButton); - await act(async () => { - (createUser as jest.Mock).mockImplementationOnce(() => - Promise.resolve(undefined) - ); + fireEvent.click(submitButton); }); expect(createUser as jest.Mock).toHaveBeenCalledTimes(1); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx index c47bf70249bd..c4a210be0566 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.test.tsx @@ -202,6 +202,12 @@ jest.mock('../../components/common/Loader/Loader', () => { jest.useFakeTimers(); +jest.mock('../../hoc/LimitWrapper', () => { + return jest + .fn() + .mockImplementation(({ children }) => <>LimitWrapper{children}); +}); + describe('TestDetailsPageV1 component', () => { it('TableDetailsPageV1 should fetch permissions', () => { render(); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx index 0b044fa6e7b6..a92acd200e55 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/TableDetailsPageV1/TableDetailsPageV1.tsx @@ -1,3 +1,4 @@ +/* eslint-disable i18next/no-literal-string */ /* * Copyright 2023 Collate. * Licensed under the Apache License, Version 2.0 (the "License"); @@ -73,6 +74,7 @@ import { Suggestion } from '../../generated/entity/feed/suggestion'; import { ThreadType } from '../../generated/entity/feed/thread'; import { TestSummary } from '../../generated/tests/testCase'; import { TagLabel } from '../../generated/type/tagLabel'; +import LimitWrapper from '../../hoc/LimitWrapper'; import { useApplicationStore } from '../../hooks/useApplicationStore'; import { useFqn } from '../../hooks/useFqn'; import { useSub } from '../../hooks/usePubSub'; @@ -1088,7 +1090,6 @@ const TableDetailsPageV1: React.FC = () => { onVersionClick={versionHandler} /> - {/* Entity Tabs */} { onChange={handleTabChange} /> - + + <> + {threadLink ? ( { {isAdminUser && ( - + + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/limitsAPI.interface.ts b/openmetadata-ui/src/main/resources/ui/src/rest/limitsAPI.interface.ts new file mode 100644 index 000000000000..38f08fe37613 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/rest/limitsAPI.interface.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ + +export interface ResourceLimitsParams { + cache?: boolean; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/limitsAPI.test.ts b/openmetadata-ui/src/main/resources/ui/src/rest/limitsAPI.test.ts new file mode 100644 index 000000000000..15b46d55367f --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/rest/limitsAPI.test.ts @@ -0,0 +1,81 @@ +/* + * 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 axiosClient from './'; +import { getLimitByResource, getLimitConfig } from './limitsAPI'; + +jest.mock('./', () => ({ + get: jest.fn(), +})); + +describe('limitsAPI -- getLimitConfig', () => { + it('should fetch limit config successfully', async () => { + const mockResponse = { + data: { + enabled: true, + }, + }; + + (axiosClient.get as jest.Mock).mockResolvedValue(mockResponse); + + const result = await getLimitConfig(); + + expect(axiosClient.get).toHaveBeenCalledWith('/limits/config'); + expect(result).toEqual(mockResponse.data); + }); + + it('should handle error when fetching limit config', async () => { + const mockError = new Error('Failed to fetch limit config'); + + (axiosClient.get as jest.Mock).mockRejectedValue(mockError); + + await expect(getLimitConfig()).rejects.toThrow(mockError); + }); +}); + +describe('limitsAPI -- getLimitByResource', () => { + describe('getLimitByResource', () => { + it('should fetch limit by resource successfully', async () => { + const mockResource = 'exampleResource'; + const mockResponse = { + data: { + resource: mockResource, + used: 1, + enable: true, + assetLimits: { + softLimit: 1, + hardLimit: 1, + }, + }, + }; + + (axiosClient.get as jest.Mock).mockResolvedValue(mockResponse); + + const result = await getLimitByResource(mockResource); + + expect(axiosClient.get).toHaveBeenCalledWith( + '/limits/features/exampleResource', + { params: undefined } + ); + expect(result).toEqual(mockResponse.data); + }); + + it('should handle error when fetching limit by resource', async () => { + const mockResource = 'exampleResource'; + const mockError = new Error('Failed to fetch limit by resource'); + + (axiosClient.get as jest.Mock).mockRejectedValue(mockError); + + await expect(getLimitByResource(mockResource)).rejects.toThrow(mockError); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/limitsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/limitsAPI.ts new file mode 100644 index 000000000000..fdd79ee673da --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/rest/limitsAPI.ts @@ -0,0 +1,38 @@ +/* + * 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 axiosClient from '.'; +import { + LimitConfig, + ResourceLimit, +} from '../context/LimitsProvider/useLimitsStore'; +import { ResourceLimitsParams } from './limitsAPI.interface'; + +const BASE_URL = '/limits'; + +export const getLimitConfig = async () => { + const response = await axiosClient.get(`${BASE_URL}/config`); + + return response.data; +}; + +export const getLimitByResource = async ( + resource: string, + params?: ResourceLimitsParams +) => { + const response = await axiosClient.get( + `${BASE_URL}/features/${resource}`, + { params } + ); + + return response.data; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/antd-master.less b/openmetadata-ui/src/main/resources/ui/src/styles/antd-master.less index 882d1310c2dd..78a2d9516921 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/antd-master.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/antd-master.less @@ -11,7 +11,7 @@ * limitations under the License. */ -@import '~antd/dist/antd.variable.less'; +@import url('antd/dist/antd.variable.less'); @import url('./variables.less'); @import url('./position.less'); @import url('./spacing.less'); diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/app.less b/openmetadata-ui/src/main/resources/ui/src/styles/app.less index e520d4623a23..60298abcaa45 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/app.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/app.less @@ -11,7 +11,6 @@ * limitations under the License. */ -// common css utils file @import (reference) url('./variables.less'); /* Generic */ @@ -564,7 +563,7 @@ a[href].link-text-grey, } .full-height { - height: calc(100vh - 64px); // removing only navbar height + height: calc(100vh - @om-navbar-height); // removing only navbar height } .tab-content-height { diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/components/entity-version-time-line.less b/openmetadata-ui/src/main/resources/ui/src/styles/components/entity-version-time-line.less index daf4d205ee31..94689fb2d769 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/components/entity-version-time-line.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/components/entity-version-time-line.less @@ -11,11 +11,11 @@ * limitations under the License. */ -@import url('../variables.less'); +@import (reference) url('../variables.less'); .versions-list-container { position: fixed; - top: 64px; + top: @om-navbar-height; .ant-drawer-content-wrapper { box-shadow: none; @@ -55,6 +55,12 @@ .timeline-content { display: flex; + + &.limit-reached { + opacity: 0.5; + + cursor: not-allowed; + } } .timeline-wrapper { padding-right: 1rem; @@ -109,3 +115,9 @@ background-color: @primary-1; transform: translate(8px, 0); } + +.version-timestamp::before { + content: '\2022'; + margin-right: 6px; + margin-left: 4px; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/components/react-awesome-query.less b/openmetadata-ui/src/main/resources/ui/src/styles/components/react-awesome-query.less index cdc25d09f0cc..d9bb04362eb7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/components/react-awesome-query.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/components/react-awesome-query.less @@ -11,7 +11,7 @@ * limitations under the License. */ -@import url('../variables.less'); +@import (reference) url('../variables.less'); .selectTextOverflow() { text-overflow: ellipsis; diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/layout/page-layout.less b/openmetadata-ui/src/main/resources/ui/src/styles/layout/page-layout.less index 617e7256caab..4d2cea492ece 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/layout/page-layout.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/layout/page-layout.less @@ -11,11 +11,11 @@ * limitations under the License. */ -@import url('../variables.less'); -@import (reference) '~antd/dist/antd.less'; +@import (reference) url('../variables.less'); +@import (reference) url('antd/dist/antd.less'); .page-layout-v1-vertical-scroll { - height: calc(100vh - 64px); + height: calc(100vh - @om-navbar-height); overflow-y: auto; overflow-x: hidden; } diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/temp.css b/openmetadata-ui/src/main/resources/ui/src/styles/temp.css index 1d9db805808b..0f97f437bd94 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/temp.css +++ b/openmetadata-ui/src/main/resources/ui/src/styles/temp.css @@ -811,3 +811,11 @@ body .list-option.rdw-option-active { color: #fff !important; opacity: 0.5 !important; } + +:root { + --ant-navbar-height: 64px; +} + +:root .extra-banner { + --ant-navbar-height: 128px; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/variables.less b/openmetadata-ui/src/main/resources/ui/src/styles/variables.less index d069d641ae6f..8ffaf65135d8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/variables.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/variables.less @@ -101,23 +101,36 @@ @trigger-btn-hover-bg: #efefef; @text-highlighter: #ffc34e40; @team-avatar-bg: #0950c51a; -@navbar-height: 64px; +@om-navbar-height: ~'var(--ant-navbar-height)'; @sidebar-width: 60px; // Sizing -@page-height: calc(100vh - 64px); +@page-height: calc(100vh - @om-navbar-height); @left-side-panel-width: 230px; -@entity-details-tab-height: calc(100vh - 236px); + +// 172px - navbar height +@entity-details-tab-height: calc(100vh - 172px - @om-navbar-height); @users-page-tabs-height: calc( - 100vh - 120px + 100vh - @om-navbar-height - 58px ); /* navbar+tab_height+padding = 64+46+12 */ -@glossary-page-height: calc(100vh - 206px); -@domain-page-height: calc(100vh - 200px); -@glossary-term-page-height: calc(100vh - 200px); -@explore-page-height: calc(100vh - 113px); -@welcome-page-height: calc(100vh - 112px); -@data-product-page-height: calc(100vh - 156px); -@glossary-page-tab-height: calc(100vh - 206px); + +// 142px - navbar height +@glossary-page-height: calc(100vh - 142px - @om-navbar-height); + +// 136px - navbar height +@domain-page-height: calc(100vh - 136px - @om-navbar-height); + +// 136px - navbar height +@glossary-term-page-height: calc(100vh - 136px -@om-navbar-height); + +// Navbar height - 49px for filter bar on explore page +@explore-page-height: calc(100vh - @om-navbar-height - 49px); + +// 48px - navbar height +@welcome-page-height: calc(100vh - 48px - @om-navbar-height); + +// 152px - navbar height +@glossary-page-tab-height: calc(100vh - 152px - @om-navbar-height); @lineage-sidebar-width: 110px; // Severity diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/APIUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/APIUtils.ts index 540e94ebdf18..c9fb29e59623 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/APIUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/APIUtils.ts @@ -11,10 +11,8 @@ * limitations under the License. */ -import { get, isArray, isObject, transform } from 'lodash'; -import { FormattedTableData } from 'Models'; +import { isArray, isObject, transform } from 'lodash'; import { SearchIndex } from '../enums/search.enum'; -import { Tag } from '../generated/entity/classification/tag'; import { GlossaryTerm } from '../generated/entity/data/glossaryTerm'; import { DataProduct } from '../generated/entity/domains/dataProduct'; import { Domain } from '../generated/entity/domains/domain'; @@ -39,61 +37,6 @@ export type SearchEntityHits = SearchResponse< | SearchIndex.SEARCH_INDEX >['hits']['hits']; -// if more value is added, also update its interface file at -> interface/types.d.ts -export const formatDataResponse = ( - hits: SearchEntityHits -): FormattedTableData[] => { - const formattedData = hits.map((hit) => { - const newData = {} as FormattedTableData; - const source = hit._source; - newData.index = hit._index; - newData.id = hit._source.id ?? ''; - newData.name = hit._source.name; - newData.displayName = hit._source.displayName ?? ''; - newData.description = hit._source.description ?? ''; - newData.fullyQualifiedName = hit._source.fullyQualifiedName ?? ''; - newData.tags = get(hit, '_source.tags', []); - newData.service = get(hit, '_source.service.name'); - newData.serviceType = get(hit, '_source.serviceType'); - newData.tier = hit._source.tier; - newData.owner = get(hit, '_source.owner'); - newData.highlight = hit.highlight; - newData.entityType = hit._source.entityType; - newData.deleted = get(hit, '_source.deleted'); - - if ('tableType' in source) { - newData.tableType = source.tableType ?? ''; - } - - if ('usageSummary' in source) { - newData.dailyStats = source.usageSummary?.dailyStats?.count; - newData.dailyPercentileRank = - source.usageSummary?.dailyStats?.percentileRank; - newData.weeklyStats = source.usageSummary?.weeklyStats?.count; - newData.weeklyPercentileRank = - source.usageSummary?.weeklyStats?.percentileRank; - } - - if ('database' in source) { - newData.database = source.database?.name; - } - - if ('databaseSchema' in source) { - newData.databaseSchema = source.databaseSchema?.name; - } - - if ('columns' in source) { - newData.columns = source.columns; - } - - newData.changeDescription = source.changeDescription; - - return newData; - }); - - return formattedData; -}; - export const formatUsersResponse = ( hits: SearchResponse['hits']['hits'] ): User[] => { @@ -182,21 +125,6 @@ export const formatSearchGlossaryTermResponse = ( })); }; -export const formatSearchTagsResponse = ( - hits: SearchResponse['hits']['hits'] -): Tag[] => { - return hits.map((d) => ({ - name: d._source.name, - description: d._source.description, - id: d._source.id, - classification: d._source.classification, - displayName: d._source.displayName, - fqdn: d._source.fullyQualifiedName, - fullyQualifiedName: d._source.fullyQualifiedName, - type: d._source.entityType, - })); -}; - export const omitDeep = ( obj: T, predicate: (value: string, key: string | number | symbol) => boolean diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtil.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtil.tsx index 44538aa1ee43..88fcce7d96c5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtil.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Alerts/AlertsUtil.tsx @@ -23,13 +23,13 @@ import { ReactComponent as MSTeamsIcon } from '../../assets/svg/ms-teams.svg'; import { ReactComponent as SlackIcon } from '../../assets/svg/slack.svg'; import { ReactComponent as WebhookIcon } from '../../assets/svg/webhook.svg'; import { AsyncSelect } from '../../components/common/AsyncSelect/AsyncSelect'; +import { InlineAlertProps } from '../../components/common/InlineAlert/InlineAlert.interface'; import { DESTINATION_DROPDOWN_TABS, DESTINATION_SOURCE_ITEMS, DESTINATION_TYPE_BASED_PLACEHOLDERS, EXTERNAL_CATEGORY_OPTIONS, } from '../../constants/Alerts.constants'; -import { HTTP_STATUS_CODE } from '../../constants/Auth.constants'; import { PAGE_SIZE_LARGE } from '../../constants/constants'; import { SearchIndex } from '../../enums/search.enum'; import { StatusType } from '../../generated/entity/data/pipeline'; @@ -47,10 +47,11 @@ import { EventType } from '../../generated/type/changeEvent'; import TeamAndUserSelectItem from '../../pages/AddObservabilityPage/DestinationFormItem/TeamAndUserSelectItem/TeamAndUserSelectItem'; import { searchData } from '../../rest/miscAPI'; import { getEntityName, getEntityNameLabel } from '../EntityUtils'; +import { handleEntityCreationError } from '../formUtils'; import { getConfigFieldFromDestinationType } from '../ObservabilityUtils'; import searchClassBase from '../SearchClassBase'; import { getEntityIcon } from '../TableUtils'; -import { showErrorToast, showSuccessToast } from '../ToastUtils'; +import { showSuccessToast } from '../ToastUtils'; export const getAlertsActionTypeIcon = (type?: SubscriptionType) => { switch (type) { @@ -753,6 +754,7 @@ export const handleAlertSave = async ({ createAlertAPI, updateAlertAPI, afterSaveAction, + setInlineAlertDetails, }: { data: CreateEventSubscription; createAlertAPI: ( @@ -762,6 +764,7 @@ export const handleAlertSave = async ({ alert: CreateEventSubscription ) => Promise; afterSaveAction: () => void; + setInlineAlertDetails: (alertDetails?: InlineAlertProps | undefined) => void; fqn?: string; }) => { try { @@ -814,22 +817,15 @@ export const handleAlertSave = async ({ ); afterSaveAction(); } catch (error) { - if ((error as AxiosError).response?.status === HTTP_STATUS_CODE.CONFLICT) { - showErrorToast( - t('server.entity-already-exist', { - entity: t('label.alert'), - entityPlural: t('label.alert-lowercase-plural'), - name: data.name, - }) - ); - } else { - showErrorToast( - error as AxiosError, - t(`server.${'entity-creation-error'}`, { - entity: t('label.alert-lowercase'), - }) - ); - } + handleEntityCreationError({ + error: error as AxiosError, + entity: t('label.alert'), + entityLowercase: t('label.alert-lowercase'), + entityLowercasePlural: t('label.alert-lowercase-plural'), + setInlineAlertDetails, + name: data.name, + defaultErrorType: 'create', + }); } }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx index e74c2d2aef23..7d326a708316 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityUtils.tsx @@ -2051,6 +2051,7 @@ export const getEntityNameLabel = (entityName?: string) => { database: t('label.database'), query: t('label.query'), THREAD: t('label.thread'), + app: t('label.application'), }; return ( diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/EntityVersionUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/EntityVersionUtils.tsx index 65f0e9ca030d..609ee8e7d2ac 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/EntityVersionUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/EntityVersionUtils.tsx @@ -36,6 +36,7 @@ import { ExtentionEntities, ExtentionEntitiesKeys, } from '../components/common/CustomPropertyTable/CustomPropertyTable.interface'; +import { VersionButton } from '../components/Entity/EntityVersionTimeLine/EntityVersionTimeLine'; import { FQN_SEPARATOR_CHAR } from '../constants/char.constants'; import { EntityField } from '../constants/Feeds.constants'; import { EntityType } from '../enums/entity.enum'; @@ -923,3 +924,33 @@ export const getCommonDiffsFromVersionData = ( currentVersionData.description ), }); + +export const renderVersionButton = ( + version: string, + current: string, + versionHandler: (version: string) => void, + className?: string +) => { + const currV = JSON.parse(version); + + const majorVersionChecks = () => { + return isMajorVersion( + parseFloat(currV?.changeDescription?.previousVersion) + .toFixed(1) + .toString(), + parseFloat(currV?.version).toFixed(1).toString() + ); + }; + + return ( + + + + ); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsClassBase.ts new file mode 100644 index 000000000000..a29efaaa0154 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsClassBase.ts @@ -0,0 +1,457 @@ +/* + * 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 { t } from 'i18next'; +import { SettingMenuItem } from './GlobalSettingsUtils'; + +import { ReactComponent as AdminIcon } from '../assets/svg/admin-colored.svg'; +import { ReactComponent as ApplicationIcon } from '../assets/svg/application-colored.svg'; +import { ReactComponent as BotIcon } from '../assets/svg/bot-colored.svg'; +import { ReactComponent as AppearanceIcon } from '../assets/svg/custom-logo-colored.svg'; +import { ReactComponent as CustomDashboardLogoIcon } from '../assets/svg/customize-landing-page-colored.svg'; +import { ReactComponent as DashboardIcon } from '../assets/svg/dashboard-colored.svg'; +import { ReactComponent as DatabaseIcon } from '../assets/svg/database-colored.svg'; +import { ReactComponent as SchemaIcon } from '../assets/svg/database-schema.svg'; +import { ReactComponent as EmailIcon } from '../assets/svg/email-colored.svg'; +import { ReactComponent as GlossaryIcon } from '../assets/svg/glossary-colored.svg'; +import { ReactComponent as LoginIcon } from '../assets/svg/login-colored.svg'; +import { ReactComponent as OpenMetadataIcon } from '../assets/svg/logo-monogram.svg'; +import { ReactComponent as MessagingIcon } from '../assets/svg/messaging-colored.svg'; +import { ReactComponent as MlModelIcon } from '../assets/svg/ml-model-colored.svg'; +import { ReactComponent as OMHealthIcon } from '../assets/svg/om-health-colored.svg'; +import { ReactComponent as PersonasIcon } from '../assets/svg/persona-colored.svg'; +import { ReactComponent as PipelineIcon } from '../assets/svg/pipeline-colored.svg'; +import { ReactComponent as PoliciesIcon } from '../assets/svg/policies-colored.svg'; +import { ReactComponent as ProfilerConfigIcon } from '../assets/svg/profiler-configuration-logo.svg'; +import { ReactComponent as RolesIcon } from '../assets/svg/role-colored.svg'; +import { ReactComponent as SearchIcon } from '../assets/svg/search-colored.svg'; +import { ReactComponent as AccessControlIcon } from '../assets/svg/setting-access-control.svg'; +import { ReactComponent as CustomProperties } from '../assets/svg/setting-custom-properties.svg'; +import { ReactComponent as ManagementIcon } from '../assets/svg/setting-management.svg'; +import { ReactComponent as NotificationIcon } from '../assets/svg/setting-notification.svg'; +import { ReactComponent as ServiceIcon } from '../assets/svg/setting-services.svg'; +import { ReactComponent as StorageIcon } from '../assets/svg/storage-colored.svg'; +import { ReactComponent as StoredProcedureIcon } from '../assets/svg/stored-procedure-colored.svg'; +import { ReactComponent as TableIcon } from '../assets/svg/table-colored.svg'; +import { ReactComponent as TeamsIcon } from '../assets/svg/teams-colored.svg'; +import { ReactComponent as UsersIcon } from '../assets/svg/user-colored.svg'; +import { + GlobalSettingOptions, + GlobalSettingsMenuCategory, +} from '../constants/GlobalSettings.constants'; +import { + ResourceEntity, + UIPermission, +} from '../context/PermissionProvider/PermissionProvider.interface'; +import { userPermissions } from '../utils/PermissionsUtils'; + +class GlobalSettingsClassBase { + settingCategories: Record = { + [GlobalSettingsMenuCategory.SERVICES]: { + name: t('label.service-plural'), + url: GlobalSettingsMenuCategory.SERVICES, + }, + [GlobalSettingsMenuCategory.NOTIFICATIONS]: { + name: t('label.notification-plural'), + url: GlobalSettingsMenuCategory.NOTIFICATIONS, + }, + [GlobalSettingsMenuCategory.MEMBERS]: { + name: t('label.member-plural'), + url: GlobalSettingsMenuCategory.MEMBERS, + }, + [GlobalSettingsMenuCategory.ACCESS]: { + name: t('label.access-control'), + url: GlobalSettingsMenuCategory.ACCESS, + }, + [GlobalSettingsMenuCategory.PREFERENCES]: { + name: t('label.preference-plural'), + url: GlobalSettingsMenuCategory.PREFERENCES, + }, + [GlobalSettingsMenuCategory.CUSTOM_PROPERTIES]: { + name: t('label.custom-property-plural'), + url: GlobalSettingsMenuCategory.CUSTOM_PROPERTIES, + }, + [GlobalSettingsMenuCategory.BOTS]: { + name: t('label.bot-plural'), + url: GlobalSettingsMenuCategory.BOTS, + }, + [GlobalSettingsMenuCategory.APPLICATIONS]: { + name: t('label.application-plural'), + url: GlobalSettingsMenuCategory.APPLICATIONS, + }, + }; + + protected updateSettingCategories( + categories: Record + ) { + this.settingCategories = categories; + } + + /** + * getSidebarItems + */ + public getGlobalSettingsMenuWithPermission( + permissions: UIPermission, + isAdminUser?: boolean + ): Array { + return [ + { + category: t('label.service-plural'), + key: GlobalSettingsMenuCategory.SERVICES, + icon: ServiceIcon, + description: t('message.service-description'), + items: [ + { + label: t('label.database-plural'), + description: t('message.page-sub-header-for-databases'), + isProtected: userPermissions.hasViewPermissions( + ResourceEntity.DATABASE_SERVICE, + permissions + ), + key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.DATABASES}`, + icon: DatabaseIcon, + }, + { + label: t('label.messaging'), + description: t('message.page-sub-header-for-messagings'), + isProtected: userPermissions.hasViewPermissions( + ResourceEntity.MESSAGING_SERVICE, + permissions + ), + key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.MESSAGING}`, + icon: MessagingIcon, + }, + { + label: t('label.dashboard-plural'), + description: t('message.page-sub-header-for-dashboards'), + isProtected: userPermissions.hasViewPermissions( + ResourceEntity.DASHBOARD_SERVICE, + permissions + ), + key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.DASHBOARDS}`, + icon: DashboardIcon, + }, + { + label: t('label.pipeline-plural'), + description: t('message.page-sub-header-for-pipelines'), + isProtected: userPermissions.hasViewPermissions( + ResourceEntity.PIPELINE_SERVICE, + permissions + ), + key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.PIPELINES}`, + icon: PipelineIcon, + }, + { + label: t('label.ml-model-plural'), + description: t('message.page-sub-header-for-ml-models'), + isProtected: userPermissions.hasViewPermissions( + ResourceEntity.ML_MODEL_SERVICE, + permissions + ), + key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.MLMODELS}`, + icon: MlModelIcon, + }, + { + label: t('label.storage-plural'), + description: t('message.page-sub-header-for-storages'), + isProtected: userPermissions.hasViewPermissions( + ResourceEntity.STORAGE_SERVICE, + permissions + ), + key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.STORAGES}`, + icon: StorageIcon, + }, + { + label: t('label.search'), + description: t('message.page-sub-header-for-search'), + isProtected: userPermissions.hasViewPermissions( + ResourceEntity.SEARCH_SERVICE, + permissions + ), + key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.SEARCH}`, + icon: SearchIcon, + }, + { + label: t('label.metadata'), + description: t('message.page-sub-header-for-metadata'), + isProtected: userPermissions.hasViewPermissions( + ResourceEntity.METADATA_SERVICE, + permissions + ), + key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.METADATA}`, + icon: OpenMetadataIcon, + }, + ], + }, + { + category: t('label.application-plural'), + isProtected: Boolean(isAdminUser), + key: GlobalSettingOptions.APPLICATIONS, + icon: ApplicationIcon, + description: t('message.application-to-improve-data'), + }, + { + category: t('label.notification-plural'), + key: GlobalSettingsMenuCategory.NOTIFICATIONS, + icon: NotificationIcon, + description: t('message.notification-description'), + isProtected: Boolean(isAdminUser), + }, + { + category: t('label.team-user-management'), + key: GlobalSettingsMenuCategory.MEMBERS, + icon: ManagementIcon, + description: t('message.member-description'), + items: [ + { + label: t('label.team-plural'), + description: t('message.page-sub-header-for-teams'), + isProtected: userPermissions.hasViewPermissions( + ResourceEntity.TEAM, + permissions + ), + key: `${GlobalSettingsMenuCategory.MEMBERS}.${GlobalSettingOptions.TEAMS}`, + icon: TeamsIcon, + }, + { + label: t('label.user-plural'), + description: t('message.page-sub-header-for-users'), + isProtected: userPermissions.hasViewPermissions( + ResourceEntity.USER, + permissions + ), + key: `${GlobalSettingsMenuCategory.MEMBERS}.${GlobalSettingOptions.USERS}`, + icon: UsersIcon, + }, + { + label: t('label.admin-plural'), + description: t('message.page-sub-header-for-admins'), + isProtected: userPermissions.hasViewPermissions( + ResourceEntity.USER, + permissions + ), + key: `${GlobalSettingsMenuCategory.MEMBERS}.${GlobalSettingOptions.ADMINS}`, + icon: AdminIcon, + }, + + { + label: t('label.persona-plural'), + description: t('message.page-sub-header-for-persona'), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.MEMBERS}.${GlobalSettingOptions.PERSONA}`, + icon: PersonasIcon, + }, + ], + }, + { + category: t('label.access-control'), + key: GlobalSettingsMenuCategory.ACCESS, + icon: AccessControlIcon, + description: t('message.access-control-description'), + items: [ + { + label: t('label.role-plural'), + description: t('message.page-sub-header-for-roles'), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.ACCESS}.${GlobalSettingOptions.ROLES}`, + icon: RolesIcon, + }, + { + label: t('label.policy-plural'), + description: t('message.page-sub-header-for-policies'), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.ACCESS}.${GlobalSettingOptions.POLICIES}`, + icon: PoliciesIcon, + }, + ], + }, + { + category: t('label.preference-plural'), + key: GlobalSettingsMenuCategory.PREFERENCES, + icon: OpenMetadataIcon, + description: t('message.customize-open-metadata-description'), + items: [ + { + label: t('label.theme'), + description: t('message.appearance-configuration-message'), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.PREFERENCES}.${GlobalSettingOptions.APPEARANCE}`, + icon: AppearanceIcon, + }, + { + label: t('label.customize-entity', { + entity: t('label.landing-page'), + }), + description: t( + 'message.page-sub-header-for-customize-landing-page' + ), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.PREFERENCES}.${GlobalSettingOptions.CUSTOMIZE_LANDING_PAGE}`, + icon: CustomDashboardLogoIcon, + }, + { + label: t('label.email'), + description: t('message.email-configuration-message'), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.PREFERENCES}.${GlobalSettingOptions.EMAIL}`, + icon: EmailIcon, + }, + { + label: t('label.login-configuration'), + description: t('message.page-sub-header-for-login-configuration'), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.PREFERENCES}.${GlobalSettingOptions.LOGIN_CONFIGURATION}`, + icon: LoginIcon, + }, + { + label: t('label.health-check'), + description: t( + 'message.page-sub-header-for-om-health-configuration' + ), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.PREFERENCES}.${GlobalSettingOptions.OM_HEALTH}`, + icon: OMHealthIcon, + }, + { + label: t('label.profiler-configuration'), + description: t( + 'message.page-sub-header-for-profiler-configuration' + ), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.PREFERENCES}.${GlobalSettingOptions.PROFILER_CONFIGURATION}`, + icon: ProfilerConfigIcon, + }, + ], + }, + { + category: t('label.custom-property-plural'), + key: GlobalSettingsMenuCategory.CUSTOM_PROPERTIES, + icon: CustomProperties, + description: t('message.custom-properties-description'), + items: [ + { + label: t('label.database'), + description: t('message.define-custom-property-for-entity', { + entity: t('label.database'), + }), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.DATABASES}`, + icon: DatabaseIcon, + }, + { + label: t('label.database-schema'), + description: t('message.define-custom-property-for-entity', { + entity: t('label.database-schema'), + }), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.DATABASE_SCHEMA}`, + icon: SchemaIcon, + }, + { + label: t('label.table-plural'), + description: t('message.define-custom-property-for-entity', { + entity: t('label.table-plural'), + }), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.TABLES}`, + icon: TableIcon, + }, + { + label: t('label.stored-procedure-plural'), + description: t('message.define-custom-property-for-entity', { + entity: t('label.stored-procedure-plural'), + }), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.STORED_PROCEDURES}`, + icon: StoredProcedureIcon, + }, + { + label: t('label.dashboard-plural'), + description: t('message.define-custom-property-for-entity', { + entity: t('label.dashboard-plural'), + }), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.DASHBOARDS}`, + icon: DashboardIcon, + }, + { + label: t('label.pipeline-plural'), + description: t('message.define-custom-property-for-entity', { + entity: t('label.pipeline-plural'), + }), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.PIPELINES}`, + icon: PipelineIcon, + }, + { + label: t('label.topic-plural'), + description: t('message.define-custom-property-for-entity', { + entity: t('label.topic-plural'), + }), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.TOPICS}`, + icon: MessagingIcon, + }, + { + label: t('label.container-plural'), + description: t('message.define-custom-property-for-entity', { + entity: t('label.container-plural'), + }), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.CONTAINERS}`, + icon: StorageIcon, + }, + { + label: t('label.ml-model-plural'), + description: t('message.define-custom-property-for-entity', { + entity: t('label.ml-model-plural'), + }), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.MLMODELS}`, + icon: MlModelIcon, + }, + { + label: t('label.search-index-plural'), + description: t('message.define-custom-property-for-entity', { + entity: t('label.search-index-plural'), + }), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.SEARCH_INDEXES}`, + icon: SearchIcon, + }, + { + label: t('label.glossary-term'), + description: t('message.define-custom-property-for-entity', { + entity: t('label.glossary-term'), + }), + isProtected: Boolean(isAdminUser), + key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.GLOSSARY_TERM}`, + icon: GlossaryIcon, + }, + ].sort((a, b) => a.label.localeCompare(b.label)), + }, + { + category: t('label.bot-plural'), + description: t('message.page-sub-header-for-bots'), + isProtected: Boolean(isAdminUser), + key: GlobalSettingOptions.BOTS, + icon: BotIcon, + }, + ]; + } +} + +const globalSettingsClassBase = new GlobalSettingsClassBase(); + +export default globalSettingsClassBase; + +export { GlobalSettingsClassBase }; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx index f0902ffbe5aa..73bb33b77e16 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/GlobalSettingsUtils.tsx @@ -12,48 +12,13 @@ */ import i18next from 'i18next'; -import { ReactComponent as AdminIcon } from '../assets/svg/admin-colored.svg'; -import { ReactComponent as ApplicationIcon } from '../assets/svg/application-colored.svg'; -import { ReactComponent as BotIcon } from '../assets/svg/bot-colored.svg'; -import { ReactComponent as AppearanceIcon } from '../assets/svg/custom-logo-colored.svg'; -import { ReactComponent as CustomDashboardLogoIcon } from '../assets/svg/customize-landing-page-colored.svg'; -import { ReactComponent as DashboardIcon } from '../assets/svg/dashboard-colored.svg'; -import { ReactComponent as DatabaseIcon } from '../assets/svg/database-colored.svg'; -import { ReactComponent as SchemaIcon } from '../assets/svg/database-schema.svg'; -import { ReactComponent as EmailIcon } from '../assets/svg/email-colored.svg'; -import { ReactComponent as GlossaryIcon } from '../assets/svg/glossary-colored.svg'; -import { ReactComponent as LoginIcon } from '../assets/svg/login-colored.svg'; -import { ReactComponent as OpenMetadataIcon } from '../assets/svg/logo-monogram.svg'; -import { ReactComponent as MessagingIcon } from '../assets/svg/messaging-colored.svg'; -import { ReactComponent as MlModelIcon } from '../assets/svg/ml-model-colored.svg'; -import { ReactComponent as OMHealthIcon } from '../assets/svg/om-health-colored.svg'; -import { ReactComponent as PersonasIcon } from '../assets/svg/persona-colored.svg'; -import { ReactComponent as PipelineIcon } from '../assets/svg/pipeline-colored.svg'; -import { ReactComponent as PoliciesIcon } from '../assets/svg/policies-colored.svg'; -import { ReactComponent as ProfilerConfigIcon } from '../assets/svg/profiler-configuration-logo.svg'; -import { ReactComponent as RolesIcon } from '../assets/svg/role-colored.svg'; -import { ReactComponent as SearchIcon } from '../assets/svg/search-colored.svg'; -import { ReactComponent as AccessControlIcon } from '../assets/svg/setting-access-control.svg'; -import { ReactComponent as CustomProperties } from '../assets/svg/setting-custom-properties.svg'; -import { ReactComponent as ManagementIcon } from '../assets/svg/setting-management.svg'; -import { ReactComponent as NotificationIcon } from '../assets/svg/setting-notification.svg'; -import { ReactComponent as ServiceIcon } from '../assets/svg/setting-services.svg'; -import { ReactComponent as StorageIcon } from '../assets/svg/storage-colored.svg'; -import { ReactComponent as StoredProcedureIcon } from '../assets/svg/stored-procedure-colored.svg'; -import { ReactComponent as TableIcon } from '../assets/svg/table-colored.svg'; -import { ReactComponent as TeamsIcon } from '../assets/svg/teams-colored.svg'; -import { ReactComponent as UsersIcon } from '../assets/svg/user-colored.svg'; import { PLACEHOLDER_ROUTE_FQN, ROUTES } from '../constants/constants'; import { GlobalSettingOptions, GlobalSettingsMenuCategory, } from '../constants/GlobalSettings.constants'; -import { - ResourceEntity, - UIPermission, -} from '../context/PermissionProvider/PermissionProvider.interface'; import { EntityType } from '../enums/entity.enum'; -import { userPermissions } from '../utils/PermissionsUtils'; +import globalSettingsClassBase from './GlobalSettingsClassBase'; import { getSettingPath } from './RouterUtils'; import { getEncodedFqn } from './StringsUtils'; @@ -68,358 +33,6 @@ export interface SettingMenuItem { items?: SettingMenuItem[]; } -export const getGlobalSettingsMenuWithPermission = ( - permissions: UIPermission, - isAdminUser: boolean | undefined -): SettingMenuItem[] => { - return [ - { - category: i18next.t('label.service-plural'), - key: GlobalSettingsMenuCategory.SERVICES, - icon: ServiceIcon, - description: i18next.t('message.service-description'), - items: [ - { - label: i18next.t('label.database-plural'), - description: i18next.t('message.page-sub-header-for-databases'), - isProtected: userPermissions.hasViewPermissions( - ResourceEntity.DATABASE_SERVICE, - permissions - ), - key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.DATABASES}`, - icon: DatabaseIcon, - }, - { - label: i18next.t('label.messaging'), - description: i18next.t('message.page-sub-header-for-messagings'), - isProtected: userPermissions.hasViewPermissions( - ResourceEntity.MESSAGING_SERVICE, - permissions - ), - key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.MESSAGING}`, - icon: MessagingIcon, - }, - { - label: i18next.t('label.dashboard-plural'), - description: i18next.t('message.page-sub-header-for-dashboards'), - isProtected: userPermissions.hasViewPermissions( - ResourceEntity.DASHBOARD_SERVICE, - permissions - ), - key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.DASHBOARDS}`, - icon: DashboardIcon, - }, - { - label: i18next.t('label.pipeline-plural'), - description: i18next.t('message.page-sub-header-for-pipelines'), - isProtected: userPermissions.hasViewPermissions( - ResourceEntity.PIPELINE_SERVICE, - permissions - ), - key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.PIPELINES}`, - icon: PipelineIcon, - }, - { - label: i18next.t('label.ml-model-plural'), - description: i18next.t('message.page-sub-header-for-ml-models'), - isProtected: userPermissions.hasViewPermissions( - ResourceEntity.ML_MODEL_SERVICE, - permissions - ), - key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.MLMODELS}`, - icon: MlModelIcon, - }, - { - label: i18next.t('label.storage-plural'), - description: i18next.t('message.page-sub-header-for-storages'), - isProtected: userPermissions.hasViewPermissions( - ResourceEntity.STORAGE_SERVICE, - permissions - ), - key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.STORAGES}`, - icon: StorageIcon, - }, - { - label: i18next.t('label.search'), - description: i18next.t('message.page-sub-header-for-search'), - isProtected: userPermissions.hasViewPermissions( - ResourceEntity.SEARCH_SERVICE, - permissions - ), - key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.SEARCH}`, - icon: SearchIcon, - }, - { - label: i18next.t('label.metadata'), - description: i18next.t('message.page-sub-header-for-metadata'), - isProtected: userPermissions.hasViewPermissions( - ResourceEntity.METADATA_SERVICE, - permissions - ), - key: `${GlobalSettingsMenuCategory.SERVICES}.${GlobalSettingOptions.METADATA}`, - icon: OpenMetadataIcon, - }, - ], - }, - { - category: i18next.t('label.application-plural'), - isProtected: Boolean(isAdminUser), - key: GlobalSettingOptions.APPLICATIONS, - icon: ApplicationIcon, - description: i18next.t('message.application-to-improve-data'), - }, - { - category: i18next.t('label.notification-plural'), - key: GlobalSettingsMenuCategory.NOTIFICATIONS, - icon: NotificationIcon, - description: i18next.t('message.notification-description'), - isProtected: Boolean(isAdminUser), - }, - { - category: i18next.t('label.team-user-management'), - key: GlobalSettingsMenuCategory.MEMBERS, - icon: ManagementIcon, - description: i18next.t('message.member-description'), - items: [ - { - label: i18next.t('label.team-plural'), - description: i18next.t('message.page-sub-header-for-teams'), - isProtected: userPermissions.hasViewPermissions( - ResourceEntity.TEAM, - permissions - ), - key: `${GlobalSettingsMenuCategory.MEMBERS}.${GlobalSettingOptions.TEAMS}`, - icon: TeamsIcon, - }, - { - label: i18next.t('label.user-plural'), - description: i18next.t('message.page-sub-header-for-users'), - isProtected: userPermissions.hasViewPermissions( - ResourceEntity.USER, - permissions - ), - key: `${GlobalSettingsMenuCategory.MEMBERS}.${GlobalSettingOptions.USERS}`, - icon: UsersIcon, - }, - { - label: i18next.t('label.admin-plural'), - description: i18next.t('message.page-sub-header-for-admins'), - isProtected: userPermissions.hasViewPermissions( - ResourceEntity.USER, - permissions - ), - key: `${GlobalSettingsMenuCategory.MEMBERS}.${GlobalSettingOptions.ADMINS}`, - icon: AdminIcon, - }, - - { - label: i18next.t('label.persona-plural'), - description: i18next.t('message.page-sub-header-for-persona'), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.MEMBERS}.${GlobalSettingOptions.PERSONA}`, - icon: PersonasIcon, - }, - ], - }, - { - category: i18next.t('label.access-control'), - key: GlobalSettingsMenuCategory.ACCESS, - icon: AccessControlIcon, - description: i18next.t('message.access-control-description'), - items: [ - { - label: i18next.t('label.role-plural'), - description: i18next.t('message.page-sub-header-for-roles'), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.ACCESS}.${GlobalSettingOptions.ROLES}`, - icon: RolesIcon, - }, - { - label: i18next.t('label.policy-plural'), - description: i18next.t('message.page-sub-header-for-policies'), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.ACCESS}.${GlobalSettingOptions.POLICIES}`, - icon: PoliciesIcon, - }, - ], - }, - { - category: i18next.t('label.preference-plural'), - key: GlobalSettingsMenuCategory.PREFERENCES, - icon: OpenMetadataIcon, - description: i18next.t('message.customize-open-metadata-description'), - items: [ - { - label: i18next.t('label.theme'), - description: i18next.t('message.appearance-configuration-message'), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.PREFERENCES}.${GlobalSettingOptions.APPEARANCE}`, - icon: AppearanceIcon, - }, - { - label: i18next.t('label.customize-entity', { - entity: i18next.t('label.landing-page'), - }), - description: i18next.t( - 'message.page-sub-header-for-customize-landing-page' - ), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.PREFERENCES}.${GlobalSettingOptions.CUSTOMIZE_LANDING_PAGE}`, - icon: CustomDashboardLogoIcon, - }, - { - label: i18next.t('label.email'), - description: i18next.t('message.email-configuration-message'), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.PREFERENCES}.${GlobalSettingOptions.EMAIL}`, - icon: EmailIcon, - }, - { - label: i18next.t('label.login-configuration'), - description: i18next.t( - 'message.page-sub-header-for-login-configuration' - ), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.PREFERENCES}.${GlobalSettingOptions.LOGIN_CONFIGURATION}`, - icon: LoginIcon, - }, - { - label: i18next.t('label.health-check'), - description: i18next.t( - 'message.page-sub-header-for-om-health-configuration' - ), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.PREFERENCES}.${GlobalSettingOptions.OM_HEALTH}`, - icon: OMHealthIcon, - }, - { - label: i18next.t('label.profiler-configuration'), - description: i18next.t( - 'message.page-sub-header-for-profiler-configuration' - ), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.PREFERENCES}.${GlobalSettingOptions.PROFILER_CONFIGURATION}`, - icon: ProfilerConfigIcon, - }, - ], - }, - { - category: i18next.t('label.custom-property-plural'), - key: GlobalSettingsMenuCategory.CUSTOM_PROPERTIES, - icon: CustomProperties, - description: i18next.t('message.custom-properties-description'), - items: [ - { - label: i18next.t('label.database'), - description: i18next.t('message.define-custom-property-for-entity', { - entity: i18next.t('label.database'), - }), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.DATABASES}`, - icon: DatabaseIcon, - }, - { - label: i18next.t('label.database-schema'), - description: i18next.t('message.define-custom-property-for-entity', { - entity: i18next.t('label.database-schema'), - }), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.DATABASE_SCHEMA}`, - icon: SchemaIcon, - }, - { - label: i18next.t('label.table-plural'), - description: i18next.t('message.define-custom-property-for-entity', { - entity: i18next.t('label.table-plural'), - }), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.TABLES}`, - icon: TableIcon, - }, - { - label: i18next.t('label.stored-procedure-plural'), - description: i18next.t('message.define-custom-property-for-entity', { - entity: i18next.t('label.stored-procedure-plural'), - }), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.STORED_PROCEDURES}`, - icon: StoredProcedureIcon, - }, - { - label: i18next.t('label.dashboard-plural'), - description: i18next.t('message.define-custom-property-for-entity', { - entity: i18next.t('label.dashboard-plural'), - }), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.DASHBOARDS}`, - icon: DashboardIcon, - }, - { - label: i18next.t('label.pipeline-plural'), - description: i18next.t('message.define-custom-property-for-entity', { - entity: i18next.t('label.pipeline-plural'), - }), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.PIPELINES}`, - icon: PipelineIcon, - }, - { - label: i18next.t('label.topic-plural'), - description: i18next.t('message.define-custom-property-for-entity', { - entity: i18next.t('label.topic-plural'), - }), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.TOPICS}`, - icon: MessagingIcon, - }, - { - label: i18next.t('label.container-plural'), - description: i18next.t('message.define-custom-property-for-entity', { - entity: i18next.t('label.container-plural'), - }), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.CONTAINERS}`, - icon: StorageIcon, - }, - { - label: i18next.t('label.ml-model-plural'), - description: i18next.t('message.define-custom-property-for-entity', { - entity: i18next.t('label.ml-model-plural'), - }), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.MLMODELS}`, - icon: MlModelIcon, - }, - { - label: i18next.t('label.search-index-plural'), - description: i18next.t('message.define-custom-property-for-entity', { - entity: i18next.t('label.search-index-plural'), - }), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.SEARCH_INDEXES}`, - icon: SearchIcon, - }, - { - label: i18next.t('label.glossary-term'), - description: i18next.t('message.define-custom-property-for-entity', { - entity: i18next.t('label.glossary-term'), - }), - isProtected: Boolean(isAdminUser), - key: `${GlobalSettingsMenuCategory.CUSTOM_PROPERTIES}.${GlobalSettingOptions.GLOSSARY_TERM}`, - icon: GlossaryIcon, - }, - ].sort((a, b) => a.label.localeCompare(b.label)), - }, - { - category: i18next.t('label.bot-plural'), - description: i18next.t('message.page-sub-header-for-bots'), - isProtected: Boolean(isAdminUser), - key: GlobalSettingOptions.BOTS, - icon: BotIcon, - }, - ]; -}; - export const getGlobalSettingMenuItem = ( args: SettingMenuItem ): SettingMenuItem => { @@ -462,46 +75,11 @@ export const getCustomizePagePath = (personaFqn: string, pageFqn: string) => { .replace(':pageFqn', pageFqn); }; -export const settingCategories = { - [GlobalSettingsMenuCategory.SERVICES]: { - name: i18next.t('label.service-plural'), - url: GlobalSettingsMenuCategory.SERVICES, - }, - [GlobalSettingsMenuCategory.NOTIFICATIONS]: { - name: i18next.t('label.notification-plural'), - url: GlobalSettingsMenuCategory.NOTIFICATIONS, - }, - [GlobalSettingsMenuCategory.MEMBERS]: { - name: i18next.t('label.member-plural'), - url: GlobalSettingsMenuCategory.MEMBERS, - }, - [GlobalSettingsMenuCategory.ACCESS]: { - name: i18next.t('label.access-control'), - url: GlobalSettingsMenuCategory.ACCESS, - }, - [GlobalSettingsMenuCategory.PREFERENCES]: { - name: i18next.t('label.preference-plural'), - url: GlobalSettingsMenuCategory.PREFERENCES, - }, - [GlobalSettingsMenuCategory.CUSTOM_PROPERTIES]: { - name: i18next.t('label.custom-property-plural'), - url: GlobalSettingsMenuCategory.CUSTOM_PROPERTIES, - }, - [GlobalSettingsMenuCategory.BOTS]: { - name: i18next.t('label.bot-plural'), - url: GlobalSettingsMenuCategory.BOTS, - }, - [GlobalSettingsMenuCategory.APPLICATIONS]: { - name: i18next.t('label.application-plural'), - url: GlobalSettingsMenuCategory.APPLICATIONS, - }, -}; - export const getSettingPageEntityBreadCrumb = ( category: GlobalSettingsMenuCategory, entityName?: string ) => { - const categoryObject = settingCategories[category]; + const categoryObject = globalSettingsClassBase.settingCategories[category]; return [ { @@ -509,7 +87,7 @@ export const getSettingPageEntityBreadCrumb = ( url: ROUTES.SETTINGS, }, { - name: categoryObject.name, + name: categoryObject?.name ?? '', url: entityName ? getSettingPath(categoryObject.url) : '', activeTitle: !entityName, }, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ScheduleUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ScheduleUtils.test.ts new file mode 100644 index 000000000000..a81960a2de38 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ScheduleUtils.test.ts @@ -0,0 +1,57 @@ +/* + * 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 { getScheduleOptionsFromSchedules } from './ScheduleUtils'; + +describe('getScheduleOptionsFromSchedules', () => { + it('should return an empty array when input is an empty array', () => { + const scheduleOptions: string[] = []; + const result = getScheduleOptionsFromSchedules(scheduleOptions); + + expect(result).toEqual([]); + }); + + it('should map "run_once" to an empty string', () => { + const scheduleOptions: string[] = ['run_once']; + const result = getScheduleOptionsFromSchedules(scheduleOptions); + + expect(result).toEqual(['']); + }); + + it('should map "daily" to "day"', () => { + const scheduleOptions: string[] = ['daily']; + const result = getScheduleOptionsFromSchedules(scheduleOptions); + + expect(result).toEqual(['day']); + }); + + it('should map "weekly" to "week"', () => { + const scheduleOptions: string[] = ['weekly']; + const result = getScheduleOptionsFromSchedules(scheduleOptions); + + expect(result).toEqual(['week']); + }); + + it('should map "monthly" to "month"', () => { + const scheduleOptions: string[] = ['monthly']; + const result = getScheduleOptionsFromSchedules(scheduleOptions); + + expect(result).toEqual(['month']); + }); + + it('should map unknown options to an empty string', () => { + const scheduleOptions: string[] = ['unknown', 'invalid']; + const result = getScheduleOptionsFromSchedules(scheduleOptions); + + expect(result).toEqual(['', '']); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ScheduleUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ScheduleUtils.ts new file mode 100644 index 000000000000..a3fa6fc16f6a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ScheduleUtils.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ +export const getScheduleOptionsFromSchedules = ( + scheduleOptions: string[] +): string[] => { + return scheduleOptions.map((scheduleOption) => { + switch (scheduleOption) { + case 'run_once': + return ''; + case 'daily': + return 'day'; + case 'weekly': + return 'week'; + case 'monthly': + return 'month'; + } + + return ''; + }); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx index d351315d9ee0..9e3d2c4ed91f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/TableUtils.tsx @@ -28,8 +28,10 @@ import { } from 'lodash'; import { EntityTags } from 'Models'; import React, { CSSProperties } from 'react'; +import { ReactComponent as AlertIcon } from '../assets/svg/alert.svg'; import { ReactComponent as AnnouncementIcon } from '../assets/svg/announcements-black.svg'; import { ReactComponent as ApplicationIcon } from '../assets/svg/application.svg'; +import { ReactComponent as AutomatorBotIcon } from '../assets/svg/automator-bot.svg'; import { ReactComponent as GlossaryTermIcon } from '../assets/svg/book.svg'; import { ReactComponent as BotIcon } from '../assets/svg/bot.svg'; import { ReactComponent as ChartIcon } from '../assets/svg/chart.svg'; @@ -40,11 +42,11 @@ import { ReactComponent as IconDrag } from '../assets/svg/drag.svg'; import { ReactComponent as IconForeignKeyLineThrough } from '../assets/svg/foreign-key-line-through.svg'; import { ReactComponent as IconForeignKey } from '../assets/svg/foreign-key.svg'; import { ReactComponent as GlossaryIcon } from '../assets/svg/glossary.svg'; -import { ReactComponent as AlertIcon } from '../assets/svg/ic-alert.svg'; import { ReactComponent as IconDown } from '../assets/svg/ic-arrow-down.svg'; import { ReactComponent as IconRight } from '../assets/svg/ic-arrow-right.svg'; import { ReactComponent as IconTestSuite } from '../assets/svg/ic-checklist.svg'; import { ReactComponent as DashboardIcon } from '../assets/svg/ic-dashboard.svg'; +import { ReactComponent as DataQualityIcon } from '../assets/svg/ic-data-contract.svg'; import { ReactComponent as DataProductIcon } from '../assets/svg/ic-data-product.svg'; import { ReactComponent as DatabaseIcon } from '../assets/svg/ic-database.svg'; import { ReactComponent as DomainIcon } from '../assets/svg/ic-domain.svg'; @@ -63,10 +65,13 @@ import { ReactComponent as IconNotNull } from '../assets/svg/icon-not-null.svg'; import { ReactComponent as RoleIcon } from '../assets/svg/icon-role-grey.svg'; import { ReactComponent as IconUniqueLineThrough } from '../assets/svg/icon-unique-line-through.svg'; import { ReactComponent as IconUnique } from '../assets/svg/icon-unique.svg'; +import { ReactComponent as NotificationIcon } from '../assets/svg/notification.svg'; import { ReactComponent as PolicyIcon } from '../assets/svg/policies.svg'; +import { ReactComponent as ServicesIcon } from '../assets/svg/services.svg'; import { ReactComponent as TagIcon } from '../assets/svg/tag.svg'; import { ReactComponent as TaskIcon } from '../assets/svg/task-ic.svg'; import { ReactComponent as TeamIcon } from '../assets/svg/teams.svg'; +import { ReactComponent as UserIcon } from '../assets/svg/user.svg'; import { SourceType } from '../components/SearchedData/SearchedData.interface'; import { NON_SERVICE_TYPE_ASSETS } from '../constants/Assets.constants'; @@ -210,97 +215,73 @@ export const getEntityIcon = ( let Icon; let className = iconClass; const style: CSSProperties = iconStyle; + const entityIconMapping: Record = { + [SearchIndex.DATABASE]: DatabaseIcon, + [EntityType.DATABASE]: DatabaseIcon, + [SearchIndex.DATABASE_SCHEMA]: SchemaIcon, + [EntityType.DATABASE_SCHEMA]: SchemaIcon, + [SearchIndex.TOPIC]: TopicIcon, + [EntityType.TOPIC]: TopicIcon, + [EntityType.MESSAGING_SERVICE]: TopicIcon, + [SearchIndex.MESSAGING_SERVICE]: TopicIcon, + [SearchIndex.DASHBOARD]: DashboardIcon, + [EntityType.DASHBOARD]: DashboardIcon, + [EntityType.DASHBOARD_SERVICE]: DashboardIcon, + [SearchIndex.DASHBOARD_SERVICE]: DashboardIcon, + [SearchIndex.MLMODEL]: MlModelIcon, + [EntityType.MLMODEL]: MlModelIcon, + [EntityType.MLMODEL_SERVICE]: MlModelIcon, + [SearchIndex.ML_MODEL_SERVICE]: MlModelIcon, + [SearchIndex.PIPELINE]: PipelineIcon, + [EntityType.PIPELINE]: PipelineIcon, + [EntityType.PIPELINE_SERVICE]: PipelineIcon, + [SearchIndex.PIPELINE_SERVICE]: PipelineIcon, + [SearchIndex.CONTAINER]: ContainerIcon, + [EntityType.CONTAINER]: ContainerIcon, + [EntityType.STORAGE_SERVICE]: ContainerIcon, + [SearchIndex.STORAGE_SERVICE]: ContainerIcon, + [SearchIndex.DASHBOARD_DATA_MODEL]: IconDataModel, + [EntityType.DASHBOARD_DATA_MODEL]: IconDataModel, + [SearchIndex.STORED_PROCEDURE]: IconStoredProcedure, + [EntityType.STORED_PROCEDURE]: IconStoredProcedure, + [EntityType.CLASSIFICATION]: ClassificationIcon, + [SearchIndex.TAG]: TagIcon, + [EntityType.TAG]: TagIcon, + [SearchIndex.GLOSSARY]: GlossaryIcon, + [EntityType.GLOSSARY]: GlossaryIcon, + [SearchIndex.GLOSSARY_TERM]: GlossaryTermIcon, + [EntityType.GLOSSARY_TERM]: GlossaryTermIcon, + [SearchIndex.DOMAIN]: DomainIcon, + [EntityType.DOMAIN]: DomainIcon, + [SearchIndex.CHART]: ChartIcon, + [EntityType.CHART]: ChartIcon, + [SearchIndex.TABLE]: TableIcon, + [EntityType.TABLE]: TableIcon, + [SearchIndex.DATA_PRODUCT]: DataProductIcon, + [EntityType.DATA_PRODUCT]: DataProductIcon, + [EntityType.TEST_CASE]: IconTestSuite, + [EntityType.TEST_SUITE]: IconTestSuite, + [EntityType.BOT]: BotIcon, + [EntityType.TEAM]: TeamIcon, + [EntityType.APPLICATION]: ApplicationIcon, + [EntityType.PERSONA]: PersonaIcon, + [EntityType.ROLE]: RoleIcon, + [EntityType.POLICY]: PolicyIcon, + [EntityType.EVENT_SUBSCRIPTION]: AlertIcon, + ['tagCategory']: ClassificationIcon, + ['ingestionPipeline']: PipelineIcon, + ['alert']: AlertIcon, + ['announcement']: AnnouncementIcon, + ['conversation']: ConversationIcon, + ['task']: TaskIcon, + ['dataQuality']: DataQualityIcon, + ['services']: ServicesIcon, + ['automator']: AutomatorBotIcon, + ['user']: UserIcon, + ['notification']: NotificationIcon, + }; switch (indexType) { - case SearchIndex.DATABASE: - case EntityType.DATABASE: - Icon = DatabaseIcon; - - break; - - case SearchIndex.DATABASE_SCHEMA: - case EntityType.DATABASE_SCHEMA: - Icon = SchemaIcon; - - break; - - case SearchIndex.TOPIC: - case EntityType.TOPIC: - case EntityType.MESSAGING_SERVICE: - case SearchIndex.MESSAGING_SERVICE: - Icon = TopicIcon; - - break; - - case SearchIndex.DASHBOARD: - case EntityType.DASHBOARD: - case EntityType.DASHBOARD_SERVICE: - case SearchIndex.DASHBOARD_SERVICE: - Icon = DashboardIcon; - - break; - - case SearchIndex.MLMODEL: - case EntityType.MLMODEL: - case EntityType.MLMODEL_SERVICE: - case SearchIndex.ML_MODEL_SERVICE: - Icon = MlModelIcon; - - break; - - case SearchIndex.PIPELINE: - case EntityType.PIPELINE: - case EntityType.PIPELINE_SERVICE: - case SearchIndex.PIPELINE_SERVICE: - case 'ingestionPipeline': - Icon = PipelineIcon; - - break; - - case SearchIndex.CONTAINER: - case EntityType.CONTAINER: - case EntityType.STORAGE_SERVICE: - case SearchIndex.STORAGE_SERVICE: - Icon = ContainerIcon; - - break; - - case SearchIndex.DASHBOARD_DATA_MODEL: - case EntityType.DASHBOARD_DATA_MODEL: - Icon = IconDataModel; - - break; - - case SearchIndex.STORED_PROCEDURE: - case EntityType.STORED_PROCEDURE: - Icon = IconStoredProcedure; - - break; - - case EntityType.CLASSIFICATION: - case 'tagCategory': - Icon = ClassificationIcon; - - break; - - case SearchIndex.TAG: - case EntityType.TAG: - Icon = TagIcon; - - break; - - case SearchIndex.GLOSSARY: - case EntityType.GLOSSARY: - Icon = GlossaryIcon; - - break; - - case EntityType.GLOSSARY_TERM: - case SearchIndex.GLOSSARY_TERM: - Icon = GlossaryTermIcon; - - break; - case EntityType.SEARCH_INDEX: case SearchIndex.SEARCH_INDEX: case EntityType.SEARCH_SERVICE: @@ -310,89 +291,13 @@ export const getEntityIcon = ( break; - case EntityType.DOMAIN: - case SearchIndex.DOMAIN: - Icon = DomainIcon; - - break; - - case EntityType.CHART: - case SearchIndex.CHART: - Icon = ChartIcon; - - break; - - case EntityType.DATA_PRODUCT: - case SearchIndex.DATA_PRODUCT: - Icon = DataProductIcon; - - break; - - case 'announcement': - Icon = AnnouncementIcon; - - break; - - case 'conversation': - Icon = ConversationIcon; - - break; - - case 'task': - Icon = TaskIcon; - - break; - - case EntityType.EVENT_SUBSCRIPTION: - Icon = AlertIcon; - - break; - - case EntityType.TEST_CASE: - case EntityType.TEST_SUITE: - Icon = IconTestSuite; - - break; - - case EntityType.BOT: - Icon = BotIcon; - - break; - - case EntityType.TEAM: - Icon = TeamIcon; - - break; - - case EntityType.APPLICATION: - Icon = ApplicationIcon; - - break; - - case EntityType.PERSONA: - Icon = PersonaIcon; - - break; - - case EntityType.ROLE: - Icon = RoleIcon; - - break; - - case EntityType.POLICY: - Icon = PolicyIcon; - - break; - - case SearchIndex.TABLE: - case EntityType.TABLE: default: - Icon = TableIcon; + Icon = entityIconMapping[indexType]; break; } - return ; + return Icon ? : <>; }; export const getServiceIcon = (source: SourceType) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/Users.util.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/Users.util.tsx index b842a3aaf3eb..73aef1d11e4c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/Users.util.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/Users.util.tsx @@ -13,13 +13,17 @@ import { Popover, Skeleton, Space, Tag } from 'antd'; import { ColumnsType } from 'antd/lib/table'; +import { AxiosError } from 'axios'; import { t } from 'i18next'; import { isEmpty, isUndefined, uniqueId } from 'lodash'; import React from 'react'; import { Link } from 'react-router-dom'; import UserPopOverCard from '../components/common/PopOverCard/UserPopOverCard'; +import { HTTP_STATUS_CODE } from '../constants/Auth.constants'; +import { ERROR_MESSAGE } from '../constants/constants'; import { MASKED_EMAIL } from '../constants/User.constants'; import { EntityReference, User } from '../generated/entity/teams/user'; +import { getIsErrorMatch } from './CommonUtils'; import { getEntityName } from './EntityUtils'; import { LIST_CAP } from './PermissionsUtils'; import { getRoleWithFqnPath, getTeamsWithFqnPath } from './RouterUtils'; @@ -150,3 +154,30 @@ export const commonUserDetailColumns = ( export const isMaskedEmail = (email: string) => { return email === MASKED_EMAIL; }; + +export const getUserCreationErrorMessage = ({ + error, + entity, + entityLowercase, + entityName, +}: { + error?: AxiosError; + entity: string; + entityLowercase?: string; + entityName?: string; +}) => { + if (error) { + if (getIsErrorMatch(error, ERROR_MESSAGE.alreadyExist)) { + return t('server.email-already-exist', { + entity: entityLowercase ?? '', + name: entityName ?? '', + }); + } + + if (error.response?.status === HTTP_STATUS_CODE.LIMIT_REACHED) { + return t('server.entity-limit-reached', { entity }); + } + } + + return t('server.create-entity-error', { entity }); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.tsx index 50f64a147969..48ba0d42acee 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/formUtils.tsx @@ -22,7 +22,9 @@ import { TooltipProps, } from 'antd'; import { TooltipPlacement } from 'antd/lib/tooltip'; +import { AxiosError } from 'axios'; import classNames from 'classnames'; +import { t } from 'i18next'; import { compact, startCase } from 'lodash'; import React, { Fragment, ReactNode } from 'react'; import AsyncSelectList from '../components/common/AsyncSelectList/AsyncSelectList'; @@ -31,6 +33,7 @@ import ColorPicker from '../components/common/ColorPicker/ColorPicker.component' import FilterPattern from '../components/common/FilterPattern/FilterPattern'; import { FilterPatternProps } from '../components/common/FilterPattern/filterPattern.interface'; import FormItemLabel from '../components/common/Form/FormItemLabel'; +import { InlineAlertProps } from '../components/common/InlineAlert/InlineAlert.interface'; import RichTextEditor from '../components/common/RichTextEditor/RichTextEditor'; import { RichTextEditorProp } from '../components/common/RichTextEditor/RichTextEditor.interface'; import SliderWithInput from '../components/common/SliderWithInput/SliderWithInput'; @@ -39,11 +42,13 @@ import { UserSelectableList } from '../components/common/UserSelectableList/User import { UserSelectableListProps } from '../components/common/UserSelectableList/UserSelectableList.interface'; import { UserTeamSelectableList } from '../components/common/UserTeamSelectableList/UserTeamSelectableList.component'; import { UserSelectDropdownProps } from '../components/common/UserTeamSelectableList/UserTeamSelectableList.interface'; +import { HTTP_STATUS_CODE } from '../constants/Auth.constants'; import { FieldProp, FieldTypes } from '../interface/FormUtils.interface'; import TagSuggestion, { TagSuggestionProps, } from '../pages/TasksPage/shared/TagSuggestion'; import i18n from './i18next/LocalUtil'; +import { getErrorText } from './StringsUtils'; export const getField = (field: FieldProp) => { const { @@ -249,3 +254,66 @@ export const transformErrors: ErrorTransformer = (errors) => { return compact(errorRet); }; + +export const handleEntityCreationError = ({ + error, + setInlineAlertDetails, + entity, + entityLowercase, + entityLowercasePlural, + name, + defaultErrorType, +}: { + error: AxiosError; + setInlineAlertDetails: (alertDetails?: InlineAlertProps | undefined) => void; + entity: string; + entityLowercase?: string; + entityLowercasePlural?: string; + name: string; + defaultErrorType?: 'create'; +}) => { + if (error.response?.status === HTTP_STATUS_CODE.CONFLICT) { + setInlineErrorValue( + t('server.entity-already-exist', { + entity, + entityPlural: entityLowercasePlural ?? entity, + name: name, + }), + setInlineAlertDetails + ); + + return; + } + + if (error.response?.status === HTTP_STATUS_CODE.LIMIT_REACHED) { + setInlineErrorValue( + t('server.entity-limit-reached', { + entity, + }), + setInlineAlertDetails + ); + + return; + } + + setInlineErrorValue( + defaultErrorType === 'create' + ? t(`server.entity-creation-error`, { + entity: entityLowercase ?? entity, + }) + : getErrorText(error, t('server.unexpected-error')), + setInlineAlertDetails + ); +}; + +export const setInlineErrorValue = ( + description: string, + setInlineAlertDetails: (alertDetails?: InlineAlertProps | undefined) => void +) => { + setInlineAlertDetails({ + type: 'error', + heading: t('label.error'), + description, + onClose: () => setInlineAlertDetails(undefined), + }); +}; diff --git a/openmetadata-ui/src/main/resources/ui/webpack.config.dev.js b/openmetadata-ui/src/main/resources/ui/webpack.config.dev.js index afa06d147742..b33d25e07849 100644 --- a/openmetadata-ui/src/main/resources/ui/webpack.config.dev.js +++ b/openmetadata-ui/src/main/resources/ui/webpack.config.dev.js @@ -109,6 +109,7 @@ module.exports = { { loader: 'css-loader', // translates CSS into CommonJS }, + 'postcss-loader', { loader: 'less-loader', // compiles Less to CSS options: { diff --git a/openmetadata-ui/src/main/resources/ui/webpack.config.prod.js b/openmetadata-ui/src/main/resources/ui/webpack.config.prod.js index fb084217ed56..8581c3f0524e 100644 --- a/openmetadata-ui/src/main/resources/ui/webpack.config.prod.js +++ b/openmetadata-ui/src/main/resources/ui/webpack.config.prod.js @@ -110,6 +110,7 @@ module.exports = { { loader: 'css-loader', // translates CSS into CommonJS }, + 'postcss-loader', { loader: 'less-loader', // compiles Less to CSS options: { From 54aa9046b56ec0567aa4ac3c975f5b37827e0ee1 Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Mon, 3 Jun 2024 22:00:03 -0700 Subject: [PATCH 041/117] Add token limitations --- .../openmetadata/service/resources/teams/UserResource.java | 4 +++- .../entity/policies/accessControl/resourceDescriptor.json | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java index f76ac84c0723..72687286bfc9 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java @@ -792,11 +792,11 @@ public AuthenticationMechanism getAuthenticationMechanism( @Context SecurityContext securityContext, @Parameter(description = "Id of the user", schema = @Schema(type = "UUID")) @PathParam("id") UUID id) { - User user = repository.get(uriInfo, id, new Fields(Set.of(AUTH_MECHANISM_FIELD))); if (!Boolean.TRUE.equals(user.getIsBot())) { throw new IllegalArgumentException("JWT token is only supported for bot users"); } + limits.enforceLimits(securityContext, getResourceContext(), new OperationContext(entityType, MetadataOperation.GENERATE_TOKEN)); decryptOrNullify(securityContext, user); authorizer.authorizeAdmin(securityContext); return user.getAuthenticationMechanism(); @@ -1212,6 +1212,7 @@ public Response getPersonalAccessToken( @Parameter(description = "User Name of the User for which to get. (Default = `false`)") @QueryParam("username") String userName) { + limits.enforceLimits(securityContext, getResourceContext(), new OperationContext(entityType, MetadataOperation.GENERATE_TOKEN)); if (userName != null) { authorizer.authorizeAdmin(securityContext); } else { @@ -1290,6 +1291,7 @@ public Response createAccessToken( @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreatePersonalToken tokenRequest) { + limits.enforceLimits(securityContext, getResourceContext(), new OperationContext(entityType, MetadataOperation.GENERATE_TOKEN)); String userName = securityContext.getUserPrincipal().getName(); User user = repository.getByName( diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/policies/accessControl/resourceDescriptor.json b/openmetadata-spec/src/main/resources/json/schema/entity/policies/accessControl/resourceDescriptor.json index 48a6daf92d91..cbfd206b4a69 100644 --- a/openmetadata-spec/src/main/resources/json/schema/entity/policies/accessControl/resourceDescriptor.json +++ b/openmetadata-spec/src/main/resources/json/schema/entity/policies/accessControl/resourceDescriptor.json @@ -47,7 +47,8 @@ "DeleteTestCaseFailedRowsSample", "Deploy", "Trigger", - "Kill" + "Kill", + "GenerateToken" ] } }, From 74e127d37746f9b2e5734ab820ec44498e7e0f1a Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Mon, 3 Jun 2024 22:01:15 -0700 Subject: [PATCH 042/117] Add token limitations --- .../service/resources/teams/UserResource.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java index 72687286bfc9..274b295b0de1 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java @@ -796,7 +796,10 @@ public AuthenticationMechanism getAuthenticationMechanism( if (!Boolean.TRUE.equals(user.getIsBot())) { throw new IllegalArgumentException("JWT token is only supported for bot users"); } - limits.enforceLimits(securityContext, getResourceContext(), new OperationContext(entityType, MetadataOperation.GENERATE_TOKEN)); + limits.enforceLimits( + securityContext, + getResourceContext(), + new OperationContext(entityType, MetadataOperation.GENERATE_TOKEN)); decryptOrNullify(securityContext, user); authorizer.authorizeAdmin(securityContext); return user.getAuthenticationMechanism(); @@ -1212,7 +1215,10 @@ public Response getPersonalAccessToken( @Parameter(description = "User Name of the User for which to get. (Default = `false`)") @QueryParam("username") String userName) { - limits.enforceLimits(securityContext, getResourceContext(), new OperationContext(entityType, MetadataOperation.GENERATE_TOKEN)); + limits.enforceLimits( + securityContext, + getResourceContext(), + new OperationContext(entityType, MetadataOperation.GENERATE_TOKEN)); if (userName != null) { authorizer.authorizeAdmin(securityContext); } else { @@ -1291,7 +1297,10 @@ public Response createAccessToken( @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreatePersonalToken tokenRequest) { - limits.enforceLimits(securityContext, getResourceContext(), new OperationContext(entityType, MetadataOperation.GENERATE_TOKEN)); + limits.enforceLimits( + securityContext, + getResourceContext(), + new OperationContext(entityType, MetadataOperation.GENERATE_TOKEN)); String userName = securityContext.getUserPrincipal().getName(); User user = repository.getByName( From 7d54cdafbc93cf3752a172d7cb2b976713395c12 Mon Sep 17 00:00:00 2001 From: Sriharsha Chintalapani Date: Mon, 3 Jun 2024 21:42:40 -0700 Subject: [PATCH 043/117] Add appType as part of schema in ingestion pipeline (#16519) --- bootstrap/sql/migrations/native/1.4.2/mysql/schemaChanges.sql | 1 + bootstrap/sql/migrations/native/1.4.2/postgres/schemaChanges.sql | 1 + 2 files changed, 2 insertions(+) diff --git a/bootstrap/sql/migrations/native/1.4.2/mysql/schemaChanges.sql b/bootstrap/sql/migrations/native/1.4.2/mysql/schemaChanges.sql index e69de29bb2d1..c4ed21e371b5 100644 --- a/bootstrap/sql/migrations/native/1.4.2/mysql/schemaChanges.sql +++ b/bootstrap/sql/migrations/native/1.4.2/mysql/schemaChanges.sql @@ -0,0 +1 @@ +ALTER TABLE ingestion_pipeline_entity ADD COLUMN appType VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.sourceConfig.config.appConfig.type') STORED NULL; \ No newline at end of file diff --git a/bootstrap/sql/migrations/native/1.4.2/postgres/schemaChanges.sql b/bootstrap/sql/migrations/native/1.4.2/postgres/schemaChanges.sql index e69de29bb2d1..c1b1cda7abc2 100644 --- a/bootstrap/sql/migrations/native/1.4.2/postgres/schemaChanges.sql +++ b/bootstrap/sql/migrations/native/1.4.2/postgres/schemaChanges.sql @@ -0,0 +1 @@ +ALTER TABLE ingestion_pipeline_entity ADD COLUMN appType VARCHAR(256) GENERATED ALWAYS AS (json -> 'sourceConfig' -> 'config' -> 'appConfig' ->> 'type') STORED NULL; \ No newline at end of file From c7f4de2ce27e962248bd38a5bee5d2d1a2f787d2 Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Wed, 5 Jun 2024 11:34:24 +0530 Subject: [PATCH 044/117] #16489: fix the redirect issue to new tab for tags and glossary (#16512) * fix the redirect issue to new tab for tags and glossary * fix the redirect on cancel icon and unit test issue * changes as per comments (cherry picked from commit 8d312f0853609cfef260739cf789d459838a3421) --- .../TableDataCardBody.test.tsx | 5 +++ .../SearchIndexSummary.test.tsx | 10 ++++-- .../TableSummary/TableSummary.test.tsx | 10 ++++-- .../Tag/TagsV1/TagsV1.component.tsx | 26 ++++++++------ .../src/components/Tag/TagsV1/TagsV1.test.tsx | 20 +++++------ .../components/TagsInput/TagsInput.test.tsx | 34 +++++++++++++++---- 6 files changed, 74 insertions(+), 31 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDataCardBody/TableDataCardBody.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDataCardBody/TableDataCardBody.test.tsx index 5f9f7229555d..705dfa4c01ff 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDataCardBody/TableDataCardBody.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableDataCardBody/TableDataCardBody.test.tsx @@ -19,6 +19,11 @@ import TableDataCardBody from './TableDataCardBody'; jest.mock('../../common/RichTextEditor/RichTextEditorPreviewer', () => { return jest.fn().mockReturnValue(

RichTextEditorPreviewer

); }); + +jest.mock('../../Tag/TagsViewer/TagsViewer', () => { + return jest.fn().mockReturnValue(

TagsViewer

); +}); + jest.mock('../../common/EntitySummaryDetails/EntitySummaryDetails', () => { return jest .fn() diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/SearchIndexSummary/SearchIndexSummary.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/SearchIndexSummary/SearchIndexSummary.test.tsx index bca30ad606f1..a4a5b1dd4f73 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/SearchIndexSummary/SearchIndexSummary.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/SearchIndexSummary/SearchIndexSummary.test.tsx @@ -33,7 +33,10 @@ describe('SearchIndexSummary component tests', () => { it('Component should render properly, when loaded in the Explore page.', async () => { await act(async () => { render( - + , + { + wrapper: MemoryRouter, + } ); }); @@ -115,7 +118,10 @@ describe('SearchIndexSummary component tests', () => { }, ], }} - /> + />, + { + wrapper: MemoryRouter, + } ); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/TableSummary/TableSummary.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/TableSummary/TableSummary.test.tsx index e0da86fa9443..d521754531ef 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/TableSummary/TableSummary.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/EntitySummaryPanel/TableSummary/TableSummary.test.tsx @@ -54,6 +54,12 @@ jest.mock('../SummaryList/SummaryList.component', () => .fn() .mockImplementation(() =>
SummaryList
) ); + +jest.mock( + '../../../common/SummaryTagsDescription/SummaryTagsDescription.component', + () => jest.fn().mockImplementation(() =>

SummaryTagsDescription

) +); + jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useLocation: jest.fn().mockReturnValue({ pathname: '/table' }), @@ -75,7 +81,7 @@ describe('TableSummary component tests', () => { const profilerHeader = screen.getByTestId('profiler-header'); const schemaHeader = screen.getByTestId('schema-header'); - const tagsHeader = screen.getByTestId('tags-header'); + const summaryTagDescription = screen.getByText('SummaryTagsDescription'); const typeLabel = screen.getByTestId('label.type-label'); const queriesLabel = screen.getByTestId('label.query-plural-label'); const columnsLabel = screen.getByTestId('label.column-plural-label'); @@ -88,7 +94,7 @@ describe('TableSummary component tests', () => { expect(profilerHeader).toBeInTheDocument(); expect(schemaHeader).toBeInTheDocument(); - expect(tagsHeader).toBeInTheDocument(); + expect(summaryTagDescription).toBeInTheDocument(); expect(typeLabel).toBeInTheDocument(); expect(queriesLabel).toBeInTheDocument(); expect(columnsLabel).toBeInTheDocument(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.component.tsx index 381eae5ca18f..da6c30317668 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.component.tsx @@ -12,8 +12,8 @@ */ import { Tag, Tooltip, Typography } from 'antd'; import classNames from 'classnames'; -import React, { useCallback, useMemo } from 'react'; -import { useHistory } from 'react-router-dom'; +import React, { useMemo } from 'react'; +import { Link } from 'react-router-dom'; import { ReactComponent as IconTerm } from '../../../assets/svg/book.svg'; import { ReactComponent as PlusIcon } from '../../../assets/svg/plus-primary.svg'; import { ReactComponent as IconTag } from '../../../assets/svg/tag.svg'; @@ -40,7 +40,6 @@ const TagsV1 = ({ tagType, size, }: TagsV1Props) => { - const history = useHistory(); const color = useMemo( () => (isVersionPage ? undefined : tag.style?.color), [tag] @@ -87,11 +86,11 @@ const TagsV1 = ({ [showOnlyName, tag.tagFQN] ); - const redirectLink = useCallback( + const redirectLink = useMemo( () => (tagType ?? tag.source) === TagSource.Glossary - ? history.push(getGlossaryPath(tag.tagFQN)) - : history.push(getTagPath(Fqn.split(tag.tagFQN)[0])), + ? getGlossaryPath(tag.tagFQN) + : getTagPath(Fqn.split(tag.tagFQN)[0]), [tagType, tag.source, tag.tagFQN] ); @@ -151,18 +150,23 @@ const TagsV1 = ({ ? { backgroundColor: reduceColorOpacity(color, 0.05) } : undefined } - onClick={redirectLink} {...tagProps}> - {tagContent} + {/* Wrap only content to avoid redirect on closeable icons */} + + {tagContent} + ), - [color, tagContent, className] + [color, tagContent, redirectLink] ); const addTagChip = useMemo( () => ( }> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.test.tsx index 64887ad04eb9..f740f27b5f02 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Tag/TagsV1/TagsV1.test.tsx @@ -17,12 +17,14 @@ import { TAG_CONSTANT, TAG_START_WITH } from '../../../constants/Tag.constants'; import { LabelType, State, TagSource } from '../../../generated/type/tagLabel'; import TagsV1 from './TagsV1.component'; -const mockPush = jest.fn(); +const mockLinkButton = jest.fn(); jest.mock('react-router-dom', () => ({ - useHistory: jest.fn().mockImplementation(() => ({ - push: mockPush, - })), + Link: jest.fn().mockImplementation(({ children, ...rest }) => ( + + {children} + + )), })); jest.mock('../../../utils/TagsUtils', () => ({ @@ -71,12 +73,11 @@ describe('Test tags Component', () => { }} /> ); - const tag = getByTestId(container, 'tags'); + const tag = getByTestId(container, 'tag-redirect-link'); fireEvent.click(tag); - expect(mockPush).toHaveBeenCalledTimes(1); - expect(mockPush).toHaveBeenCalledWith('/tags/testTag'); + expect(mockLinkButton).toHaveBeenCalledTimes(1); }); it('Clicking on tag with source Glossary should redirect to the proper glossary term page', () => { @@ -92,12 +93,11 @@ describe('Test tags Component', () => { }} /> ); - const tag = getByTestId(container, 'tags'); + const tag = getByTestId(container, 'tag-redirect-link'); fireEvent.click(tag); - expect(mockPush).toHaveBeenCalledTimes(1); - expect(mockPush).toHaveBeenCalledWith('/glossary/glossaryTag.Test1'); + expect(mockLinkButton).toHaveBeenCalledTimes(1); }); it('should render size based tags, for small class should contain small', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/TagsInput/TagsInput.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/TagsInput/TagsInput.test.tsx index fac9147a17b3..45fa3a643f08 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/TagsInput/TagsInput.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/TagsInput/TagsInput.test.tsx @@ -12,6 +12,7 @@ */ import { act, render, screen } from '@testing-library/react'; import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { LabelType, State, TagSource } from '../../generated/type/tagLabel'; import TagsInput from './TagsInput.component'; @@ -44,7 +45,10 @@ describe('TagsInput', () => { editable={false} tags={tags} onTagsUpdate={mockOnTagsUpdate} - /> + />, + { + wrapper: MemoryRouter, + } ); }); @@ -63,7 +67,10 @@ describe('TagsInput', () => { editable={false} tags={tags} onTagsUpdate={mockOnTagsUpdate} - /> + />, + { + wrapper: MemoryRouter, + } ); }); @@ -79,7 +86,10 @@ describe('TagsInput', () => { it('should render edit button when no editable', async () => { await act(async () => { render( - + , + { + wrapper: MemoryRouter, + } ); }); @@ -93,7 +103,10 @@ describe('TagsInput', () => { editable={false} tags={tags} onTagsUpdate={mockOnTagsUpdate} - /> + />, + { + wrapper: MemoryRouter, + } ); }); @@ -103,7 +116,14 @@ describe('TagsInput', () => { it('should not render tags if tags is empty', async () => { await act(async () => { render( - + , + { + wrapper: MemoryRouter, + } ); expect(await screen.findByTestId('tags-container')).toBeInTheDocument(); @@ -114,7 +134,9 @@ describe('TagsInput', () => { it('should render add tags if tags is empty and has permission', async () => { await act(async () => { - render(); + render(, { + wrapper: MemoryRouter, + }); expect(await screen.findByTestId('entity-tags')).toBeInTheDocument(); expect(await screen.findByTestId('add-tag')).toBeInTheDocument(); From eba4cd7c12de9c1a744e402cf5d405b916dfe149 Mon Sep 17 00:00:00 2001 From: Teddy Date: Tue, 4 Jun 2024 07:32:01 +0200 Subject: [PATCH 045/117] Fix #16229 - Tag and Service filters for test cases (#16484) * fix: added test case support for tags (inherit from table/column)]" * feat: add tag and service filter for test cases * feat: add tier query param * fix: tests (cherry picked from commit 6b00dde90285924445567ee7c396c89f0fcf3f1d) --- .../service/jdbi3/TestCaseRepository.java | 43 ++++- .../resources/dqtests/TestCaseResource.java | 20 +- .../service/search/SearchListFilter.java | 26 +++ .../service/search/indexes/TestCaseIndex.java | 1 + .../en/test_case_index_mapping.json | 21 +++ .../jp/test_case_index_mapping.json | 21 +++ .../zh/test_case_index_mapping.json | 21 +++ .../service/resources/EntityResourceTest.java | 2 +- .../dqtests/TestCaseResourceTest.java | 171 +++++++++++++++++- .../resources/json/schema/tests/testCase.json | 8 + 10 files changed, 325 insertions(+), 9 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java index 02cbfb448adc..6f0241792725 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java @@ -8,6 +8,8 @@ import static org.openmetadata.schema.type.EventType.ENTITY_UPDATED; import static org.openmetadata.schema.type.EventType.LOGICAL_TEST_CASE_ADDED; import static org.openmetadata.schema.type.Include.ALL; +import static org.openmetadata.service.Entity.FIELD_OWNER; +import static org.openmetadata.service.Entity.FIELD_TAGS; import static org.openmetadata.service.Entity.TEST_CASE; import static org.openmetadata.service.Entity.TEST_DEFINITION; import static org.openmetadata.service.Entity.TEST_SUITE; @@ -104,14 +106,32 @@ public void setFields(TestCase test, Fields fields) { : test.getTestCaseResult()); test.setIncidentId( fields.contains(INCIDENTS_FIELD) ? getIncidentId(test) : test.getIncidentId()); + test.setTags(fields.contains(FIELD_TAGS) ? getTestCaseTags(test) : test.getTags()); } @Override public void setInheritedFields(TestCase testCase, Fields fields) { EntityLink entityLink = EntityLink.parse(testCase.getEntityLink()); - Table table = Entity.getEntity(entityLink, "owner,domain", ALL); + Table table = Entity.getEntity(entityLink, "owner,domain,tags,columns", ALL); inheritOwner(testCase, fields, table); inheritDomain(testCase, fields, table); + inheritTags(testCase, fields, table); + } + + private void inheritTags(TestCase testCase, Fields fields, Table table) { + if (fields.contains(FIELD_TAGS)) { + List tags = new ArrayList<>(); + EntityLink entityLink = EntityLink.parse(testCase.getEntityLink()); + tags.addAll(table.getTags()); + if (entityLink.getFieldName() != null && entityLink.getFieldName().equals("columns")) { + // if we have a column test case get the columns tags as well + table.getColumns().stream() + .filter(column -> column.getName().equals(entityLink.getArrayFieldName())) + .findFirst() + .ifPresent(column -> tags.addAll(column.getTags())); + } + testCase.setTags(tags); + } } @Override @@ -293,10 +313,8 @@ public RestUtil.PutResponse addTestCaseResult( String updatedBy, UriInfo uriInfo, String fqn, TestCaseResult testCaseResult) { // Validate the request content TestCase testCase = findByName(fqn, Include.NON_DELETED); - ArrayList fields = new ArrayList<>(); - fields.add("testDefinition"); - fields.add("owner"); - fields.add(TEST_SUITE_FIELD); + ArrayList fields = + new ArrayList<>(List.of("testDefinition", FIELD_OWNER, FIELD_TAGS, TEST_SUITE_FIELD)); // set the test case resolution status reference if test failed, by either // creating a new incident or returning the stateId of an unresolved incident @@ -627,6 +645,21 @@ private UUID getIncidentId(TestCase test) { return ongoingIncident; } + private List getTestCaseTags(TestCase test) { + List tags = new ArrayList<>(); + EntityLink entityLink = EntityLink.parse(test.getEntityLink()); + Table table = Entity.getEntity(entityLink, "tags,columns", ALL); + tags.addAll(table.getTags()); + if (entityLink.getFieldName() != null && entityLink.getFieldName().equals("columns")) { + // if we have a column test case get the columns tags as well + table.getColumns().stream() + .filter(column -> column.getName().equals(entityLink.getArrayFieldName())) + .findFirst() + .ifPresent(column -> tags.addAll(column.getTags())); + } + return tags; + } + public int getTestCaseCount(List testCaseIds) { return daoCollection.testCaseDAO().countOfTestCases(testCaseIds); } diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java index 1091f1150689..3e26124b77ed 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/dqtests/TestCaseResource.java @@ -87,7 +87,7 @@ public class TestCaseResource extends EntityResource { public static final String COLLECTION_PATH = "/v1/dataQuality/testCases"; - static final String FIELDS = "owner,testSuite,testDefinition,testSuites,incidentId,domain"; + static final String FIELDS = "owner,testSuite,testDefinition,testSuites,incidentId,domain,tags"; static final String SEARCH_FIELDS_EXCLUDE = "testPlatforms,table,database,databaseSchema,service,testSuite"; @@ -352,12 +352,26 @@ public ResultList listFromSearch( @QueryParam("sortType") @DefaultValue("desc") String sortType, + @Parameter( + description = "Return only required fields in the response", + schema = @Schema(type = "string")) + @QueryParam("includeFields") + String includeFields, @Parameter(description = "domain filter to use in list", schema = @Schema(type = "string")) @QueryParam("domain") String domain, @Parameter(description = "owner filter to use in list", schema = @Schema(type = "string")) @QueryParam("owner") String owner, + @Parameter(description = "tags filter to use in list", schema = @Schema(type = "string")) + @QueryParam("tags") + String tags, + @Parameter(description = "tier filter to use in list", schema = @Schema(type = "string")) + @QueryParam("tier") + String tier, + @Parameter(description = "service filter to use in list", schema = @Schema(type = "string")) + @QueryParam("serviceName") + String serviceName, @Parameter( description = "search query term to use in list", schema = @Schema(type = "string")) @@ -378,7 +392,11 @@ public ResultList listFromSearch( searchListFilter.addQueryParam("testPlatforms", testPlatforms); searchListFilter.addQueryParam("q", q); searchListFilter.addQueryParam("excludeFields", SEARCH_FIELDS_EXCLUDE); + searchListFilter.addQueryParam("includeFields", includeFields); searchListFilter.addQueryParam("domain", domain); + searchListFilter.addQueryParam("tags", tags); + searchListFilter.addQueryParam("tier", tier); + searchListFilter.addQueryParam("serviceName", serviceName); if (!nullOrEmpty(owner)) { EntityInterface entity; StringBuffer owners = new StringBuffer(); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchListFilter.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchListFilter.java index cf7468571341..984f13bc222d 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchListFilter.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/SearchListFilter.java @@ -138,6 +138,32 @@ private String getTestCaseCondition() { String testPlatform = getQueryParam("testPlatforms"); String startTimestamp = getQueryParam("startTimestamp"); String endTimestamp = getQueryParam("endTimestamp"); + String tags = getQueryParam("tags"); + String tier = getQueryParam("tier"); + String serviceName = getQueryParam("serviceName"); + + if (tags != null) { + String tagsList = + Arrays.stream(tags.split(",")) + .map(this::escapeDoubleQuotes) + .collect(Collectors.joining("\", \"", "\"", "\"")); + conditions.add( + String.format( + "{\"nested\":{\"path\":\"tags\",\"query\":{\"terms\":{\"tags.tagFQN\":[%s]}}}}", + tagsList)); + } + + if (tier != null) { + conditions.add( + String.format( + "{\"nested\":{\"path\":\"tags\",\"query\":{\"terms\":{\"tags.tagFQN\":[\"%s\"]}}}}", + escapeDoubleQuotes(tier))); + } + + if (serviceName != null) { + conditions.add( + String.format("{\"term\": {\"service.name\": \"%s\"}}", escapeDoubleQuotes(serviceName))); + } if (entityFQN != null) { conditions.add( diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TestCaseIndex.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TestCaseIndex.java index d99717c9d96f..a4754a2f288a 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TestCaseIndex.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TestCaseIndex.java @@ -43,6 +43,7 @@ public Map buildSearchIndexDocInternal(Map doc) doc.put("suggest", suggest); doc.put("entityType", Entity.TEST_CASE); doc.put("owner", getEntityWithDisplayName(testCase.getOwner())); + doc.put("tags", testCase.getTags()); doc.put("testPlatforms", getTestDefinitionPlatforms(testCase.getTestDefinition().getId())); doc.put("followers", SearchIndexUtils.parseFollowers(testCase.getFollowers())); setParentRelationships(doc, testCase); diff --git a/openmetadata-service/src/main/resources/elasticsearch/en/test_case_index_mapping.json b/openmetadata-service/src/main/resources/elasticsearch/en/test_case_index_mapping.json index 7f46ceca1f6f..f2c822d5f166 100644 --- a/openmetadata-service/src/main/resources/elasticsearch/en/test_case_index_mapping.json +++ b/openmetadata-service/src/main/resources/elasticsearch/en/test_case_index_mapping.json @@ -406,6 +406,27 @@ } } }, + "tags": { + "type": "nested", + "properties": { + "tagFQN": { + "type": "keyword" + }, + "labelType": { + "type": "keyword" + }, + "description": { + "type": "text", + "index_options": "docs" + }, + "source": { + "type": "keyword" + }, + "state": { + "type": "keyword" + } + } + }, "version": { "type": "float" }, diff --git a/openmetadata-service/src/main/resources/elasticsearch/jp/test_case_index_mapping.json b/openmetadata-service/src/main/resources/elasticsearch/jp/test_case_index_mapping.json index b37fbcc0fa5a..521b7167be10 100644 --- a/openmetadata-service/src/main/resources/elasticsearch/jp/test_case_index_mapping.json +++ b/openmetadata-service/src/main/resources/elasticsearch/jp/test_case_index_mapping.json @@ -202,6 +202,27 @@ } ] }, + "tags": { + "type": "nested", + "properties": { + "tagFQN": { + "type": "keyword" + }, + "labelType": { + "type": "keyword" + }, + "description": { + "type": "text", + "index_options": "docs" + }, + "source": { + "type": "keyword" + }, + "state": { + "type": "keyword" + } + } + }, "owner": { "properties": { "id": { diff --git a/openmetadata-service/src/main/resources/elasticsearch/zh/test_case_index_mapping.json b/openmetadata-service/src/main/resources/elasticsearch/zh/test_case_index_mapping.json index 2469ae431c5e..d83db090bdd5 100644 --- a/openmetadata-service/src/main/resources/elasticsearch/zh/test_case_index_mapping.json +++ b/openmetadata-service/src/main/resources/elasticsearch/zh/test_case_index_mapping.json @@ -105,6 +105,27 @@ "type": "keyword", "normalizer": "lowercase_normalizer" }, + "tags": { + "type": "nested", + "properties": { + "tagFQN": { + "type": "keyword" + }, + "labelType": { + "type": "keyword" + }, + "description": { + "type": "text", + "index_options": "docs" + }, + "source": { + "type": "keyword" + }, + "state": { + "type": "keyword" + } + } + }, "entityFQN": { "type": "keyword", "normalizer": "lowercase_normalizer" diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java index dc380e20e2bb..8cd75709e642 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java @@ -224,7 +224,7 @@ public abstract class EntityResourceTest + assertFalse( + tc.getTags().stream() + .noneMatch( + t -> + t.getTagFQN() + .matches( + String.format( + "(%s|%s)", + PII_SENSITIVE_TAG_LABEL.getTagFQN(), + PERSONAL_DATA_TAG_LABEL.getTagFQN()))))); + + queryParams.put("tags", PERSONAL_DATA_TAG_LABEL.getTagFQN()); + allEntities = listEntitiesFromSearch(queryParams, testCasesNum, 0, ADMIN_AUTH_HEADERS); + // check we have all test cases with PERSONAL_DATA_TAG_LABEL + allEntities + .getData() + .forEach( + tc -> + assertTrue( + tc.getTags().stream() + .anyMatch( + t -> t.getTagFQN().contains(PERSONAL_DATA_TAG_LABEL.getTagFQN())))); + + queryParams.clear(); + queryParams.put("tier", TIER1_TAG_LABEL.getTagFQN()); + queryParams.put("fields", "tags"); + allEntities = listEntitiesFromSearch(queryParams, testCasesNum, 0, ADMIN_AUTH_HEADERS); + // check we have all test cases with TIER1_TAG_LABEL + allEntities + .getData() + .forEach( + tc -> + assertTrue( + tc.getTags().stream() + .anyMatch(t -> t.getTagFQN().contains(TIER1_TAG_LABEL.getTagFQN())))); + + queryParams.clear(); + String serviceName = tables.get(0).getService().getName(); + queryParams.put("serviceName", serviceName); + allEntities = listEntitiesFromSearch(queryParams, testCasesNum, 0, ADMIN_AUTH_HEADERS); + assertTrue( + allEntities.getData().stream().allMatch(tc -> tc.getEntityLink().contains(serviceName))); + + // Test return only requested fields + queryParams.put("includeFields", "id,name,entityLink"); + allEntities = listEntitiesFromSearch(queryParams, testCasesNum, 0, ADMIN_AUTH_HEADERS); + TestCase testCase = allEntities.getData().get(0); + assertNull(testCase.getDescription()); + assertNull(testCase.getTestSuite()); + assertNotNull(testCase.getEntityLink()); + assertNotNull(testCase.getName()); + assertNotNull(testCase.getId()); + } + + @Test + void test_testCaseInheritedFields(TestInfo testInfo) throws HttpResponseException, IOException { + // Set up the test case + TableResourceTest tableResourceTest = new TableResourceTest(); + TestSuiteResourceTest testSuiteResourceTest = new TestSuiteResourceTest(); + CreateTable createTable = tableResourceTest.createRequest(testInfo); + createTable + .withDatabaseSchema(DATABASE_SCHEMA.getFullyQualifiedName()) + .withColumns( + List.of( + new Column() + .withName(C1) + .withDisplayName("c1") + .withDataType(ColumnDataType.VARCHAR) + .withDataLength(10) + .withTags(List.of(PII_SENSITIVE_TAG_LABEL)))) + .withOwner(USER1_REF) + .withDomain(DOMAIN1.getFullyQualifiedName()) + .withTags(List.of(PERSONAL_DATA_TAG_LABEL, TIER1_TAG_LABEL)); + Table table = tableResourceTest.createEntity(createTable, ADMIN_AUTH_HEADERS); + CreateTestSuite createTestSuite = + testSuiteResourceTest.createRequest(table.getFullyQualifiedName()); + TestSuite testSuite = + testSuiteResourceTest.createExecutableTestSuite(createTestSuite, ADMIN_AUTH_HEADERS); + + CreateTestCase create = + createRequest(testInfo) + .withEntityLink(String.format("<#E::table::%s>", table.getFullyQualifiedName())) + .withTestSuite(testSuite.getFullyQualifiedName()) + .withTestDefinition(TEST_DEFINITION2.getFullyQualifiedName()); + createEntity(create, ADMIN_AUTH_HEADERS); + create = + createRequest(testInfo) + .withEntityLink( + String.format("<#E::table::%s::columns::%s>", table.getFullyQualifiedName(), C1)) + .withTestSuite(testSuite.getFullyQualifiedName()) + .withTestDefinition(TEST_DEFINITION3.getFullyQualifiedName()) + .withParameterValues( + List.of( + new TestCaseParameterValue().withValue("20").withName("missingCountValue"))); + createEntity(create, ADMIN_AUTH_HEADERS); + + // Run the tests assertions + Map queryParams = new HashMap<>(); + queryParams.put("entityLink", String.format("<#E::table::%s>", table.getFullyQualifiedName())); + queryParams.put("includeAllTests", "true"); + queryParams.put("fields", "domain,owner,tags"); + ResultList testCases = listEntitiesFromSearch(queryParams, 10, 0, ADMIN_AUTH_HEADERS); + assertEquals(2, testCases.getData().size()); + for (TestCase testCase : testCases.getData()) { + assertEquals(table.getOwner().getId(), testCase.getOwner().getId()); + assertEquals(table.getDomain().getId(), testCase.getDomain().getId()); + List tags = testCase.getTags(); + HashSet actualTags = + tags.stream().map(TagLabel::getName).collect(Collectors.toCollection(HashSet::new)); + HashSet expectedTags; + if (testCase.getEntityLink().contains(C1)) { + expectedTags = + new HashSet<>( + List.of( + PERSONAL_DATA_TAG_LABEL.getName(), + TIER1_TAG_LABEL.getName(), + PII_SENSITIVE_TAG_LABEL.getName())); + } else { + expectedTags = + new HashSet<>(List.of(PERSONAL_DATA_TAG_LABEL.getName(), TIER1_TAG_LABEL.getName())); + } + assertEquals(expectedTags, actualTags); + } + + createTable.setOwner(USER2_REF); + createTable.setDomain(DOMAIN.getFullyQualifiedName()); + createTable.setTags(List.of(USER_ADDRESS_TAG_LABEL)); + createTable.withColumns( + List.of( + new Column() + .withName(C1) + .withDisplayName("c1") + .withDataType(ColumnDataType.VARCHAR) + .withDataLength(10) + .withTags(List.of(PERSONAL_DATA_TAG_LABEL)))); + table = tableResourceTest.updateEntity(createTable, OK, ADMIN_AUTH_HEADERS); + testCases = listEntitiesFromSearch(queryParams, 10, 0, ADMIN_AUTH_HEADERS); + + for (TestCase testCase : testCases.getData()) { + assertEquals(table.getOwner().getId(), testCase.getOwner().getId()); + assertEquals(table.getDomain().getId(), testCase.getDomain().getId()); + List tags = testCase.getTags(); + HashSet actualTags = + tags.stream().map(TagLabel::getName).collect(Collectors.toCollection(HashSet::new)); + HashSet expectedTags; + List expectedTagsList = table.getTags(); + if (testCase.getEntityLink().contains(C1)) { + expectedTagsList.addAll(table.getColumns().get(0).getTags()); + } + expectedTags = new HashSet<>(expectedTagsList.stream().map(TagLabel::getName).toList()); + assertEquals(expectedTags, actualTags); + } } public void putTestCaseResult(String fqn, TestCaseResult data, Map authHeaders) diff --git a/openmetadata-spec/src/main/resources/json/schema/tests/testCase.json b/openmetadata-spec/src/main/resources/json/schema/tests/testCase.json index b27e4d3628f7..c0a6120f4fe4 100644 --- a/openmetadata-spec/src/main/resources/json/schema/tests/testCase.json +++ b/openmetadata-spec/src/main/resources/json/schema/tests/testCase.json @@ -122,6 +122,14 @@ "domain" : { "description": "Domain the test case belongs to. When not set, the test case inherits the domain from the table it belongs to.", "$ref": "../type/entityReference.json" + }, + "tags": { + "description": "Tags for this test case. This is an inherited field from the parent entity and is not set directly on the test case.", + "type": "array", + "items": { + "$ref": "../type/tagLabel.json" + }, + "default": null } }, "required": ["name", "testDefinition", "entityLink", "testSuite"], From 7b0a7c36a8293e2c644c969cbed9cf805a84015f Mon Sep 17 00:00:00 2001 From: Teddy Date: Fri, 31 May 2024 12:17:28 +0200 Subject: [PATCH 046/117] fix: None type is not iterable (#16496) (cherry picked from commit 656da03b14ca24171cf7924b9dd33663e6bed423) --- ingestion/src/metadata/profiler/processor/metric_filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ingestion/src/metadata/profiler/processor/metric_filter.py b/ingestion/src/metadata/profiler/processor/metric_filter.py index d2a1a5a3f6d8..de297ddba4ec 100644 --- a/ingestion/src/metadata/profiler/processor/metric_filter.py +++ b/ingestion/src/metadata/profiler/processor/metric_filter.py @@ -229,7 +229,7 @@ def filter_column_metrics_from_table_config( metric_names = next( ( include_columns.metrics - for include_columns in columns_config + for include_columns in columns_config or [] if include_columns.columnName == column.name ), None, From 3bb67580a24f8f5d322b7581c9c48d4215bb1fee Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Fri, 31 May 2024 17:47:52 +0530 Subject: [PATCH 047/117] minor(ui): refresh token for OIDC SSO (#16483) * minor(ui): refresh token for OIDC SSO * remove frame window timeout issue * increase iFrame timeout for oidc (cherry picked from commit 1a6c4c972052836a9b3cfa273b7ea1aa3202eafe) --- .../AppAuthenticators/OidcAuthenticator.tsx | 25 +++++++++++-------- .../Auth/AuthProviders/AuthProvider.tsx | 14 +++-------- .../ui/src/utils/AuthProvider.util.ts | 5 ++-- 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/OidcAuthenticator.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/OidcAuthenticator.tsx index 78b6db60abb0..42eb0171e08f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/OidcAuthenticator.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AppAuthenticators/OidcAuthenticator.tsx @@ -75,7 +75,7 @@ const OidcAuthenticator = forwardRef( } = useApplicationStore(); const history = useHistory(); const userManager = useMemo( - () => makeUserManager(userConfig), + () => makeUserManager({ ...userConfig, silentRequestTimeout: 20000 }), [userConfig] ); @@ -176,16 +176,19 @@ const OidcAuthenticator = forwardRef( )} /> - {/* render the children only if user is authenticated */} - {isAuthenticated ? ( - {children} - ) : // render the sign in page if user is not authenticated and not signing up - !isSigningUp && isEmpty(currentUser) && isEmpty(newUser) ? ( - - ) : ( - // render the authenticator component to handle the auth flow while user is signing in - - )} + {!window.location.pathname.includes(ROUTES.SILENT_CALLBACK) && + // render the children only if user is authenticated + (isAuthenticated ? ( + !window.location.pathname.includes(ROUTES.SILENT_CALLBACK) && ( + {children} + ) + ) : // render the sign in page if user is not authenticated and not signing up + !isSigningUp && isEmpty(currentUser) && isEmpty(newUser) ? ( + + ) : ( + // render the authenticator component to handle the auth flow while user is signing in + + ))} {/* show loader when application is loading and user is signing up*/} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx index 712a049e6178..54115ec0a8bc 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Auth/AuthProviders/AuthProvider.tsx @@ -278,9 +278,11 @@ export const AuthProvider = ({ * if it's not succeed then it will proceed for logout */ const trySilentSignIn = async (forceLogout?: boolean) => { - const pathName = location.pathname; + const pathName = window.location.pathname; // Do not try silent sign in for SignIn or SignUp route - if ([ROUTES.SIGNIN, ROUTES.SIGNUP].includes(pathName)) { + if ( + [ROUTES.SIGNIN, ROUTES.SIGNUP, ROUTES.SILENT_CALLBACK].includes(pathName) + ) { return; } @@ -303,14 +305,6 @@ export const AuthProvider = ({ resetUserDetails(forceLogout); } } catch (error) { - const err = error as AxiosError; - if (err.message.includes('Frame window timed out')) { - // Start expiry timer if silent signIn is timed out - // eslint-disable-next-line @typescript-eslint/no-use-before-define - startTokenExpiryTimer(); - - return; - } // reset user details if silent signIn fails resetUserDetails(forceLogout); } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts b/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts index 9e88bf9f12a5..60ac5aa2647e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider.util.ts @@ -35,9 +35,10 @@ import { isDev } from './EnvironmentUtils'; const cookieStorage = new CookieStorage(); -// 25s for server auth approch +// 25s for server auth approach export const EXPIRY_THRESHOLD_MILLES = 25 * 1000; -// 2 minutes for client auth approch + +// 2 minutes for client auth approach export const EXPIRY_THRESHOLD_MILLES_PUBLIC = 2 * 60 * 1000; export const getRedirectUri = (callbackUrl: string) => { From 6af76c56dfe3c6be8245752aacb313a4e9d54e69 Mon Sep 17 00:00:00 2001 From: Chirag Madlani <12962843+chirag-madlani@users.noreply.github.com> Date: Tue, 4 Jun 2024 15:44:46 +0530 Subject: [PATCH 048/117] feat(ui): support tag & tier filter for test case (#16502) * feat(ui): support tag & tier filter for test case * fix tag filter * allow single select for tier * added service name filter * update cypress for tags, tier & service * add specific add for filters * fix tier api call (cherry picked from commit 5b71d79e8ac2d08a154882dfe71b9b3a0f73bffc) --- .../ui/cypress/common/Utils/DataQuality.ts | 34 +++ .../e2e/Pages/DataQualityAndProfiler.spec.ts | 106 +++++++- .../TestCases/TestCases.component.tsx | 238 +++++++++++++++--- .../ui/src/constants/profiler.constant.ts | 3 + .../src/main/resources/ui/src/rest/testAPI.ts | 3 + .../src/utils/DataQuality/DataQualityUtils.ts | 3 + 6 files changed, 348 insertions(+), 39 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/DataQuality.ts b/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/DataQuality.ts index bc06577e8cd8..8812c88a7f3d 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/DataQuality.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/DataQuality.ts @@ -299,6 +299,40 @@ export const prepareDataQualityTestCases = (token: string) => { ); }); + cy.request({ + method: 'PATCH', + url: `/api/v1/tables/name/${filterTable.databaseSchema}.${filterTable.name}`, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json-patch+json', + }, + body: [ + { + op: 'add', + path: '/tags/0', + value: { + tagFQN: 'PII.None', + name: 'None', + description: 'Non PII', + source: 'Classification', + labelType: 'Manual', + state: 'Confirmed', + }, + }, + { + op: 'add', + path: '/tags/1', + value: { + tagFQN: 'Tier.Tier2', + name: 'Tier2', + source: 'Classification', + labelType: 'Manual', + state: 'Confirmed', + }, + }, + ], + }); + prepareDataQualityTestCasesViaREST({ testSuite: filterTableTestSuite, token, diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/DataQualityAndProfiler.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/DataQualityAndProfiler.spec.ts index 036b85790a63..af2d982a81d8 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/DataQualityAndProfiler.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/DataQualityAndProfiler.spec.ts @@ -106,6 +106,16 @@ const verifyFilterTestCase = () => { cy.get(`[data-testid="${testCase}"]`).scrollIntoView().should('be.visible'); }); }; +const verifyFilter2TestCase = (negation = false) => { + filterTable2TestCases.map((testCase) => { + negation + ? cy.get(`[data-testid="${testCase}"]`).should('not.exist') + : cy + .get(`[data-testid="${testCase}"]`) + .scrollIntoView() + .should('be.visible'); + }); +}; describe( 'Data Quality and Profiler should work properly', @@ -957,6 +967,12 @@ describe( 'getTestCase' ); + interceptURL( + 'GET', + `/api/v1/search/query?q=*index=tag_search_index*`, + 'searchTags' + ); + cy.sidebarClick(SidebarItem.DATA_QUALITY); cy.get('[data-testid="by-test-cases"]').click(); @@ -978,6 +994,19 @@ describe( waitForAnimations: true, }); cy.get('[value="lastRunRange"]').click({ waitForAnimations: true }); + cy.get('[data-testid="advanced-filter"]').click({ + waitForAnimations: true, + }); + cy.get('[value="serviceName"]').click({ waitForAnimations: true }); + + cy.get('[data-testid="advanced-filter"]').click({ + waitForAnimations: true, + }); + cy.get('[value="tags"]').click({ waitForAnimations: true }); + cy.get('[data-testid="advanced-filter"]').click({ + waitForAnimations: true, + }); + cy.get('[value="tier"]').click({ waitForAnimations: true }); // Test case search filter cy.get( @@ -990,6 +1019,78 @@ describe( cy.get('.ant-input-clear-icon').click(); verifyResponseStatusCode('@getTestCase', 200); + // Test case filter by service name + interceptURL( + 'GET', + `/api/v1/dataQuality/testCases/search/list?*serviceName=${DATABASE_SERVICE.service.name}*`, + 'getTestCaseByServiceName' + ); + interceptURL( + 'GET', + `/api/v1/search/query?q=*index=database_service_search_index*`, + 'searchService' + ); + cy.get('#serviceName') + .scrollIntoView() + .type(DATABASE_SERVICE.service.name); + verifyResponseStatusCode('@searchService', 200); + cy.get('.ant-select-dropdown') + .not('.ant-select-dropdown-hidden') + .find(`[data-testid="${DATABASE_SERVICE.service.name}"]`) + .click({ force: true }); + verifyResponseStatusCode('@getTestCaseByServiceName', 200); + verifyFilterTestCase(); + verifyFilter2TestCase(); + // remove service filter + cy.get('[data-testid="advanced-filter"]').click({ + waitForAnimations: true, + }); + cy.get('[value="serviceName"]').click({ waitForAnimations: true }); + verifyResponseStatusCode('@getTestCase', 200); + + // Test case filter by Tags + interceptURL( + 'GET', + `/api/v1/dataQuality/testCases/search/list?*tags=${'PII.None'}*`, + 'getTestCaseByTags' + ); + cy.get('#tags').scrollIntoView().click().type('PII.None'); + verifyResponseStatusCode('@searchTags', 200); + cy.get('.ant-select-dropdown') + .not('.ant-select-dropdown-hidden') + .find(`[data-testid="${'PII.None'}"]`) + .click({ force: true }); + verifyResponseStatusCode('@getTestCaseByTags', 200); + verifyFilterTestCase(); + verifyFilter2TestCase(true); + // remove service filter + cy.get('[data-testid="advanced-filter"]').click({ + waitForAnimations: true, + }); + cy.get('[value="tags"]').click({ waitForAnimations: true }); + verifyResponseStatusCode('@getTestCase', 200); + + // Test case filter by Tier + interceptURL( + 'GET', + `/api/v1/dataQuality/testCases/search/list?*tier=${'Tier.Tier2'}*`, + 'getTestCaseByTier' + ); + cy.get('#tier').click(); + cy.get('.ant-select-dropdown') + .not('.ant-select-dropdown-hidden') + .find(`[data-testid="${'Tier.Tier2'}"]`) + .click({ force: true }); + verifyResponseStatusCode('@getTestCaseByTier', 200); + verifyFilterTestCase(); + verifyFilter2TestCase(true); + // remove service filter + cy.get('[data-testid="advanced-filter"]').click({ + waitForAnimations: true, + }); + cy.get('[value="tier"]').click({ waitForAnimations: true }); + verifyResponseStatusCode('@getTestCase', 200); + // Test case filter by table name interceptURL( 'GET', @@ -1011,10 +1112,7 @@ describe( .click({ force: true }); verifyResponseStatusCode('@searchTestCaseByTable', 200); verifyFilterTestCase(); - - filterTable2TestCases.map((testCase) => { - cy.get(`[data-testid="${testCase}"]`).should('not.exist'); - }); + verifyFilter2TestCase(true); // Test case filter by test type interceptURL( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCases/TestCases.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCases/TestCases.component.tsx index 3095cd066d09..f54d680bcad5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCases/TestCases.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/TestCases/TestCases.component.tsx @@ -51,6 +51,7 @@ import { INITIAL_PAGING_VALUE, PAGE_SIZE, PAGE_SIZE_BASE, + TIER_CATEGORY, } from '../../../constants/constants'; import { TEST_CASE_FILTERS, @@ -65,6 +66,7 @@ import { TestCase } from '../../../generated/tests/testCase'; import { usePaging } from '../../../hooks/paging/usePaging'; import { DataQualityPageTabs } from '../../../pages/DataQuality/DataQualityPage.interface'; import { searchQuery } from '../../../rest/searchAPI'; +import { getTags } from '../../../rest/tagAPI'; import { getListTestCaseBySearch, ListTestCaseParamsBySearch, @@ -73,6 +75,7 @@ import { buildTestCaseParams } from '../../../utils/DataQuality/DataQualityUtils import { getEntityName } from '../../../utils/EntityUtils'; import { getDataQualityPagePath } from '../../../utils/RouterUtils'; import { generateEntityLink } from '../../../utils/TableUtils'; +import tagClassBase from '../../../utils/TagClassBase'; import { showErrorToast } from '../../../utils/ToastUtils'; import DatePickerMenu from '../../common/DatePickerMenu/DatePickerMenu.component'; import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; @@ -90,7 +93,10 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => { const { permissions } = usePermissionProvider(); const { testCase: testCasePermission } = permissions; const [tableOptions, setTableOptions] = useState([]); - const [isTableLoading, setIsTableLoading] = useState(false); + const [isOptionsLoading, setIsOptionsLoading] = useState(false); + const [tagOptions, setTagOptions] = useState([]); + const [tierOptions, setTierOptions] = useState([]); + const [serviceOptions, setServiceOptions] = useState([]); const params = useMemo(() => { const search = location.search; @@ -204,6 +210,7 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => { buildTestCaseParams(params, selectedFilter), isUndefined ); + if (!isEqual(filters, updatedParams)) { fetchTestCases(INITIAL_PAGING_VALUE, updatedParams); } @@ -211,40 +218,74 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => { setFilters(updatedParams); }; - const handleMenuClick = ({ key }: { key: string }) => { - setSelectedFilter((prevSelected) => { - if (prevSelected.includes(key)) { - const updatedValue = prevSelected.filter( - (selected) => selected !== key - ); - const updatedFilters = omitBy( - buildTestCaseParams(filters, updatedValue), - isUndefined - ); - form.setFieldsValue({ [key]: undefined }); - if (!isEqual(filters, updatedFilters)) { - fetchTestCases(INITIAL_PAGING_VALUE, updatedFilters); - } - setFilters(updatedFilters); + const fetchTierOptions = async () => { + try { + setIsOptionsLoading(true); + const { data } = await getTags({ + parent: 'Tier', + }); - return updatedValue; - } + const options = data.map((hit) => { + return { + label: ( + + + {hit.fullyQualifiedName} + + + {getEntityName(hit)} + + + ), + value: hit.fullyQualifiedName, + }; + }); - return [...prevSelected, key]; - }); + setTierOptions(options); + } catch (error) { + setTierOptions([]); + } finally { + setIsOptionsLoading(false); + } }; - const filterMenu: ItemType[] = useMemo(() => { - return entries(TEST_CASE_FILTERS).map(([name, filter]) => ({ - key: filter, - label: startCase(name), - value: filter, - onClick: handleMenuClick, - })); - }, [filters]); + const fetchTagOptions = async (search?: string) => { + setIsOptionsLoading(true); + try { + const { data } = await tagClassBase.getTags(search ?? '', 1); + + const options = data + .filter( + ({ data: { classification } }) => + classification?.name !== TIER_CATEGORY + ) + .map(({ label, value }) => { + return { + label: ( + + + {value} + + {label} + + ), + value: value, + }; + }); + + setTagOptions(options); + } catch (error) { + setTagOptions([]); + } finally { + setIsOptionsLoading(false); + } + }; const fetchTableData = async (search = WILD_CARD_CHAR) => { - setIsTableLoading(true); + setIsOptionsLoading(true); try { const response = await searchQuery({ query: `*${search}*`, @@ -277,14 +318,99 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => { } catch (error) { setTableOptions([]); } finally { - setIsTableLoading(false); + setIsOptionsLoading(false); + } + }; + + const fetchServiceOptions = async (search = WILD_CARD_CHAR) => { + setIsOptionsLoading(true); + try { + const response = await searchQuery({ + query: `*${search}*`, + pageNumber: 1, + pageSize: PAGE_SIZE_BASE, + searchIndex: SearchIndex.DATABASE_SERVICE, + fetchSource: true, + includeFields: ['name', 'fullyQualifiedName', 'displayName'], + }); + + const options = response.hits.hits.map((hit) => { + return { + label: ( + + + {hit._source.fullyQualifiedName} + + + {getEntityName(hit._source)} + + + ), + value: hit._source.fullyQualifiedName, + }; + }); + setServiceOptions(options); + } catch (error) { + setServiceOptions([]); + } finally { + setIsOptionsLoading(false); } }; + const handleMenuClick = ({ key }: { key: string }) => { + setSelectedFilter((prevSelected) => { + if (prevSelected.includes(key)) { + const updatedValue = prevSelected.filter( + (selected) => selected !== key + ); + const updatedFilters = omitBy( + buildTestCaseParams(filters, updatedValue), + isUndefined + ); + form.setFieldsValue({ [key]: undefined }); + if (!isEqual(filters, updatedFilters)) { + fetchTestCases(INITIAL_PAGING_VALUE, updatedFilters); + } + setFilters(updatedFilters); + + return updatedValue; + } + + return [...prevSelected, key]; + }); + + // Fetch options based on the selected filter + key === TEST_CASE_FILTERS.tier && fetchTierOptions(); + key === TEST_CASE_FILTERS.tags && fetchTagOptions(); + key === TEST_CASE_FILTERS.table && fetchTableData(); + key === TEST_CASE_FILTERS.service && fetchServiceOptions(); + }; + + const filterMenu: ItemType[] = useMemo(() => { + return entries(TEST_CASE_FILTERS).map(([name, filter]) => ({ + key: filter, + label: startCase(name), + value: filter, + onClick: handleMenuClick, + })); + }, [filters]); + const debounceFetchTableData = useCallback(debounce(fetchTableData, 1000), [ fetchTableData, ]); + const debounceFetchTagOptions = useCallback(debounce(fetchTagOptions, 1000), [ + fetchTagOptions, + ]); + + const debounceFetchServiceOptions = useCallback( + debounce(fetchServiceOptions, 1000), + [fetchServiceOptions] + ); + useEffect(() => { if (testCasePermission?.ViewAll || testCasePermission?.ViewBasic) { if (tab === DataQualityPageTabs.TEST_CASES) { @@ -295,10 +421,6 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => { } }, [tab, searchValue, testCasePermission, pageSize]); - useEffect(() => { - fetchTableData(); - }, []); - const pagingData = useMemo( () => ({ paging, @@ -363,7 +485,7 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => { allowClear showSearch data-testid="table-select-filter" - loading={isTableLoading} + loading={isOptionsLoading} options={tableOptions} placeholder={t('label.table')} onSearch={debounceFetchTableData} @@ -420,6 +542,52 @@ export const TestCases = ({ summaryPanel }: { summaryPanel: ReactNode }) => { )} + {selectedFilter.includes(TEST_CASE_FILTERS.tags) && ( + + + + )} + {selectedFilter.includes(TEST_CASE_FILTERS.service) && ( + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.test.tsx index 1036c315882d..dca3ee95a2a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.test.tsx @@ -63,13 +63,16 @@ jest.mock('../UserProfileImage/UserProfileImage.component', () => { }); jest.mock('../../../../common/InlineEdit/InlineEdit.component', () => { - return jest.fn().mockImplementation(({ onSave, children }) => ( + return jest.fn().mockImplementation(({ onSave, onCancel, children }) => (
InlineEdit {children} +
)); }); @@ -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 066/117] 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 068/117] [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 && (
diff --git a/openmetadata-ui/src/main/resources/ui/src/context/LimitsProvider/useLimitsStore.ts b/openmetadata-ui/src/main/resources/ui/src/context/LimitsProvider/useLimitsStore.ts index fa344cdad40a..9ecb2e8ab454 100644 --- a/openmetadata-ui/src/main/resources/ui/src/context/LimitsProvider/useLimitsStore.ts +++ b/openmetadata-ui/src/main/resources/ui/src/context/LimitsProvider/useLimitsStore.ts @@ -10,10 +10,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { capitalize, isNil } from 'lodash'; +import { isNil, startCase } from 'lodash'; import { create } from 'zustand'; import { getLimitByResource } from '../../rest/limitsAPI'; +const WARN_SUB_HEADER = "Check the usage of your plan's resource limits."; +const ERROR_SUB_HEADER = + 'You have used {{currentCount}} out of 5 of the Bots resource.'; + export interface ResourceLimit { featureLimitStatuses: Array<{ configuredLimit: { @@ -133,11 +137,14 @@ export const useLimitStore = create<{ setBannerDetails({ header: `You have reached ${ hardLimitExceed ? '100%' : '75%' - } of ${plan} plan Limit in OpenMetadata. `, + } of your ${plan} Plan usage limit.`, type: hardLimitExceed ? 'danger' : 'warning', - subheader: `You have used ${currentCount} out of ${ - limits.hardLimit - } limit for resource ${capitalize(resource)}.`, + subheader: hardLimitExceed + ? ERROR_SUB_HEADER.replace( + '{{currentCount}}', + currentCount + '' + ).replace('{{resource}}', startCase(resource)) + : WARN_SUB_HEADER, softLimitExceed, hardLimitExceed, }); From 44e0e3ce461476ed7e3b25b9722ccd4273a19fbc Mon Sep 17 00:00:00 2001 From: Chira Madlani Date: Sat, 8 Jun 2024 19:49:20 +0530 Subject: [PATCH 076/117] allow force fetch limit --- .../ui/src/context/LimitsProvider/useLimitsStore.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/context/LimitsProvider/useLimitsStore.ts b/openmetadata-ui/src/main/resources/ui/src/context/LimitsProvider/useLimitsStore.ts index 9ecb2e8ab454..5c695b316247 100644 --- a/openmetadata-ui/src/main/resources/ui/src/context/LimitsProvider/useLimitsStore.ts +++ b/openmetadata-ui/src/main/resources/ui/src/context/LimitsProvider/useLimitsStore.ts @@ -79,7 +79,8 @@ export const useLimitStore = create<{ bannerDetails: BannerDetails | null; getResourceLimit: ( resource: string, - showBanner?: boolean + showBanner?: boolean, + force?: boolean ) => Promise; setConfig: (config: LimitConfig) => void; setResourceLimit: ( @@ -106,12 +107,16 @@ export const useLimitStore = create<{ setBannerDetails: (details: BannerDetails | null) => { set({ bannerDetails: details }); }, - getResourceLimit: async (resource: string, showBanner = true) => { + getResourceLimit: async ( + resource: string, + showBanner = true, + force = true + ) => { const { setResourceLimit, resourceLimit, setBannerDetails, config } = get(); let rLimit = resourceLimit[resource]; - if (isNil(rLimit)) { + if (isNil(rLimit) || force) { const limit = await getLimitByResource(resource); setResourceLimit(resource, limit.featureLimitStatuses[0]); From 4d16531965fb0d489a4afdebd45ab5b7f3d1eb5c Mon Sep 17 00:00:00 2001 From: Chira Madlani Date: Sat, 8 Jun 2024 20:29:39 +0530 Subject: [PATCH 077/117] fix ingestion schedule --- .../AddDataQualityTest/TestSuiteIngestion.tsx | 24 +++++++++++++++++-- .../context/LimitsProvider/useLimitsStore.ts | 9 ++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx index d4fc514c544f..9b9cf509a081 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/TestSuiteIngestion.tsx @@ -22,6 +22,7 @@ import { DEPLOYED_PROGRESS_VAL, INGESTION_PROGRESS_END_VAL, } from '../../../constants/constants'; +import { useLimitStore } from '../../../context/LimitsProvider/useLimitsStore'; import { FormSubmitType } from '../../../enums/form.enum'; import { IngestionActionMessage } from '../../../enums/ingestion.enum'; import { @@ -45,9 +46,11 @@ import { replaceAllSpacialCharWith_, Transi18next, } from '../../../utils/CommonUtils'; +import { getScheduleOptionsFromSchedules } from '../../../utils/ScheduleUtils'; import { getIngestionName } from '../../../utils/ServiceUtils'; import { generateUUID } from '../../../utils/StringsUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; +import { getWeekCron } from '../../common/CronEditor/CronEditor.constant'; import SuccessScreen from '../../common/SuccessScreen/SuccessScreen'; import DeployIngestionLoaderModal from '../../Modals/DeployIngestionLoaderModal/DeployIngestionLoaderModal'; import { @@ -78,6 +81,21 @@ const TestSuiteIngestion: React.FC = ({ const [isIngestionCreated, setIsIngestionCreated] = useState(false); const [ingestionProgress, setIngestionProgress] = useState(0); const [isLoading, setIsLoading] = useState(false); + const { config } = useLimitStore(); + + const { pipelineSchedules } = + config?.limits.config.featureLimits.find( + (feature) => feature.name === 'dataQuality' + ) ?? {}; + + const schedulerOptions = useMemo(() => { + if (isEmpty(pipelineSchedules) || !pipelineSchedules) { + return undefined; + } + + return getScheduleOptionsFromSchedules(pipelineSchedules); + }, [pipelineSchedules]); + const getSuccessMessage = useMemo(() => { return ( = ({ const initialFormData = useMemo(() => { return { repeatFrequency: - ingestionPipeline?.airflowConfig.scheduleInterval || - getIngestionFrequency(PipelineType.TestSuite), + ingestionPipeline?.airflowConfig.scheduleInterval ?? config?.enable + ? getWeekCron({ hour: 0, min: 0, dow: 0 }) + : getIngestionFrequency(PipelineType.TestSuite), enableDebugLog: ingestionPipeline?.loggerLevel === LogLevels.Debug, }; }, [ingestionPipeline]); @@ -256,6 +275,7 @@ const TestSuiteIngestion: React.FC = ({ ) : ( Date: Sat, 8 Jun 2024 08:54:23 -0700 Subject: [PATCH 078/117] Revert "Merge branch '1.4.2' into limits" This reverts commit 8e965207a23ba527d0f5ba91463c1869077bf091, reversing changes made to 4d16531965fb0d489a4afdebd45ab5b7f3d1eb5c. --- .../integration/ometa/test_ometa_glossary.py | 4 +- .../service/OpenMetadataApplication.java | 6 +- .../service/events/EventFilter.java | 28 +- .../exception/CatalogExceptionMessage.java | 10 - .../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 + .../service/resources/teams/UserResource.java | 116 ++- .../service/search/indexes/TopicIndex.java | 2 +- .../service/OpenMetadataApplicationTest.java | 18 +- .../service/resources/EntityResourceTest.java | 18 +- .../glossary/GlossaryResourceTest.java | 13 +- .../glossary/GlossaryTermResourceTest.java | 11 +- .../policies/PolicyResourceTest.java | 15 - .../resources/system/SystemResourceTest.java | 94 +- .../resources/teams/PersonaResourceTest.java | 6 +- .../resources/teams/RoleResourceTest.java | 7 - .../resources/teams/TeamResourceTest.java | 7 +- .../resources/teams/UserResourceTest.java | 21 +- .../openmetadata/service/util/TestUtils.java | 3 - .../resources/openmetadata-secure-test.yaml | 16 +- .../org/openmetadata/schema/CreateEntity.java | 4 - .../json/schema/api/data/createGlossary.json | 5 +- .../schema/api/data/createGlossaryTerm.json | 7 +- .../ui/cypress/common/Entities/EntityClass.ts | 12 +- .../ui/cypress/common/GlossaryUtils.ts | 409 +------- .../ui/cypress/common/Utils/Annoucement.ts | 11 +- .../ui/cypress/common/Utils/Entity.ts | 8 +- .../ui/cypress/common/Utils/Users.ts | 9 +- .../ui/cypress/constants/constants.ts | 165 ++++ .../ui/cypress/constants/glossary.constant.ts | 121 --- .../ui/cypress/e2e/Flow/Lineage.spec.ts | 64 -- .../ui/cypress/e2e/Flow/PersonaFlow.spec.ts | 2 +- .../e2e/Pages/Customproperties.spec.ts | 240 ++++- .../ui/cypress/e2e/Pages/Entity.spec.ts | 8 +- .../ui/cypress/e2e/Pages/Glossary.spec.ts | 932 ++++++++++-------- .../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 | 8 +- .../TestSuiteList/TestSuites.test.tsx | 32 +- .../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 +- .../Modals/WhatsNewModal/whatsNewData.ts | 211 +++- .../Settings/Users/Users.component.tsx | 3 +- .../UserProfileDetails.component.tsx | 11 +- .../UserProfileDetails.test.tsx | 27 +- .../UserProfileImage.component.tsx | 3 +- .../UserProfileImage.test.tsx | 4 - .../UserProfileInheritedRoles.component.tsx | 2 - .../UserProfileRoles.component.tsx | 29 +- .../UserProfileRoles.test.tsx | 65 +- .../UserProfileTeams.component.tsx | 17 +- .../UserProfileTeams.test.tsx | 63 +- .../AsyncSelectList/AsyncSelectList.tsx | 1 + .../components/common/Chip/Chip.component.tsx | 25 +- .../components/common/Chip/Chip.interface.ts | 2 - .../src/components/common/Chip/Chip.test.tsx | 86 -- .../ui/src/components/common/Chip/chip.less | 23 - .../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 - .../main/resources/ui/src/mocks/User.mock.ts | 165 ---- .../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 +- .../ui/src/pages/TeamsPage/TeamsPage.test.tsx | 203 ---- .../ui/src/pages/TeamsPage/TeamsPage.tsx | 2 +- .../src/pages/UserPage/UserPage.component.tsx | 14 +- .../ui/src/pages/UserPage/UserPage.test.tsx | 42 +- .../resources/ui/src/styles/variables.less | 1 - .../ui/src/utils/EntityLineageUtils.test.tsx | 4 +- .../ui/src/utils/EntityLineageUtils.tsx | 19 +- .../ui/src/utils/TasksUtils.test.tsx | 73 +- .../main/resources/ui/src/utils/TasksUtils.ts | 36 +- 116 files changed, 1661 insertions(+), 2841 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/Chip/Chip.test.tsx delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/common/Chip/chip.less delete mode 100644 openmetadata-ui/src/main/resources/ui/src/pages/TeamsPage/TeamsPage.test.tsx diff --git a/ingestion/tests/integration/ometa/test_ometa_glossary.py b/ingestion/tests/integration/ometa/test_ometa_glossary.py index 6899bf707862..f86d42bc497c 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(2, len(res_glossary_term.reviewers.__root__)) + self.assertEqual(1, 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(2, len(res_glossary_term.reviewers.__root__)) + self.assertEqual(0, len(res_glossary_term.reviewers.__root__)) def test_patch_glossary_term_synonyms(self): """ diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java index 8166c4886045..b94d38ac67da 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java @@ -172,12 +172,12 @@ public void run(OpenMetadataApplicationConfig catalogConfig, Environment environ // as first step register all the repositories Entity.initializeRepositories(catalogConfig, jdbi); - // Configure the Fernet instance - Fernet.getInstance().setFernetKey(catalogConfig); - // Init Settings Cache after repositories SettingsCache.initialize(catalogConfig); + // Configure the Fernet instance + Fernet.getInstance().setFernetKey(catalogConfig); + initializeWebsockets(catalogConfig, environment); // init Secret Manager diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/events/EventFilter.java b/openmetadata-service/src/main/java/org/openmetadata/service/events/EventFilter.java index 555980fb8ba2..da54682bf744 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/events/EventFilter.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/events/EventFilter.java @@ -13,8 +13,6 @@ package org.openmetadata.service.events; -import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty; - import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -46,26 +44,22 @@ public EventFilter(OpenMetadataApplicationConfig config) { registerEventHandlers(config); } - @SuppressWarnings("unchecked") private void registerEventHandlers(OpenMetadataApplicationConfig config) { - if (!nullOrEmpty(config.getEventHandlerConfiguration())) { + try { Set eventHandlerClassNames = new HashSet<>(config.getEventHandlerConfiguration().getEventHandlerClassNames()); for (String eventHandlerClassName : eventHandlerClassNames) { - try { - EventHandler eventHandler = - ((Class) Class.forName(eventHandlerClassName)) - .getConstructor() - .newInstance(); - eventHandler.init(config); - eventHandlers.add(eventHandler); - LOG.info("Added event handler {}", eventHandlerClassName); - } catch (Exception e) { - LOG.info("Exception ", e); - } + @SuppressWarnings("unchecked") + EventHandler eventHandler = + ((Class) Class.forName(eventHandlerClassName)) + .getConstructor() + .newInstance(); + eventHandler.init(config); + eventHandlers.add(eventHandler); + LOG.info("Added event handler {}", eventHandlerClassName); } - } else { - LOG.info("Event handler configuration is empty"); + } catch (Exception e) { + LOG.info("Exception ", e); } } 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 433abb54f400..ed8d27831827 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 @@ -182,12 +182,6 @@ public static String notAdmin(String name) { return String.format("Principal: CatalogPrincipal{name='%s'} is not admin", name); } - public static String operationNotAllowed(String name, MetadataOperation operation) { - return String.format( - "Principal: CatalogPrincipal{name='%s'} operations [%s] not allowed", - name, operation.value()); - } - public static String notReviewer(String name) { return String.format("User '%s' is not a reviewer", name); } @@ -319,10 +313,6 @@ 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 de4125dcc6cc..2bfa6bc9b464 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,13 +908,6 @@ 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 e492c82d3449..9326ff524905 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,7 +44,6 @@ 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; @@ -472,7 +471,6 @@ 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()); @@ -485,7 +483,6 @@ 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; } @@ -757,7 +754,6 @@ public final void storeRelationshipsInternal(T entity) { applyTags(entity); storeDomain(entity, entity.getDomain()); storeDataProducts(entity, entity.getDataProducts()); - storeReviewers(entity, entity.getReviewers()); storeRelationships(entity); } @@ -858,7 +854,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(updated, patchFields); // Restore inherited fields after a change + setInheritedFields(original, patchFields); // Restore inherited fields after a change } return new PatchResponse<>(Status.OK, withHref(uriInfo, updated), change); } @@ -887,7 +883,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(updated, patchFields); // Restore inherited fields after a change + setInheritedFields(original, patchFields); // Restore inherited fields after a change } return new PatchResponse<>(Status.OK, withHref(uriInfo, updated), change); } @@ -1676,13 +1672,9 @@ public final void deleteRelationship( public final void deleteTo( UUID toId, String toEntityType, Relationship relationship, String fromEntityType) { - if (fromEntityType == null) { - daoCollection.relationshipDAO().deleteTo(toId, toEntityType, relationship.ordinal()); - } else { - daoCollection - .relationshipDAO() - .deleteTo(toId, toEntityType, relationship.ordinal(), fromEntityType); - } + daoCollection + .relationshipDAO() + .deleteTo(toId, toEntityType, relationship.ordinal(), fromEntityType); } public final void deleteFrom( @@ -1707,43 +1699,6 @@ 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) { @@ -1796,7 +1751,7 @@ protected List getChildren(T entity) { protected List getReviewers(T entity) { return supportsReviewers - ? findFrom(entity.getId(), entityType, Relationship.REVIEWS, null) + ? findFrom(entity.getId(), entityType, Relationship.REVIEWS, Entity.USER) : null; } @@ -1830,24 +1785,9 @@ 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) && 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); + if (fields.contains(FIELD_REVIEWERS) && nullOrEmpty(entity.getReviewers()) && parent != null) { + entity.setReviewers(parent.getReviewers()); + listOrEmpty(entity.getReviewers()).forEach(reviewer -> reviewer.withInherited(true)); } } @@ -1887,17 +1827,6 @@ 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)) { @@ -2538,12 +2467,10 @@ protected void updateReviewers() { } List origReviewers = getEntityReferences(original.getReviewers()); List updatedReviewers = getEntityReferences(updated.getReviewers()); - validateReviewers(updatedReviewers); - // Either all users or team which is one team at a time, assuming all ref to have same type, - // validateReviewer checks it + validateUsers(updatedReviewers); updateFromRelationships( "reviewers", - null, + Entity.USER, origReviewers, updatedReviewers, Relationship.REVIEWS, @@ -2819,7 +2746,7 @@ public final void updateFromRelationships( // Add relationships from updated for (EntityReference ref : updatedFromRefs) { - addRelationship(ref.getId(), toId, ref.getType(), toEntityType, relationshipType); + addRelationship(ref.getId(), toId, fromEntityType, 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 2add4f80c006..8b1839e3a01a 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,6 +16,7 @@ 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; @@ -98,7 +99,9 @@ public void clearFields(Glossary glossary, Fields fields) { } @Override - public void prepare(Glossary glossary, boolean update) {} + public void prepare(Glossary glossary, boolean update) { + validateUsers(glossary.getReviewers()); + } @Override public void storeEntity(Glossary glossary, boolean update) { @@ -111,7 +114,10 @@ public void storeEntity(Glossary glossary, boolean update) { @Override public void storeRelationships(Glossary glossary) { - // Nothing to do + for (EntityReference reviewer : listOrEmpty(glossary.getReviewers())) { + addRelationship( + reviewer.getId(), glossary.getId(), Entity.USER, Entity.GLOSSARY, Relationship.REVIEWS); + } } 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 fd82d197aae0..3be4a779d8cb 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,7 +21,6 @@ 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; @@ -64,7 +63,6 @@ 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; @@ -183,6 +181,9 @@ 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 @@ -223,6 +224,10 @@ 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 @@ -693,20 +698,8 @@ private void checkUpdatedByReviewer(GlossaryTerm term, String updatedBy) { boolean isReviewer = reviewers.stream() .anyMatch( - 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); - } - }); + e -> + 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 2b6ce40dbf81..51be38154efd 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 @@ -564,6 +564,7 @@ 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 0eb40a8e7d9c..a1b0e822bda0 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 @@ -687,6 +687,7 @@ 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/main/java/org/openmetadata/service/resources/teams/UserResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java index cddb40d0f7ec..274b295b0de1 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/UserResource.java @@ -30,8 +30,21 @@ import static org.openmetadata.service.util.UserUtil.reSyncUserRolesFromToken; import static org.openmetadata.service.util.UserUtil.validateAndGetRolesRef; +import at.favre.lib.crypto.bcrypt.BCrypt; +import freemarker.template.TemplateException; +import io.dropwizard.jersey.PATCH; +import io.dropwizard.jersey.errors.ErrorMessage; +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import java.io.IOException; -import java.security.AuthProvider; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Base64; @@ -40,7 +53,6 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; - import javax.json.JsonObject; import javax.json.JsonPatch; import javax.json.JsonValue; @@ -63,7 +75,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriInfo; - +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable; import org.openmetadata.common.utils.CommonUtil; @@ -94,11 +106,14 @@ import org.openmetadata.schema.email.SmtpSettings; import org.openmetadata.schema.entity.teams.AuthenticationMechanism; import org.openmetadata.schema.entity.teams.User; +import org.openmetadata.schema.services.connections.metadata.AuthProvider; import org.openmetadata.schema.type.EntityHistory; +import org.openmetadata.schema.type.EntityReference; import org.openmetadata.schema.type.Include; import org.openmetadata.schema.type.MetadataOperation; import org.openmetadata.schema.type.Relationship; import org.openmetadata.schema.type.csv.CsvImportResult; +import org.openmetadata.service.Entity; import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.auth.JwtResponse; import org.openmetadata.service.exception.CatalogExceptionMessage; @@ -108,22 +123,31 @@ import org.openmetadata.service.jdbi3.ListFilter; import org.openmetadata.service.jdbi3.TokenRepository; import org.openmetadata.service.jdbi3.UserRepository; - -import at.favre.lib.crypto.bcrypt.BCrypt; -import freemarker.template.TemplateException; -import io.dropwizard.jersey.PATCH; -import io.dropwizard.jersey.errors.ErrorMessage; -import io.swagger.v3.oas.annotations.ExternalDocumentation; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.ArraySchema; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.parameters.RequestBody; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.extern.slf4j.Slf4j; +import org.openmetadata.service.jdbi3.UserRepository.UserCsv; +import org.openmetadata.service.limits.Limits; +import org.openmetadata.service.resources.Collection; +import org.openmetadata.service.resources.EntityResource; +import org.openmetadata.service.secrets.SecretsManager; +import org.openmetadata.service.secrets.SecretsManagerFactory; +import org.openmetadata.service.secrets.masker.EntityMaskerFactory; +import org.openmetadata.service.security.AuthorizationException; +import org.openmetadata.service.security.Authorizer; +import org.openmetadata.service.security.auth.AuthenticatorHandler; +import org.openmetadata.service.security.auth.BotTokenCache; +import org.openmetadata.service.security.auth.UserTokenCache; +import org.openmetadata.service.security.jwt.JWTTokenGenerator; +import org.openmetadata.service.security.mask.PIIMasker; +import org.openmetadata.service.security.policyevaluator.OperationContext; +import org.openmetadata.service.security.policyevaluator.ResourceContext; +import org.openmetadata.service.security.saml.JwtTokenCacheManager; +import org.openmetadata.service.util.EmailUtil; +import org.openmetadata.service.util.EntityUtil; +import org.openmetadata.service.util.EntityUtil.Fields; +import org.openmetadata.service.util.JsonUtils; +import org.openmetadata.service.util.PasswordUtil; +import org.openmetadata.service.util.RestUtil.PutResponse; +import org.openmetadata.service.util.ResultList; +import org.openmetadata.service.util.TokenUtil; @Slf4j @Path("/v1/users") @@ -537,37 +561,19 @@ public Response createUser( addAuthMechanismToBot(user, create, uriInfo); } - // - try { - validateAndAddUserAuthForBasic(user, create); - } catch (RuntimeException ex) { - return Response.status(CONFLICT) - .type(MediaType.APPLICATION_JSON_TYPE) - .entity( - new ErrorMessage( - CONFLICT.getStatusCode(), CatalogExceptionMessage.ENTITY_ALREADY_EXISTS)) - .build(); - } - - // Add the roles on user creation - updateUserRolesIfRequired(user, containerRequestContext); - - // TODO do we need to authenticate user is creating himself? - Response createdUser = create(uriInfo, securityContext, user); - - // Send Invite mail to user - sendInviteMailToUserForBasicAuth(uriInfo, user, create); - - // Update response to remove auth fields - decryptOrNullify(securityContext, (User) createdUser.getEntity()); - return createdUser; - } - - private void validateAndAddUserAuthForBasic(User user, CreateUser create) { if (isBasicAuth()) { - // basic auth doesn't allow duplicate emails, since username part of the email is used as - // login name - validateEmailAlreadyExists(create.getEmail()); + try { + // basic auth doesn't allow duplicate emails, since username part of the email is used as + // login name + validateEmailAlreadyExists(create.getEmail()); + } catch (RuntimeException ex) { + return Response.status(CONFLICT) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity( + new ErrorMessage( + CONFLICT.getStatusCode(), CatalogExceptionMessage.ENTITY_ALREADY_EXISTS)) + .build(); + } user.setName(user.getEmail().split("@")[0]); if (Boolean.FALSE.equals(create.getIsBot()) && create.getCreatePasswordType() == ADMIN_CREATE) { @@ -575,18 +581,17 @@ private void validateAndAddUserAuthForBasic(User user, CreateUser create) { } // else the user will get a mail if configured smtp } - } - private void updateUserRolesIfRequired( - User user, ContainerRequestContext containerRequestContext) { + // Add the roles on user creation if (Boolean.TRUE.equals(authorizerConfiguration.getUseRolesFromProvider()) && Boolean.FALSE.equals(user.getIsBot() != null && user.getIsBot())) { user.setRoles( validateAndGetRolesRef(getRolesFromAuthorizationToken(containerRequestContext))); } - } - private void sendInviteMailToUserForBasicAuth(UriInfo uriInfo, User user, CreateUser create) { + // TODO do we need to authenticate user is creating himself? + + addHref(uriInfo, repository.create(uriInfo, user)); if (isBasicAuth() && isEmailServiceEnabled) { try { authHandler.sendInviteMailToUser( @@ -599,6 +604,9 @@ private void sendInviteMailToUserForBasicAuth(UriInfo uriInfo, User user, Create LOG.error("Error in sending invite to User" + ex.getMessage()); } } + Response response = Response.created(user.getHref()).entity(user).build(); + decryptOrNullify(securityContext, (User) response.getEntity()); + return response; } private boolean isBasicAuth() { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TopicIndex.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TopicIndex.java index c7ae28ad0eeb..39a8ae458939 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TopicIndex.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TopicIndex.java @@ -19,7 +19,7 @@ import org.openmetadata.service.util.FullyQualifiedName; public class TopicIndex implements SearchIndex { - final Set excludeTopicFields = Set.of("sampleData"); + final Set excludeTopicFields = Set.of("sampleData", "messageSchema"); final Topic topic; public TopicIndex(Topic topic) { diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/OpenMetadataApplicationTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/OpenMetadataApplicationTest.java index 24513fe6dc15..576742e5c727 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/OpenMetadataApplicationTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/OpenMetadataApplicationTest.java @@ -16,15 +16,20 @@ import static java.lang.String.format; import static org.openmetadata.service.util.TablesInitializer.validateAndRunSystemDataMigrations; +import es.org.elasticsearch.client.RestClient; +import es.org.elasticsearch.client.RestClientBuilder; +import io.dropwizard.jersey.jackson.JacksonFeature; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit5.DropwizardAppExtension; import java.net.URI; import java.time.Duration; import java.util.HashSet; import java.util.Set; - import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.WebTarget; - +import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; @@ -45,6 +50,7 @@ import org.openmetadata.common.utils.CommonUtil; import org.openmetadata.schema.service.configuration.elasticsearch.ElasticSearchConfiguration; import org.openmetadata.schema.type.IndexMappingLanguage; +import org.openmetadata.service.fernet.Fernet; import org.openmetadata.service.jdbi3.locator.ConnectionAwareAnnotationSqlLocator; import org.openmetadata.service.jdbi3.locator.ConnectionType; import org.openmetadata.service.resources.CollectionRegistry; @@ -56,14 +62,6 @@ import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; import org.testcontainers.elasticsearch.ElasticsearchContainer; -import es.org.elasticsearch.client.RestClient; -import es.org.elasticsearch.client.RestClientBuilder; -import io.dropwizard.jersey.jackson.JacksonFeature; -import io.dropwizard.testing.ConfigOverride; -import io.dropwizard.testing.ResourceHelpers; -import io.dropwizard.testing.junit5.DropwizardAppExtension; -import lombok.extern.slf4j.Slf4j; - @Slf4j @TestInstance(TestInstance.Lifecycle.PER_CLASS) public abstract class OpenMetadataApplicationTest { diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java index b0e0f88589e9..fc616ab77aa6 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/EntityResourceTest.java @@ -258,7 +258,6 @@ public abstract class EntityResourceTest updateRecords = @@ -472,8 +473,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;%s,team;%s,%s", - user1, user2, team11, "Draft")); + "importExportTest.g1,g11,dsp2,new-dsc11,h1;h3;h3,,,,%s,team;%s,%s", + user1, 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 99eb3534de8a..8233314b30ba 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,6 +35,7 @@ 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.*; @@ -344,7 +345,7 @@ void test_GlossaryTermApprovalWorkflow(TestInfo test) throws IOException { createGlossary = glossaryTest .createRequest(getEntityName(test, 2)) - .withReviewers(listOf(USER1.getEntityReference(), USER2.getEntityReference())); + .withReviewers(listOf(USER1.getFullyQualifiedName(), USER2.getFullyQualifiedName())); Glossary glossary2 = glossaryTest.createEntity(createGlossary, ADMIN_AUTH_HEADERS); // Creating a glossary term g2t1 should be in `Draft` mode (because glossary has reviewers) @@ -867,7 +868,7 @@ public GlossaryTerm createTerm( .withStyle(new Style().withColor("#FF5733").withIconURL("https://img")) .withParent(getFqn(parent)) .withOwner(owner) - .withReviewers(reviewers); + .withReviewers(getFqns(reviewers)); return createAndCheckEntity(createGlossaryTerm, createdBy); } @@ -901,7 +902,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)); + .withReviewers(List.of(USER1_REF.getFullyQualifiedName())); } @Override @@ -927,7 +928,7 @@ public void validateCreatedEntity( } assertEntityReferenceNames(request.getRelatedTerms(), entity.getRelatedTerms()); - assertEntityReferences(request.getReviewers(), entity.getReviewers()); + assertEntityReferenceNames(request.getReviewers(), entity.getReviewers()); // Entity specific validation TestUtils.validateTags(request.getTags(), entity.getTags()); @@ -1151,7 +1152,7 @@ public Glossary createGlossary( public Glossary createGlossary( String name, List reviewers, EntityReference owner) throws IOException { CreateGlossary create = - glossaryTest.createRequest(name).withReviewers(reviewers).withOwner(owner); + glossaryTest.createRequest(name).withReviewers(getFqns(reviewers)).withOwner(owner); return glossaryTest.createAndCheckEntity(create, ADMIN_AUTH_HEADERS); } diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/policies/PolicyResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/policies/PolicyResourceTest.java index 0297e81f6527..98969f897515 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/policies/PolicyResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/policies/PolicyResourceTest.java @@ -104,8 +104,6 @@ public PolicyResourceTest() { } public void setupPolicies() throws IOException { - CREATE_ACCESS_PERMISSION_POLICY = - createEntity(createAccessControlPolicyWithCreateRule(), ADMIN_AUTH_HEADERS); POLICY1 = createEntity(createRequest("policy1").withOwner(null), ADMIN_AUTH_HEADERS); POLICY2 = createEntity(createRequest("policy2").withOwner(null), ADMIN_AUTH_HEADERS); TEAM_ONLY_POLICY = getEntityByName("TeamOnlyPolicy", "", ADMIN_AUTH_HEADERS); @@ -771,19 +769,6 @@ private CreatePolicy createAccessControlPolicyWithRules(String name, List .withOwner(USER1_REF); } - private CreatePolicy createAccessControlPolicyWithCreateRule() { - return new CreatePolicy() - .withName("CreatePermissionPolicy") - .withDescription("Create User Permission") - .withRules( - List.of( - new Rule() - .withName("CreatePermission") - .withResources(List.of(ALL_RESOURCES)) - .withOperations(List.of(MetadataOperation.CREATE)) - .withEffect(ALLOW))); - } - private void validateCondition(String expression) throws HttpResponseException { WebTarget target = getResource(collectionName + "/validation/condition/" + expression); TestUtils.get(target, ADMIN_AUTH_HEADERS); diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/system/SystemResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/system/SystemResourceTest.java index 97e781c09a57..d51440554246 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/system/SystemResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/system/SystemResourceTest.java @@ -1,7 +1,5 @@ package org.openmetadata.service.resources.system; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS; import com.fasterxml.jackson.databind.ObjectMapper; @@ -19,6 +17,7 @@ import javax.ws.rs.core.Response; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.HttpResponseException; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; @@ -52,10 +51,8 @@ import org.openmetadata.schema.type.ColumnDataType; import org.openmetadata.schema.util.EntitiesCount; import org.openmetadata.schema.util.ServicesCount; -import org.openmetadata.service.Entity; import org.openmetadata.service.OpenMetadataApplicationConfig; import org.openmetadata.service.OpenMetadataApplicationTest; -import org.openmetadata.service.fernet.Fernet; import org.openmetadata.service.resources.EntityResourceTest; import org.openmetadata.service.resources.dashboards.DashboardResourceTest; import org.openmetadata.service.resources.databases.TableResourceTest; @@ -164,21 +161,23 @@ void entitiesCount(TestInfo test) throws HttpResponseException { // Ensure counts of entities is increased by 1 EntitiesCount afterCount = getEntitiesCount(); - assertEquals(beforeCount.getDashboardCount() + 1, afterCount.getDashboardCount()); - assertEquals(beforeCount.getPipelineCount() + 1, afterCount.getPipelineCount()); - assertEquals(beforeCount.getServicesCount() + 1, afterCount.getServicesCount()); - assertEquals(beforeCount.getUserCount() + 1, afterCount.getUserCount()); - assertEquals(beforeCount.getTableCount() + 1, afterCount.getTableCount()); - assertEquals(beforeCount.getTeamCount() + 1, afterCount.getTeamCount()); - assertEquals(beforeCount.getTopicCount() + 1, afterCount.getTopicCount()); - assertEquals(beforeCount.getTestSuiteCount() + 1, afterCount.getTestSuiteCount()); - assertEquals(beforeCount.getStorageContainerCount() + 1, afterCount.getStorageContainerCount()); - assertEquals(beforeCount.getGlossaryCount() + 1, afterCount.getGlossaryCount()); - assertEquals(beforeCount.getGlossaryTermCount() + 1, afterCount.getGlossaryTermCount()); + Assertions.assertEquals(beforeCount.getDashboardCount() + 1, afterCount.getDashboardCount()); + Assertions.assertEquals(beforeCount.getPipelineCount() + 1, afterCount.getPipelineCount()); + Assertions.assertEquals(beforeCount.getServicesCount() + 1, afterCount.getServicesCount()); + Assertions.assertEquals(beforeCount.getUserCount() + 1, afterCount.getUserCount()); + Assertions.assertEquals(beforeCount.getTableCount() + 1, afterCount.getTableCount()); + Assertions.assertEquals(beforeCount.getTeamCount() + 1, afterCount.getTeamCount()); + Assertions.assertEquals(beforeCount.getTopicCount() + 1, afterCount.getTopicCount()); + Assertions.assertEquals(beforeCount.getTestSuiteCount() + 1, afterCount.getTestSuiteCount()); + Assertions.assertEquals( + beforeCount.getStorageContainerCount() + 1, afterCount.getStorageContainerCount()); + Assertions.assertEquals(beforeCount.getGlossaryCount() + 1, afterCount.getGlossaryCount()); + Assertions.assertEquals( + beforeCount.getGlossaryTermCount() + 1, afterCount.getGlossaryTermCount()); } @Test - @Order(2) + @Order(1) void testSystemConfigs() throws HttpResponseException { // Test Email Config Settings emailSettings = getSystemConfig(SettingsType.EMAIL_CONFIGURATION); @@ -186,7 +185,7 @@ void testSystemConfigs() throws HttpResponseException { // Password for Email is always sent in hidden SmtpSettings expected = config.getSmtpSettings(); expected.setPassword("***********"); - assertEquals(expected, smtp); + Assertions.assertEquals(expected, smtp); // Test Custom Ui Theme Preference Config Settings uiThemeConfigWrapped = getSystemConfig(SettingsType.CUSTOM_UI_THEME_PREFERENCE); @@ -194,29 +193,13 @@ void testSystemConfigs() throws HttpResponseException { JsonUtils.convertValue(uiThemeConfigWrapped.getConfigValue(), UiThemePreference.class); // Defaults - assertEquals("", uiThemePreference.getCustomTheme().getPrimaryColor()); - assertEquals("", uiThemePreference.getCustomTheme().getSuccessColor()); - assertEquals("", uiThemePreference.getCustomTheme().getErrorColor()); - assertEquals("", uiThemePreference.getCustomTheme().getWarningColor()); - assertEquals("", uiThemePreference.getCustomTheme().getInfoColor()); - assertEquals("", uiThemePreference.getCustomLogoConfig().getCustomLogoUrlPath()); - assertEquals("", uiThemePreference.getCustomLogoConfig().getCustomMonogramUrlPath()); - } - - @Test - @Order(1) - void testDefaultEmailSystemConfig() { - // Test Email Config - Settings stored = - Entity.getCollectionDAO() - .systemDAO() - .getConfigWithKey(SettingsType.EMAIL_CONFIGURATION.value()); - SmtpSettings storedAndEncrypted = - JsonUtils.convertValue(stored.getConfigValue(), SmtpSettings.class); - assertTrue(Fernet.isTokenized(storedAndEncrypted.getPassword())); - assertEquals( - config.getSmtpSettings().getPassword(), - Fernet.getInstance().decryptIfApplies(storedAndEncrypted.getPassword())); + Assertions.assertEquals("", uiThemePreference.getCustomTheme().getPrimaryColor()); + Assertions.assertEquals("", uiThemePreference.getCustomTheme().getSuccessColor()); + Assertions.assertEquals("", uiThemePreference.getCustomTheme().getErrorColor()); + Assertions.assertEquals("", uiThemePreference.getCustomTheme().getWarningColor()); + Assertions.assertEquals("", uiThemePreference.getCustomTheme().getInfoColor()); + Assertions.assertEquals("", uiThemePreference.getCustomLogoConfig().getCustomLogoUrlPath()); + Assertions.assertEquals("", uiThemePreference.getCustomLogoConfig().getCustomMonogramUrlPath()); } @Test @@ -234,8 +217,8 @@ void testSystemConfigsUpdate(TestInfo test) throws HttpResponseException { SmtpSettings updateEmailSettings = JsonUtils.convertValue( getSystemConfig(SettingsType.EMAIL_CONFIGURATION).getConfigValue(), SmtpSettings.class); - assertEquals(updateEmailSettings.getUsername(), test.getDisplayName()); - assertEquals(updateEmailSettings.getEmailingEntity(), test.getDisplayName()); + Assertions.assertEquals(updateEmailSettings.getUsername(), test.getDisplayName()); + Assertions.assertEquals(updateEmailSettings.getEmailingEntity(), test.getDisplayName()); // Test Custom Logo Update and theme preference UiThemePreference updateConfigReq = @@ -260,7 +243,7 @@ void testSystemConfigsUpdate(TestInfo test) throws HttpResponseException { JsonUtils.convertValue( getSystemConfig(SettingsType.CUSTOM_UI_THEME_PREFERENCE).getConfigValue(), UiThemePreference.class); - assertEquals(updateConfigReq, updatedConfig); + Assertions.assertEquals(updateConfigReq, updatedConfig); } @Test @@ -302,11 +285,16 @@ void servicesCount(TestInfo test) throws HttpResponseException { // Get count after creating services and ensure it increased by 1 ServicesCount afterCount = getServicesCount(); - assertEquals(beforeCount.getMessagingServiceCount() + 1, afterCount.getMessagingServiceCount()); - assertEquals(beforeCount.getDashboardServiceCount() + 1, afterCount.getDashboardServiceCount()); - assertEquals(beforeCount.getPipelineServiceCount() + 1, afterCount.getPipelineServiceCount()); - assertEquals(beforeCount.getMlModelServiceCount() + 1, afterCount.getMlModelServiceCount()); - assertEquals(beforeCount.getStorageServiceCount() + 1, afterCount.getStorageServiceCount()); + Assertions.assertEquals( + beforeCount.getMessagingServiceCount() + 1, afterCount.getMessagingServiceCount()); + Assertions.assertEquals( + beforeCount.getDashboardServiceCount() + 1, afterCount.getDashboardServiceCount()); + Assertions.assertEquals( + beforeCount.getPipelineServiceCount() + 1, afterCount.getPipelineServiceCount()); + Assertions.assertEquals( + beforeCount.getMlModelServiceCount() + 1, afterCount.getMlModelServiceCount()); + Assertions.assertEquals( + beforeCount.getStorageServiceCount() + 1, afterCount.getStorageServiceCount()); } @Test @@ -333,7 +321,7 @@ void botUserCountCheck(TestInfo test) throws HttpResponseException { int afterUserCount = getEntitiesCount().getUserCount(); // The bot user count should not be considered. - assertEquals(beforeUserCount, afterUserCount); + Assertions.assertEquals(beforeUserCount, afterUserCount); } @Test @@ -341,7 +329,7 @@ void validate_test() throws HttpResponseException { ValidationResponse response = getValidation(); // Check migrations are OK - assertEquals(Boolean.TRUE, response.getMigrations().getPassed()); + Assertions.assertEquals(Boolean.TRUE, response.getMigrations().getPassed()); } @Test @@ -366,7 +354,7 @@ void globalProfilerConfig(TestInfo test) throws HttpResponseException { createSystemConfig(profilerSettings); ProfilerConfiguration createdProfilerSettings = JsonUtils.convertValue(getProfilerConfig().getConfigValue(), ProfilerConfiguration.class); - assertEquals(profilerConfiguration, createdProfilerSettings); + Assertions.assertEquals(profilerConfiguration, createdProfilerSettings); // Update the profiler config profilerConfiguration.setMetricConfiguration(List.of(intMetricConfigDefinition)); @@ -377,7 +365,7 @@ void globalProfilerConfig(TestInfo test) throws HttpResponseException { updateSystemConfig(profilerSettings); ProfilerConfiguration updatedProfilerSettings = JsonUtils.convertValue(getProfilerConfig().getConfigValue(), ProfilerConfiguration.class); - assertEquals(profilerConfiguration, updatedProfilerSettings); + Assertions.assertEquals(profilerConfiguration, updatedProfilerSettings); // Delete the profiler config profilerConfiguration.setMetricConfiguration(new ArrayList<>()); @@ -387,7 +375,7 @@ void globalProfilerConfig(TestInfo test) throws HttpResponseException { .withConfigValue(profilerConfiguration)); updatedProfilerSettings = JsonUtils.convertValue(getProfilerConfig().getConfigValue(), ProfilerConfiguration.class); - assertEquals(profilerConfiguration, updatedProfilerSettings); + Assertions.assertEquals(profilerConfiguration, updatedProfilerSettings); } private static ValidationResponse getValidation() throws HttpResponseException { diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/PersonaResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/PersonaResourceTest.java index 42bfb0e62188..538145851308 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/PersonaResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/PersonaResourceTest.java @@ -85,11 +85,9 @@ void post_teamWithUsersAndDefaultRoles_200_OK(TestInfo test) throws IOException // Add team to user relationships while creating a team UserResourceTest userResourceTest = new UserResourceTest(); User user1 = - userResourceTest.createEntity( - userResourceTest.createRequest(test, 1), USER_WITH_CREATE_HEADERS); + userResourceTest.createEntity(userResourceTest.createRequest(test, 1), TEST_AUTH_HEADERS); User user2 = - userResourceTest.createEntity( - userResourceTest.createRequest(test, 2), USER_WITH_CREATE_HEADERS); + userResourceTest.createEntity(userResourceTest.createRequest(test, 2), TEST_AUTH_HEADERS); List users = Arrays.asList(user1.getId(), user2.getId()); CreatePersona create = diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/RoleResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/RoleResourceTest.java index a367e397e6cc..4893ae45ec98 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/RoleResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/RoleResourceTest.java @@ -74,13 +74,6 @@ public void setupRoles(TestInfo test) throws HttpResponseException { ROLE1 = createEntity(createRequest(test), ADMIN_AUTH_HEADERS); ROLE1_REF = ROLE1.getEntityReference(); - - CREATE_ACCESS_ROLE = - createEntity( - new CreateRole() - .withName("CreateAccessRole") - .withPolicies(List.of(CREATE_ACCESS_PERMISSION_POLICY.getFullyQualifiedName())), - ADMIN_AUTH_HEADERS); } /** Creates the given number of roles */ diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/TeamResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/TeamResourceTest.java index d533d2bfb3a1..ffe71d5f4ca8 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/TeamResourceTest.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/teams/TeamResourceTest.java @@ -50,7 +50,6 @@ import static org.openmetadata.service.util.TestUtils.ADMIN_AUTH_HEADERS; import static org.openmetadata.service.util.TestUtils.TEST_AUTH_HEADERS; import static org.openmetadata.service.util.TestUtils.TEST_USER_NAME; -import static org.openmetadata.service.util.TestUtils.USER_WITH_CREATE_HEADERS; import static org.openmetadata.service.util.TestUtils.UpdateType.CHANGE_CONSOLIDATED; import static org.openmetadata.service.util.TestUtils.UpdateType.MINOR_UPDATE; import static org.openmetadata.service.util.TestUtils.UpdateType.NO_CHANGE; @@ -196,11 +195,9 @@ void post_teamWithUsersAndDefaultRoles_200_OK(TestInfo test) throws IOException // Add team to user relationships while creating a team UserResourceTest userResourceTest = new UserResourceTest(); User user1 = - userResourceTest.createEntity( - userResourceTest.createRequest(test, 1), USER_WITH_CREATE_HEADERS); + userResourceTest.createEntity(userResourceTest.createRequest(test, 1), TEST_AUTH_HEADERS); User user2 = - userResourceTest.createEntity( - userResourceTest.createRequest(test, 2), USER_WITH_CREATE_HEADERS); + userResourceTest.createEntity(userResourceTest.createRequest(test, 2), TEST_AUTH_HEADERS); List users = Arrays.asList(user1.getId(), user2.getId()); RoleResourceTest roleResourceTest = new RoleResourceTest(); 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 d30d76a21be5..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 @@ -41,7 +41,6 @@ import static org.openmetadata.service.exception.CatalogExceptionMessage.PASSWORD_INVALID_FORMAT; import static org.openmetadata.service.exception.CatalogExceptionMessage.entityNotFound; import static org.openmetadata.service.exception.CatalogExceptionMessage.notAdmin; -import static org.openmetadata.service.exception.CatalogExceptionMessage.operationNotAllowed; import static org.openmetadata.service.exception.CatalogExceptionMessage.permissionNotAllowed; import static org.openmetadata.service.resources.teams.UserResource.USER_PROTECTED_FIELDS; import static org.openmetadata.service.security.SecurityUtil.authHeaders; @@ -52,8 +51,6 @@ import static org.openmetadata.service.util.TestUtils.INGESTION_BOT; import static org.openmetadata.service.util.TestUtils.TEST_AUTH_HEADERS; import static org.openmetadata.service.util.TestUtils.TEST_USER_NAME; -import static org.openmetadata.service.util.TestUtils.USER_WITH_CREATE_HEADERS; -import static org.openmetadata.service.util.TestUtils.USER_WITH_CREATE_PERMISSION_NAME; import static org.openmetadata.service.util.TestUtils.UpdateType.CHANGE_CONSOLIDATED; import static org.openmetadata.service.util.TestUtils.UpdateType.MINOR_UPDATE; import static org.openmetadata.service.util.TestUtils.UpdateType.REVERT; @@ -156,14 +153,6 @@ public UserResourceTest() { } public void setupUsers(TestInfo test) throws HttpResponseException { - CreateUser createUserWithAccess = - new CreateUser() - .withName(USER_WITH_CREATE_PERMISSION_NAME) - .withEmail(USER_WITH_CREATE_PERMISSION_NAME + "@open-metadata.org") - .withProfile(PROFILE) - .withRoles(List.of(CREATE_ACCESS_ROLE.getId())) - .withIsBot(false); - USER_WITH_CREATE_ACCESS = createEntity(createUserWithAccess, ADMIN_AUTH_HEADERS); CreateUser create = createRequest(test).withRoles(List.of(DATA_CONSUMER_ROLE.getId())); USER1 = createEntity(create, ADMIN_AUTH_HEADERS); USER1_REF = USER1.getEntityReference(); @@ -328,9 +317,7 @@ void post_validAdminUser_Non_Admin_401(TestInfo test) { .withIsAdmin(true); assertResponse( - () -> createAndCheckEntity(create, TEST_AUTH_HEADERS), - FORBIDDEN, - operationNotAllowed(TEST_USER_NAME, MetadataOperation.CREATE)); + () -> createAndCheckEntity(create, TEST_AUTH_HEADERS), FORBIDDEN, notAdmin(TEST_USER_NAME)); } @Test @@ -626,7 +613,7 @@ void patch_makeAdmin_as_nonAdmin_user_401(TestInfo test) throws HttpResponseExce User user = createEntity( createRequest(test, 6).withName("test2").withEmail("test2@email.com"), - USER_WITH_CREATE_HEADERS); + authHeaders("test2@email.com")); String userJson = JsonUtils.pojoToJson(user); user.setIsAdmin(Boolean.TRUE); assertResponse( @@ -884,7 +871,7 @@ void put_generateToken_bot_user_200_ok() throws HttpResponseException { .withEmail("ingestion-bot-jwt@email.com") .withRoles(List.of(ROLE1_REF.getId())) .withAuthenticationMechanism(authMechanism); - User user = createEntity(create, USER_WITH_CREATE_HEADERS); + User user = createEntity(create, authHeaders("ingestion-bot-jwt@email.com")); user = getEntity(user.getId(), "*", ADMIN_AUTH_HEADERS); assertEquals(1, user.getRoles().size()); TestUtils.put( @@ -935,7 +922,7 @@ void post_createUser_BasicAuth_AdminCreate_login_200_ok(TestInfo test) .withCreatePasswordType(CreateUser.CreatePasswordType.ADMIN_CREATE) .withPassword("Test@1234") .withConfirmPassword("Test@1234"), - USER_WITH_CREATE_HEADERS); + authHeaders("testBasicAuth@email.com")); // jwtAuth Response should be null always user = getEntity(user.getId(), ADMIN_AUTH_HEADERS); diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/util/TestUtils.java b/openmetadata-service/src/test/java/org/openmetadata/service/util/TestUtils.java index 49dd4f7d02c7..cad0733a838c 100644 --- a/openmetadata-service/src/test/java/org/openmetadata/service/util/TestUtils.java +++ b/openmetadata-service/src/test/java/org/openmetadata/service/util/TestUtils.java @@ -104,9 +104,6 @@ public final class TestUtils { public static final String TEST_USER_NAME = "test"; public static final Map TEST_AUTH_HEADERS = authHeaders(TEST_USER_NAME + "@open-metadata.org"); - public static final String USER_WITH_CREATE_PERMISSION_NAME = "testWithCreateUserPermission"; - public static final Map USER_WITH_CREATE_HEADERS = - authHeaders(USER_WITH_CREATE_PERMISSION_NAME + "@open-metadata.org"); public static final UUID NON_EXISTENT_ENTITY = UUID.randomUUID(); diff --git a/openmetadata-service/src/test/resources/openmetadata-secure-test.yaml b/openmetadata-service/src/test/resources/openmetadata-secure-test.yaml index 9b19ff8423fd..1a6027d3c884 100644 --- a/openmetadata-service/src/test/resources/openmetadata-secure-test.yaml +++ b/openmetadata-service/src/test/resources/openmetadata-secure-test.yaml @@ -215,18 +215,16 @@ pipelineServiceClientConfiguration: email: enableSmtpServer : false - emailingEntity: "openmetadata" - supportUrl: "http://localhost:8585" + emailingEntity: "" + supportUrl: "" openMetadataUrl: "http://localhost:8585" - senderMail: "test@openmetadata.org" - serverEndpoint: "test.smtp.com" - serverPort: "584" - username: "test" - password: "test" + senderMail: "" + serverEndpoint: "" + serverPort: "" + username: "" + password: "" transportationStrategy: "SMTP_TLS" -fernetConfiguration: - fernetKey: ${FERNET_KEY:-ihZpp5gmmDvVsgoOG6OVivKWwC9vd5JQ} dataQualityConfiguration: severityIncidentClassifier: "org.openmetadata.service.util.incidentSeverityClassifier.LogisticRegressionIncidentSeverityClassifier" 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 e5875156469d..14aaf7c33c03 100644 --- a/openmetadata-spec/src/main/java/org/openmetadata/schema/CreateEntity.java +++ b/openmetadata-spec/src/main/java/org/openmetadata/schema/CreateEntity.java @@ -31,10 +31,6 @@ 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 cf94947a50c8..cdc288d72ecc 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,7 +22,10 @@ }, "reviewers": { "description": "User references of the reviewers for this glossary.", - "$ref": "../../type/entityReferenceList.json" + "type": "array", + "items": { + "$ref": "../../type/basic.json#/definitions/entityName" + } }, "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 4c7e707283f7..09b84822d2fc 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,8 +53,11 @@ } }, "reviewers": { - "description": "User or Team references of the reviewers for this glossary.", - "$ref": "../../type/entityReferenceList.json" + "description": "User names of the reviewers for this glossary.", + "type" : "array", + "items" : { + "$ref" : "../../type/basic.json#/definitions/entityName" + } }, "owner": { "description": "Owner of this glossary term.", diff --git a/openmetadata-ui/src/main/resources/ui/cypress/common/Entities/EntityClass.ts b/openmetadata-ui/src/main/resources/ui/cypress/common/Entities/EntityClass.ts index c3d51bce48b1..98dcc2a88191 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/common/Entities/EntityClass.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/common/Entities/EntityClass.ts @@ -449,14 +449,10 @@ class EntityClass { // Announcement createAnnouncement() { - createAnnouncementUtil( - { - title: 'Cypress announcement', - description: 'Cypress announcement description', - }, - this.entityName, - this.name - ); + createAnnouncementUtil({ + title: 'Cypress announcement', + description: 'Cypress announcement description', + }); } replyAnnouncement() { 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 c64e077f181d..ddc624c4e795 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/common/GlossaryUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/common/GlossaryUtils.ts @@ -11,68 +11,48 @@ * limitations under the License. */ -import { - DELETE_TERM, - INVALID_NAMES, - NAME_MAX_LENGTH_VALIDATION_ERROR, - NAME_VALIDATION_ERROR, -} from '../constants/constants'; +import { DELETE_TERM } from '../constants/constants'; import { SidebarItem } from '../constants/Entity.interface'; import { - descriptionBox, interceptURL, toastNotification, verifyResponseStatusCode, } from './common'; -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'); +export const visitGlossaryPage = () => { + interceptURL('GET', '/api/v1/glossaries?fields=*', 'getGlossaries'); - // 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); + cy.sidebarClick(SidebarItem.GLOSSARY); - // 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); + verifyResponseStatusCode('@getGlossaries', 200); }; -export const selectActiveGlossary = (glossaryName) => { - interceptURL('GET', '/api/v1/glossaryTerms*', 'getGlossaryTerms'); - cy.get('.ant-menu-item').contains(glossaryName).click(); - verifyResponseStatusCode('@getGlossaryTerms', 200); -}; +export const addReviewer = (reviewerName, entity) => { + interceptURL('GET', '/api/v1/users?limit=25&isBot=false', 'getUsers'); -export const checkDisplayName = (displayName) => { - cy.get('[data-testid="entity-header-display-name"]') - .filter(':visible') - .scrollIntoView() - .within(() => { - cy.contains(displayName); - }); -}; + cy.get('[data-testid="glossary-reviewer"] [data-testid="Add"]').click(); -export const visitGlossaryPage = () => { - interceptURL('GET', '/api/v1/glossaries?fields=*', 'getGlossaries'); + verifyResponseStatusCode('@getUsers', 200); - cy.sidebarClick(SidebarItem.GLOSSARY); + interceptURL( + 'GET', + `api/v1/search/query?q=*${encodeURI(reviewerName)}*`, + 'searchOwner' + ); - verifyResponseStatusCode('@getGlossaries', 200); + cy.get('[data-testid="searchbar"]').type(reviewerName); + + verifyResponseStatusCode('@searchOwner', 200); + + interceptURL('PATCH', `/api/v1/${entity}/*`, 'patchOwner'); + + cy.get(`.ant-popover [title="${reviewerName}"]`).click(); + + cy.get('[data-testid="selectable-list-update-btn"]').click(); + + verifyResponseStatusCode('@patchOwner', 200); + + cy.get('[data-testid="glossary-reviewer"]').should('contain', reviewerName); }; export const removeReviewer = (entity) => { @@ -115,338 +95,3 @@ 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/Annoucement.ts b/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Annoucement.ts index 4e60726382c1..468d2592dc70 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Annoucement.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Annoucement.ts @@ -36,7 +36,7 @@ const announcementForm = ({ title, description, startDate, endDate }) => { cy.get('[id="announcement-submit"]').scrollIntoView().click(); }; -export const createAnnouncement = (announcement, entityName, updatedName) => { +export const createAnnouncement = (announcement) => { interceptURL( 'GET', '/api/v1/feed?entityLink=*type=Announcement', @@ -75,15 +75,6 @@ export const createAnnouncement = (announcement, entityName, updatedName) => { announcement.title ); cy.goToHomePage(); - - cy.get('[data-testid="announcement-container"]') - .find(`a[href*="${encodeURIComponent(entityName)}"]`) - .click(); - - cy.get('[data-testid="entity-header-display-name"]').should( - 'contain', - `Cypress ${updatedName} updated` - ); }; export const deleteAnnouncement = () => { 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 3885263d6518..dee9c4c72a35 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,6 +352,9 @@ export const deletedEntityCommonChecks = ({ if (isTableEntity) { checkLineageTabActions({ deleted }); + } + + if (isTableEntity) { checkForTableSpecificFields({ deleted }); } @@ -392,7 +395,6 @@ export const deletedEntityCommonChecks = ({ '[data-testid="manage-dropdown-list-container"] [data-testid="delete-button"]' ).should('be.visible'); } - cy.clickOutside(); }; @@ -458,9 +460,7 @@ export const deleteEntity = ( 'getDatabaseSchemas' ); - cy.get('[data-testid="entity-page-header"] [data-testid="breadcrumb-link"]') - .last() - .click(); + cy.get('[data-testid="breadcrumb-link"]:last-child').click({ force: true }); verifyResponseStatusCode('@getDatabaseSchemas', 200); cy.get('[data-testid="show-deleted"]') diff --git a/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Users.ts b/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Users.ts index 55831d823087..a8ce674160e9 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Users.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/common/Utils/Users.ts @@ -245,9 +245,9 @@ export const editTeams = (teamName: string) => { cy.get('[data-testid="inline-save-btn"]').click({ timeout: 10000 }); verifyResponseStatusCode('@updateTeams', 200); - cy.get(`[data-testid="${teamName}-link"]`) - .scrollIntoView() - .should('be.visible'); + cy.get('.ant-collapse-expand-icon > .anticon > svg').click(); + cy.get('.page-layout-v1-vertical-scroll').scrollTo(0, 0); + cy.get(`[data-testid="${teamName}"]`).should('be.visible'); }; export const handleUserUpdateDetails = ( @@ -284,7 +284,8 @@ export const handleAdminUpdateDetails = ( editDisplayName(editedUserName); // edit teams - cy.get('.ant-collapse-expand-icon > .anticon > svg').scrollIntoView().click(); + cy.get('.ant-collapse-expand-icon > .anticon > svg').scrollIntoView(); + cy.get('.ant-collapse-expand-icon > .anticon > svg').click(); editTeams(teamName); // edit description 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 1a87b37bfbe5..5f86e4e96ca3 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/constants/constants.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/constants/constants.ts @@ -305,6 +305,171 @@ 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 e4b1cd39c518..964f5c964b09 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,124 +36,3 @@ 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/Lineage.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/Lineage.spec.ts index 07e3216d1612..d5cd9baecb50 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/Lineage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/Lineage.spec.ts @@ -137,35 +137,6 @@ const applyPipelineFromModal = (fromNode, toNode, pipelineData) => { verifyResponseStatusCode('@lineageApi', 200); }; -const editPipelineEdgeDescription = ( - fromNode, - toNode, - pipelineData, - description -) => { - cy.get( - `[data-testid="pipeline-label-${fromNode.fqn}-${toNode.fqn}"]` - ).click(); - cy.get('.edge-info-drawer').should('be.visible'); - cy.get('.edge-info-drawer [data-testid="Edge"] a').contains( - pipelineData.name - ); - - interceptURL('PUT', `/api/v1/lineage`, 'updateLineage'); - cy.get('.edge-info-drawer [data-testid="edit-description"]').click(); - - cy.get('.toastui-editor-md-container > .toastui-editor > .ProseMirror') - .click() - .clear() - .type(description); - - cy.get('[data-testid="save"]').click(); - verifyResponseStatusCode('@updateLineage', 200); - cy.get( - '.edge-info-drawer [data-testid="asset-description-container"] [data-testid="viewer-container"]' - ).should('contain', description); -}; - const verifyPipelineDataInDrawer = ( fromNode, toNode, @@ -260,29 +231,6 @@ const addColumnLineage = (fromNode, toNode, exitEditMode = true) => { ); }; -const removeColumnLineage = (fromNode, toNode) => { - interceptURL('PUT', '/api/v1/lineage', 'lineageApi'); - cy.get( - `[data-testid="column-edge-${btoa(fromNode.columns[0])}-${btoa( - toNode.columns[0] - )}"]` - ).click({ force: true }); - cy.get('[data-testid="delete-button"]').click({ force: true }); - cy.get( - '[data-testid="delete-edge-confirmation-modal"] .ant-btn-primary' - ).click(); - - verifyResponseStatusCode('@lineageApi', 200); - - cy.get('[data-testid="edit-lineage"]').click(); - - cy.get( - `[data-testid="column-edge-${btoa(fromNode.columns[0])}-${btoa( - toNode.columns[0] - )}"]` - ).should('not.exist'); -}; - describe('Lineage verification', { tags: 'DataAssets' }, () => { beforeEach(() => { cy.login(); @@ -375,14 +323,6 @@ describe('Lineage verification', { tags: 'DataAssets' }, () => { PIPELINE_ITEMS[0], true ); - - editPipelineEdgeDescription( - sourceEntity, - targetEntity, - PIPELINE_ITEMS[0], - 'Test Description' - ); - cy.get('[data-testid="edit-lineage"]').click(); deleteNode(targetEntity); }); @@ -396,10 +336,6 @@ describe('Lineage verification', { tags: 'DataAssets' }, () => { activateColumnLayer(); // Add column lineage addColumnLineage(sourceEntity, targetEntity); - - cy.get('[data-testid="edit-lineage"]').click(); - removeColumnLineage(sourceEntity, targetEntity); - cy.get('[data-testid="edit-lineage"]').click(); deleteNode(targetEntity); cy.goToHomePage(); 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 23b21de5ff47..07b5fa9c4799 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(`.ant-popover [title="${userSearchText}"]`).click(); + cy.get(`[title="${userSearchText}"] .ant-checkbox-input`).check(); 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 961f4069dddb..fa181bdf4487 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 } from 'lodash'; -import { interceptURL, verifyResponseStatusCode } from '../../common/common'; +import { lowerCase, omit } from 'lodash'; import { - createGlossary, - createGlossaryTerms, - deleteGlossary, -} from '../../common/GlossaryUtils'; + descriptionBox, + interceptURL, + verifyResponseStatusCode, +} from '../../common/common'; +import { deleteGlossary } from '../../common/GlossaryUtils'; import { addCustomPropertiesForEntity, customPropertiesArray, @@ -37,10 +37,17 @@ import { visitEntityDetailsPage, } from '../../common/Utils/Entity'; import { getToken } from '../../common/Utils/LocalStorage'; -import { ENTITIES, uuid } from '../../constants/constants'; +import { + ENTITIES, + INVALID_NAMES, + NAME_MAX_LENGTH_VALIDATION_ERROR, + NAME_VALIDATION_ERROR, + NEW_GLOSSARY, + NEW_GLOSSARY_TERMS, + 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', @@ -74,6 +81,199 @@ 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(); @@ -479,8 +679,6 @@ 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`, () => { @@ -502,11 +700,17 @@ 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(GLOSSARY_1, false); - createGlossaryTerms(glossary); + createGlossary({ + ...omit(NEW_GLOSSARY, ['reviewer']), + addReviewer: false, + }); + createGlossaryTerm( + NEW_GLOSSARY_TERMS.term_1, + NEW_GLOSSARY, + 'Approved', + true + ); cy.settingClick(glossaryTerm.entityApiType, true); @@ -521,10 +725,10 @@ describe('Custom Properties should work properly', { tags: 'Settings' }, () => { }); visitEntityDetailsPage({ - term: glossary.terms[0].name, - serviceName: glossary.terms[0].fullyQualifiedName, + term: NEW_GLOSSARY_TERMS.term_1.name, + serviceName: NEW_GLOSSARY_TERMS.term_1.fullyQualifiedName, entity: 'glossaryTerms' as EntityType, - dataTestId: `${glossary.name}-${glossary.terms[0].name}`, + dataTestId: `${NEW_GLOSSARY.name}-${NEW_GLOSSARY_TERMS.term_1.name}`, }); // set custom property value @@ -571,7 +775,7 @@ describe('Custom Properties should work properly', { tags: 'Settings' }, () => { // delete glossary and glossary term cy.sidebarClick(SidebarItem.GLOSSARY); - deleteGlossary(glossary.name); + deleteGlossary(NEW_GLOSSARY.name); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Entity.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Entity.spec.ts index 871e2ed32379..929ed879d3a1 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Entity.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Entity.spec.ts @@ -106,10 +106,6 @@ describe('Entity detail page', { tags: 'DataAssets' }, () => { entity.removeGlossary(); }); - it(`Update displayName`, () => { - entity.renameEntity(); - }); - it(`Announcement create & delete`, () => { entity.createAnnouncement(); entity.replyAnnouncement(); @@ -149,6 +145,10 @@ describe('Entity detail page', { tags: 'DataAssets' }, () => { }); } + it(`Update displayName`, () => { + entity.renameEntity(); + }); + it(`follow unfollow entity`, () => { entity.followUnfollowEntity(); }); 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 e1ecde54c469..d295c17429c5 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,15 +18,7 @@ import { verifyMultipleResponseStatusCode, verifyResponseStatusCode, } from '../../common/common'; -import { - addOwnerInGlossary, - checkDisplayName, - createGlossary, - createGlossaryTerms, - deleteGlossary, - selectActiveGlossary, - verifyGlossaryDetails, -} from '../../common/GlossaryUtils'; +import { deleteGlossary } from '../../common/GlossaryUtils'; import { dragAndDropElement } from '../../common/Utils/DragAndDrop'; import { visitEntityDetailsPage } from '../../common/Utils/Entity'; import { confirmationDragAndDropGlossary } from '../../common/Utils/Glossary'; @@ -36,30 +28,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 { 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'; +import { SidebarItem } from '../../constants/Entity.interface'; +import { GLOSSARY_OWNER_LINK_TEST_ID } from '../../constants/glossary.constant'; const CREDENTIALS = generateRandomUser(); const userName = `${CREDENTIALS.firstName}${CREDENTIALS.lastName}`; -const CREDENTIALS_2 = generateRandomUser(); -const userName2 = `${CREDENTIALS_2.firstName}${CREDENTIALS_2.lastName}`; - let createdUserId = ''; -let createdUserId_2 = ''; const selectOwner = (ownerName: string, dataTestId?: string) => { interceptURL('GET', '/api/v1/users?*isBot=false*', 'getUsers'); @@ -113,12 +105,208 @@ 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); @@ -178,6 +366,51 @@ 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); @@ -223,6 +456,12 @@ 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() @@ -251,6 +490,31 @@ 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', @@ -482,14 +746,6 @@ 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); - }); }); }; @@ -522,6 +778,7 @@ 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', @@ -530,51 +787,6 @@ 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; }); }); }); @@ -591,84 +803,158 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { }); it('Create new glossary flow should work properly', () => { - createGlossary(GLOSSARY_1, true); - createGlossary(GLOSSARY_2, false); - createGlossary(GLOSSARY_3, false); - verifyGlossaryDetails(GLOSSARY_1); - verifyGlossaryDetails(GLOSSARY_2); - verifyGlossaryDetails(GLOSSARY_3); + createGlossary(NEW_GLOSSARY, true); + createGlossary(NEW_GLOSSARY_1, false); }); - it('Glossary Owner Flow', () => { + it('Assign Owner', () => { cy.get('[data-testid="glossary-left-panel"]') - .contains(GLOSSARY_1.name) + .contains(NEW_GLOSSARY.name) .click(); - checkDisplayName(GLOSSARY_1.name); + checkDisplayName(NEW_GLOSSARY.name); addOwner(userName, GLOSSARY_OWNER_LINK_TEST_ID); - cy.reload(); + }); + + it('Update Owner', () => { + cy.get('[data-testid="glossary-left-panel"]') + .contains(NEW_GLOSSARY.name) + .click(); + + checkDisplayName(NEW_GLOSSARY.name); addOwner('Alex Pollard', GLOSSARY_OWNER_LINK_TEST_ID); - cy.reload(); + }); + + it('Remove Owner', () => { + cy.get('[data-testid="glossary-left-panel"]') + .contains(NEW_GLOSSARY.name) + .click(); + + checkDisplayName(NEW_GLOSSARY.name); removeOwner('Alex Pollard', GLOSSARY_OWNER_LINK_TEST_ID); }); - it('Create glossary term should work properly', () => { - createGlossaryTerms(GLOSSARY_1); - createGlossaryTerms(GLOSSARY_2); - createGlossaryTerms(GLOSSARY_3); + it('Verify and Remove Tags from Glossary', () => { + cy.get('[data-testid="glossary-left-panel"]') + .contains(NEW_GLOSSARY.name) + .click(); - verifyStatusFilterInExplore('Approved'); - verifyStatusFilterInExplore('Draft'); + 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(); + + 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'); }); - it('Updating data of glossary should work properly', () => { - selectActiveGlossary(GLOSSARY_1.name); + it('Verify added glossary details', () => { + cy.get('[data-testid="glossary-left-panel"]') + .contains(NEW_GLOSSARY.name) + .click(); - // 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 - ); + checkDisplayName(NEW_GLOSSARY.name); - // updating tags - removeTags(GLOSSARY_1.tag, EntityType.Glossary); - assignTags('PII.None', EntityType.Glossary); + cy.get('[data-testid="viewer-container"]') + .invoke('text') + .then((text) => { + expect(text).to.contain(NEW_GLOSSARY.description); + }); - // updating description - updateDescription('Updated description', true); + cy.get(`[data-testid="glossary-reviewer-name"]`) + .invoke('text') + .then((text) => { + expect(text).to.contain(userName); + }); - voteGlossary(true); + // 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); + + checkDisplayName(NEW_GLOSSARY_1.name); + cy.get('[data-testid="viewer-container"]') + .invoke('text') + .then((text) => { + expect(text).to.contain(NEW_GLOSSARY_1.description); + }); }); - it('Team Approval Workflow for Glossary Term', () => { + 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'); + }); + + // Glossary term for Product glossary + selectActiveGlossary(NEW_GLOSSARY_1.name); + + 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'); + }); + + it('Approval Workflow for Glossary Term', () => { cy.logout(); + cy.login(CREDENTIALS.email, CREDENTIALS.password); approveGlossaryTermWorkflow({ - glossary: GLOSSARY_2, - glossaryTerm: GLOSSARY_2.terms[0], + glossary: NEW_GLOSSARY, + glossaryTerm: NEW_GLOSSARY_TERMS.term_1, }); approveGlossaryTermWorkflow({ - glossary: GLOSSARY_2, - glossaryTerm: GLOSSARY_2.terms[1], + glossary: NEW_GLOSSARY, + glossaryTerm: NEW_GLOSSARY_TERMS.term_2, }); 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 = GLOSSARY_3.terms[1].name; - const { name, fullyQualifiedName } = GLOSSARY_1.terms[0]; - const { name: newTermName, fullyQualifiedName: newTermFqn } = - GLOSSARY_1.terms[1]; + const term2 = NEW_GLOSSARY_TERMS.term_2.name; + const { name, fullyQualifiedName } = NEW_GLOSSARY_1_TERMS.term_1; // visit glossary page interceptURL( @@ -678,7 +964,7 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { ); interceptURL('GET', `/api/v1/permissions/glossary/*`, 'permissions'); - cy.get('.ant-menu-item').contains(GLOSSARY_1.name).click(); + cy.get('.ant-menu-item').contains(NEW_GLOSSARY_1.name).click(); verifyMultipleResponseStatusCode(['@glossaryTerm', '@permissions'], 200); // visit glossary term page @@ -701,149 +987,13 @@ 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(GLOSSARY_1.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 reviewer of the glossary term - cy.get( - '[data-testid="select-assignee"] > .ant-select-selector > .ant-select-selection-overflow' - ).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() - .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); - - // 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(); - - verifyResponseStatusCode('@taskResolve', 200); - - cy.reload(); - - cy.get('[data-testid="glossary-left-panel"]') - .contains(GLOSSARY_1.name) - .click(); - - checkDisplayName(GLOSSARY_1.name); - - 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) + .contains(NEW_GLOSSARY_1.name) .click(); interceptURL( @@ -856,18 +1006,10 @@ 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 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); - } - }); - + // check assignees for task which will be owner of the glossary term cy.get( '[data-testid="select-assignee"] > .ant-select-selector > .ant-select-selection-overflow' - ).should('not.contain', GLOSSARY_2.owner); + ).should('contain', 'admin'); cy.get('[data-testid="tag-selector"]') .click() @@ -884,22 +1026,6 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { 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(); @@ -908,71 +1034,36 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { cy.reload(); cy.get('[data-testid="glossary-left-panel"]') - .contains(GLOSSARY_2.name) + .contains(NEW_GLOSSARY_1.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(); + checkDisplayName(NEW_GLOSSARY_1.name); - 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); + // Verify Tags which is added at the time of creating glossary + cy.get('[data-testid="tags-container"]') + .contains('Personal') + .should('be.visible'); }); it('Assets Tab should work properly', () => { - const glossary1 = GLOSSARY_1.name; - const term1 = GLOSSARY_1.terms[0]; - const term2 = GLOSSARY_1.terms[1]; + 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 glossary2 = GLOSSARY_2.name; - const term3 = GLOSSARY_2.terms[0]; - const term4 = GLOSSARY_2.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 entity = SEARCH_ENTITY_TABLE.table_3; - selectActiveGlossary(glossary2); + cy.get('.ant-menu-item').contains(NEW_GLOSSARY_1.name).click(); - goToAssetsTab(term3.name, term3.fullyQualifiedName, true); + goToAssetsTab( + NEW_GLOSSARY_1_TERMS.term_1.name, + NEW_GLOSSARY_1_TERMS.term_1.fullyQualifiedName, + true + ); cy.contains('Adding a new Asset is easy, just give it a spin!').should( 'be.visible' ); @@ -1008,17 +1099,13 @@ 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.name); - cy.get(`[data-testid="tag-${glossary1}.${term1.name}"]`).click(); - cy.get('[data-testid="tag-selector"]').should('contain', term1.name); + 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); // Select 2nd term - 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); + 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); interceptURL('GET', '/api/v1/tags', 'tags'); interceptURL('PATCH', '/api/v1/tables/*', 'saveTag'); @@ -1028,7 +1115,7 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { cy.get('[data-testid="saveAssociatedTag"]').scrollIntoView().click(); verifyResponseStatusCode('@saveTag', 400); toastNotification( - `Tag labels ${glossary1}.${term2.name} and ${glossary1}.${term1.name} are mutually exclusive and can't be assigned together` + `Tag labels ${glossary}.${term2} and ${glossary}.${term1} are mutually exclusive and can't be assigned together` ); // Add non mutually exclusive tags @@ -1037,17 +1124,13 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { ).click(); // Select 1st term - cy.get('[data-testid="tag-selector"] #tagsForm_tags') - .click() - .type(term3.name); + cy.get('[data-testid="tag-selector"] #tagsForm_tags').click().type(term3); - cy.get(`[data-testid="tag-${glossary2}.${term3.name}"]`).click(); - cy.get('[data-testid="tag-selector"]').should('contain', term3.name); + cy.get(`[data-testid="tag-${glossary1}.${term3}"]`).click(); + cy.get('[data-testid="tag-selector"]').should('contain', term3); // Select 2nd term - cy.get('[data-testid="tag-selector"] #tagsForm_tags') - .click() - .type(term4.name); - cy.get(`[data-testid="tag-${glossary2}.${term4.name}"]`).click(); + cy.get('[data-testid="tag-selector"] #tagsForm_tags').click().type(term4); + cy.get(`[data-testid="tag-${glossary1}.${term4}"]`).click(); cy.clickOutside(); cy.get('[data-testid="saveAssociatedTag"]').scrollIntoView().click(); verifyResponseStatusCode('@saveTag', 200); @@ -1055,8 +1138,8 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { '[data-testid="entity-right-panel"] [data-testid="glossary-container"]' ) .scrollIntoView() - .should('contain', term3.name) - .should('contain', term4.name); + .should('contain', term3) + .should('contain', term4); cy.get( '[data-testid="entity-right-panel"] [data-testid="glossary-container"] [data-testid="icon"]' @@ -1068,13 +1151,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.name); + cy.get('[data-testid="tag-selector"]').click().type(term3); cy.get( - `.ant-select-dropdown [data-testid="tag-${glossary2}.${term3.name}"]` + `.ant-select-dropdown [data-testid="tag-${glossary1}.${term3}"]` ).click(); cy.get('[data-testid="tag-selector"] > .ant-select-selector').contains( - term3.name + term3 ); cy.clickOutside(); cy.get('[data-testid="saveAssociatedTag"]').scrollIntoView().click(); @@ -1082,16 +1165,20 @@ 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.name); + .should('contain', term3); 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(glossary2).click(); + cy.get('.ant-menu-item').contains(NEW_GLOSSARY_1.name).click(); - goToAssetsTab(term3.name, term3.fullyQualifiedName, false); + goToAssetsTab( + NEW_GLOSSARY_1_TERMS.term_1.name, + NEW_GLOSSARY_1_TERMS.term_1.fullyQualifiedName, + false + ); cy.get('[data-testid="entity-header-display-name"]') .contains(entity.term) @@ -1099,21 +1186,31 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { }); it('Add asset to glossary term using asset modal', () => { - const term = GLOSSARY_3.terms[0]; - addAssetToGlossaryTerm(term, GLOSSARY_3); + 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); + }); }); it('Remove asset from glossary term using asset modal', () => { - const term = GLOSSARY_3.terms[0]; - removeAssetsFromGlossaryTerm(term, GLOSSARY_3); + const terms = Object.values(CYPRESS_ASSETS_GLOSSARY_TERMS); + terms.forEach((term) => { + removeAssetsFromGlossaryTerm(term, CYPRESS_ASSETS_GLOSSARY); + }); }); it('Remove Glossary term from entity should work properly', () => { - const glossaryName = GLOSSARY_2.name; - const { name, fullyQualifiedName } = GLOSSARY_2.terms[0]; + const glossaryName = NEW_GLOSSARY_1.name; + const { name, fullyQualifiedName } = NEW_GLOSSARY_1_TERMS.term_1; const entity = SEARCH_ENTITY_TABLE.table_3; - selectActiveGlossary(glossaryName); + selectActiveGlossary(NEW_GLOSSARY_1.name); interceptURL('GET', '/api/v1/search/query*', 'assetTab'); // go assets tab @@ -1170,7 +1267,7 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { goToGlossaryPage(); - selectActiveGlossary(glossaryName); + selectActiveGlossary(NEW_GLOSSARY_1.name); goToAssetsTab(name, fullyQualifiedName); cy.contains('Adding a new Asset is easy, just give it a spin!').should( @@ -1179,7 +1276,20 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { }); it('Tags and entity summary columns should be sorted based on current Term Page', () => { - const terms = GLOSSARY_3.terms; + 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 entityTable = SEARCH_ENTITY_TABLE.table_1; visitEntityDetailsPage({ @@ -1195,7 +1305,7 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { }); goToGlossaryPage(); - selectActiveGlossary(GLOSSARY_3.name); + selectActiveGlossary(CYPRESS_ASSETS_GLOSSARY_1.name); goToAssetsTab(terms[0].name, terms[0].fullyQualifiedName, true); checkSummaryListItemSorting({ @@ -1212,9 +1322,9 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { 'fetchGlossaryTermData' ); - const parentTerm = GLOSSARY_3.terms[0]; - const childTerm = GLOSSARY_3.terms[1]; - selectActiveGlossary(GLOSSARY_3.name); + const parentTerm = CYPRESS_ASSETS_GLOSSARY_TERMS.term_1; + const childTerm = CYPRESS_ASSETS_GLOSSARY_TERMS.term_2; + selectActiveGlossary(CYPRESS_ASSETS_GLOSSARY.name); cy.get('[data-testid="expand-collapse-all-button"]').click(); visitGlossaryTermPage(childTerm.name, childTerm.fullyQualifiedName, true); @@ -1254,61 +1364,67 @@ describe('Glossary page should work properly', { tags: 'Governance' }, () => { goToGlossaryPage(); - const newTermHierarchy = `${Cypress.$.escapeSelector(GLOSSARY_3.name)}.${ - parentTerm.name - }.${childTerm.name}`; - selectActiveGlossary(GLOSSARY_3.name); + const newTermHierarchy = `${Cypress.$.escapeSelector( + CYPRESS_ASSETS_GLOSSARY.name + )}.${parentTerm.name}.${childTerm.name}`; + selectActiveGlossary(CYPRESS_ASSETS_GLOSSARY.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( - `${GLOSSARY_3.name}.${parentTerm.name}.${childTerm.name}`, + `${CYPRESS_ASSETS_GLOSSARY.name}.${parentTerm.name}.${childTerm.name}`, '.ant-table-thead > tr', true ); - confirmationDragAndDropGlossary(childTerm.name, GLOSSARY_3.name, true); + confirmationDragAndDropGlossary( + childTerm.name, + CYPRESS_ASSETS_GLOSSARY.name, + true + ); }); it('Drag and Drop should work properly for glossary term', () => { - const { fullyQualifiedName: term1Fqn, name: term1Name } = - GLOSSARY_1.terms[0]; - const { fullyQualifiedName: term2Fqn, name: term2Name } = - GLOSSARY_1.terms[1]; + selectActiveGlossary(NEW_GLOSSARY.name); - selectActiveGlossary(GLOSSARY_1.name); - dragAndDropElement(term2Fqn, term1Fqn); + dragAndDropElement( + NEW_GLOSSARY_TERMS.term_2.fullyQualifiedName, + NEW_GLOSSARY_TERMS.term_1.fullyQualifiedName + ); - confirmationDragAndDropGlossary(term2Name, term1Name); + confirmationDragAndDropGlossary( + NEW_GLOSSARY_TERMS.term_2.name, + NEW_GLOSSARY_TERMS.term_1.name + ); // clicking on the expand icon to view the child term cy.get( `[data-row-key=${Cypress.$.escapeSelector( - term1Fqn + NEW_GLOSSARY_TERMS.term_1.fullyQualifiedName )}] [data-testid="expand-icon"] > svg` ).click(); cy.get( `.ant-table-row-level-1[data-row-key="${Cypress.$.escapeSelector( - term1Fqn - )}.${term2Name}"]` + NEW_GLOSSARY_TERMS.term_1.fullyQualifiedName + )}.${NEW_GLOSSARY_TERMS.term_2.name}"]` ).should('be.visible'); }); it('Drag and Drop should work properly for glossary term at table level', () => { - selectActiveGlossary(GLOSSARY_1.name); + selectActiveGlossary(NEW_GLOSSARY.name); cy.get('[data-testid="expand-collapse-all-button"]').click(); dragAndDropElement( - `${GLOSSARY_1.terms[0].fullyQualifiedName}.${GLOSSARY_1.terms[1].name}`, + `${NEW_GLOSSARY_TERMS.term_1.fullyQualifiedName}.${NEW_GLOSSARY_TERMS.term_2.name}`, '.ant-table-thead > tr', true ); confirmationDragAndDropGlossary( - GLOSSARY_1.terms[1].name, - GLOSSARY_1.name, + NEW_GLOSSARY_TERMS.term_2.name, + NEW_GLOSSARY.name, true ); @@ -1316,19 +1432,29 @@ 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( - GLOSSARY_1.terms[1].fullyQualifiedName + NEW_GLOSSARY_TERMS.term_2.fullyQualifiedName )}"]` ).should('be.visible'); }); it('Delete glossary term should work properly', () => { - selectActiveGlossary(GLOSSARY_2.name); - GLOSSARY_2.terms.forEach(deleteGlossaryTerm); + 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); }); it('Delete glossary should work properly', () => { verifyResponseStatusCode('@fetchGlossaries', 200); - [GLOSSARY_1.name, GLOSSARY_2.name, GLOSSARY_3.name].forEach((glossary) => { + [ + NEW_GLOSSARY.name, + NEW_GLOSSARY_1.name, + CYPRESS_ASSETS_GLOSSARY.name, + CYPRESS_ASSETS_GLOSSARY_1.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 872abbb4f794..1efa9a7fae6a 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 { - addOwnerInGlossary, + addReviewer, removeReviewer, visitGlossaryPage, } from '../../common/GlossaryUtils'; @@ -245,12 +245,7 @@ describe( removeOwner(data.user.displayName, GLOSSARY_OWNER_LINK_TEST_ID); - addOwnerInGlossary( - [data.reviewer.displayName], - 'Add', - 'glossary-reviewer-name', - false - ); + addReviewer(data.reviewer.displayName, 'glossaries'); // Adding manual wait as the backend is now performing batch operations, // which causes a delay in reflecting changes @@ -402,12 +397,7 @@ describe( removeOwner(data.user.displayName, GLOSSARY_OWNER_LINK_TEST_ID); - addOwnerInGlossary( - [data.reviewer.displayName], - 'Add', - 'glossary-reviewer-name', - false - ); + addReviewer(data.reviewer.displayName, 'glossaryTerms'); 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 2cb6263556f5..f9e7713d0719 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}"]`) - .should('have.class', 'active'); + .find(`[title="${TEAM_DETAILS.username}"] input[type='checkbox']`) + .should('be.checked'); 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 fd38d1600002..19f7632afb9e 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,7 +81,6 @@ export const ActivityFeedTab = ({ columns, entityType, refetchFeed, - hasGlossaryReviewer, entityFeedTotalCount, isForFeedTab = true, onUpdateFeedCount, @@ -511,7 +510,6 @@ 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 924d9c2713c1..f328da38fbe4 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 @@ -164,9 +164,9 @@ export const TestSuites = ({ summaryPanel }: { summaryPanel: ReactNode }) => { title: `${t('label.success')} %`, dataIndex: 'summary', key: 'success', - render: (value: TestSuite['summary']) => { + render: (value: TestSummary) => { const percent = - value?.total && value?.success ? value.success / value.total : 0; + value.total && value.success ? value.success / value.total : 0; return ( { - handleOwnerSelect(updatedUser as EntityReference) - }> + onUpdate={handleOwnerSelect}> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.test.tsx index dca3ee95a2a8..1036c315882d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.test.tsx @@ -63,16 +63,13 @@ jest.mock('../UserProfileImage/UserProfileImage.component', () => { }); jest.mock('../../../../common/InlineEdit/InlineEdit.component', () => { - return jest.fn().mockImplementation(({ onSave, onCancel, children }) => ( + return jest.fn().mockImplementation(({ onSave, children }) => (
InlineEdit {children} -
)); }); @@ -227,28 +224,6 @@ 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 47fef910cfb6..53f4e58b66fe 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,7 +13,6 @@ import { Image } from 'antd'; import React, { useEffect, useMemo, useState } from 'react'; -import { getEntityName } from '../../../../../utils/EntityUtils'; import { getImageWithResolutionAndFallback, ImageQuality, @@ -51,7 +50,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 fab09848755e..dbe62b8539de 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,7 +15,6 @@ 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'; @@ -38,7 +37,6 @@ 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 d117baffd3e7..e6a69c06fdca 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, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { 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,7 +24,6 @@ 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'; @@ -88,15 +87,6 @@ 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 @@ -132,7 +122,6 @@ const UserProfileRoles = ({ : []), ...(userRoles ?? []), ]} - entityType={EntityType.ROLE} icon={} noDataPlaceholder={t('message.no-roles-assigned')} showNoDataPlaceholder={!isUserAdmin} @@ -141,14 +130,14 @@ const UserProfileRoles = ({ [userRoles, isUserAdmin] ); - const handleCloseEditRole = useCallback(() => { - setIsRolesEdit(false); - setUserRoles(); - }, [setUserRoles]); - useEffect(() => { - setUserRoles(); - }, [setUserRoles]); + const defaultUserRoles = [ + ...(userRoles?.map((role) => role.id) ?? []), + ...(isUserAdmin ? [toLower(TERM_ADMIN)] : []), + ]; + + setSelectedRoles(defaultUserRoles); + }, [isUserAdmin, userRoles]); useEffect(() => { if (isRolesEdit && isEmpty(roles)) { @@ -187,7 +176,7 @@ const UserProfileRoles = ({ setIsRolesEdit(false)} onSave={handleRolesSave}> - onSelectionChange([ - { - id: '37a00e0b-383c-4451-b63f-0bad4c745abc', - name: 'admin', - type: 'team', - }, - ]) - } - /> -
- )); + return jest.fn().mockReturnValue(

TeamsSelectable

); }); describe('Test User Profile Teams Component', () => { @@ -95,26 +67,18 @@ describe('Test User Profile Teams Component', () => { expect(await screen.findAllByText('Chip')).toHaveLength(1); }); - it('should maintain initial state if edit is close without save', async () => { + it('should render teams select input on edit click', async () => { render(); - fireEvent.click(screen.getByTestId('edit-teams-button')); - - const selectInput = screen.getByTestId('select-user-teams'); + expect(screen.getByTestId('user-team-card-container')).toBeInTheDocument(); - act(() => { - fireEvent.change(selectInput, { - target: { - value: 'test', - }, - }); - }); + const editButton = screen.getByTestId('edit-teams-button'); - fireEvent.click(screen.getByTestId('cancel')); + expect(editButton).toBeInTheDocument(); - fireEvent.click(screen.getByTestId('edit-teams-button')); + fireEvent.click(editButton); - expect(screen.getByText('Organization')).toBeInTheDocument(); + expect(screen.getByText('InlineEdit')).toBeInTheDocument(); }); it('should call updateUserDetails on click save', async () => { @@ -131,14 +95,7 @@ describe('Test User Profile Teams Component', () => { }); expect(mockPropsData.updateUserDetails).toHaveBeenCalledWith( - { - teams: [ - { - id: '9e8b7464-3f3e-4071-af05-19be142d75db', - type: 'team', - }, - ], - }, + { teams: [] }, 'teams' ); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/AsyncSelectList.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/AsyncSelectList.tsx index ffb560dfdcff..59aad2d1877b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/AsyncSelectList.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/AsyncSelectList.tsx @@ -296,6 +296,7 @@ const AsyncSelectList: FC = ({ return ( ({ usePermissionProvider: jest.fn().mockImplementation(() => ({ permissions: { @@ -45,9 +73,7 @@ jest.mock('../../../../rest/testAPI', () => { ...jest.requireActual('../../../../rest/testAPI'), getListTestSuitesBySearch: jest .fn() - .mockImplementation(() => - Promise.resolve({ data: [], paging: { total: 0 } }) - ), + .mockImplementation(() => Promise.resolve(mockList)), }; }); jest.mock('react-router-dom', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueryRightPanel/TableQueryRightPanel.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueryRightPanel/TableQueryRightPanel.component.tsx index fc6a54bd895a..be8a483e9e4a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueryRightPanel/TableQueryRightPanel.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableQueries/TableQueryRightPanel/TableQueryRightPanel.component.tsx @@ -21,6 +21,7 @@ import { ReactComponent as IconUser } from '../../../../assets/svg/user.svg'; import { DE_ACTIVE_COLOR, getUserPath } from '../../../../constants/constants'; import { EntityType } from '../../../../enums/entity.enum'; import { Query } from '../../../../generated/entity/data/query'; +import { EntityReference } from '../../../../generated/entity/type'; import { TagLabel } from '../../../../generated/type/tagLabel'; import { getEntityName } from '../../../../utils/EntityUtils'; import DescriptionV1 from '../../../common/EntityDescription/DescriptionV1'; @@ -93,7 +94,9 @@ const TableQueryRightPanel = ({ + onUpdate={(updatedUser) => + handleUpdateOwner(updatedUser as EntityReference) + }> + onUpdate={(updatedUser) => + handleUpdatedOwner(updatedUser as EntityReference) + }> + onUpdate={(updatedUser) => + handleUpdatedOwner(updatedUser as EntityReference) + }> } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.test.tsx index 98ab69b71e7d..2f9a3ea310a6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.test.tsx @@ -162,4 +162,12 @@ describe('Test TaskFeedCard component', () => { expect(activityFeedCard).toBeInTheDocument(); }); + + it('should not render task action button to the task owner if task has reviewer', async () => { + render(, { + wrapper: MemoryRouter, + }); + + expect(screen.getByTestId('task-cta-buttons')).toBeEmptyDOMElement(); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.tsx index 7bc4e5d0cd58..7ac46e072a7a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.component.tsx @@ -111,6 +111,7 @@ export const TaskTab = ({ taskThread, owner, entityType, + hasGlossaryReviewer, ...rest }: TaskTabProps) => { const history = useHistory(); @@ -337,7 +338,7 @@ export const TaskTab = ({ const hasEditAccess = isAdminUser || isAssignee || - isOwner || + (!hasGlossaryReviewer && isOwner) || (Boolean(isPartOfAssigneeTeam) && !isCreator); const onSave = (message: string) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.interface.ts index c3f3bc909791..864bfb061452 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/Task/TaskTab/TaskTab.interface.ts @@ -19,6 +19,7 @@ export type TaskTabProps = { taskThread: Thread; owner?: EntityReference; isForFeedTab?: boolean; + hasGlossaryReviewer?: boolean; onUpdateEntityDetails?: () => void; onAfterClose?: () => void; } & ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.component.tsx index b600741ae556..7717a2e3f824 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.component.tsx @@ -12,10 +12,12 @@ */ import { + ExclamationCircleOutlined, SortAscendingOutlined, SortDescendingOutlined, } from '@ant-design/icons'; import { + Alert, Button, Col, Layout, @@ -45,6 +47,7 @@ import { TAG_FQN_KEY, } from '../../constants/explore.constants'; import { ERROR_PLACEHOLDER_TYPE, SORT_ORDER } from '../../enums/common.enum'; +import { useApplicationStore } from '../../hooks/useApplicationStore'; import { QueryFieldInterface } from '../../pages/ExplorePage/ExplorePage.interface'; import { getDropDownItems } from '../../utils/AdvancedSearchUtils'; import { Transi18next } from '../../utils/CommonUtils'; @@ -52,7 +55,6 @@ import { highlightEntityNameAndDescription } from '../../utils/EntityUtils'; import { getSelectedValuesFromQuickFilter } from '../../utils/Explore.utils'; import { getApplicationDetailsPath } from '../../utils/RouterUtils'; import searchClassBase from '../../utils/SearchClassBase'; -import InlineAlert from '../common/InlineAlert/InlineAlert'; import Loader from '../common/Loader/Loader'; import { ExploreProps, @@ -64,6 +66,47 @@ import SearchedData from '../SearchedData/SearchedData'; import { SearchedDataProps } from '../SearchedData/SearchedData.interface'; import './exploreV1.less'; +const IndexNotFoundBanner = () => { + const { theme } = useApplicationStore(); + const { t } = useTranslation(); + + return ( + + +
+ + {t('server.indexing-error')} + + + + } + values={{ + settings: t('label.search-index-setting-plural'), + }} + /> + +
+
+ } + type="error" + /> + ); +}; + const ExploreV1: React.FC = ({ aggregations, activeTabKey, @@ -357,26 +400,7 @@ const ExploreV1: React.FC = ({ {isElasticSearchIssue ? ( - - } - values={{ - settings: t('label.search-index-setting-plural'), - }} - /> - } - heading={t('server.indexing-error')} - type="error" - /> + ) : ( <> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/AddGlossary/AddGlossary.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/AddGlossary/AddGlossary.component.tsx index 7675075c97d5..e5302492e554 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/AddGlossary/AddGlossary.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/AddGlossary/AddGlossary.component.tsx @@ -14,7 +14,6 @@ import { PlusOutlined } from '@ant-design/icons'; import { Button, Form, Space, Typography } from 'antd'; import { FormProps, useForm } from 'antd/lib/form/Form'; -import { toString } from 'lodash'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { ENTITY_NAME_REGEX } from '../../../constants/regex.constants'; @@ -30,8 +29,8 @@ import { import { getEntityName } from '../../../utils/EntityUtils'; import { generateFormFields, getField } from '../../../utils/formUtils'; +import { EntityType } from '../../../enums/entity.enum'; import { useApplicationStore } from '../../../hooks/useApplicationStore'; -import { UserTeam } from '../../common/AssigneeList/AssigneeList.interface'; import ResizablePanels from '../../common/ResizablePanels/ResizablePanels'; import TitleBreadcrumb from '../../common/TitleBreadcrumb/TitleBreadcrumb.component'; import { UserTag } from '../../common/UserTag/UserTag.component'; @@ -54,19 +53,16 @@ const AddGlossary = ({ 'owner', form ); - const reviewersList = - Form.useWatch('reviewers', form) ?? []; + const reviewersData = + Form.useWatch('reviewers', form) ?? []; + + const reviewersList = Array.isArray(reviewersData) + ? reviewersData + : [reviewersData]; const handleSave: FormProps['onFinish'] = (formData) => { - const { - name, - displayName, - description, - tags, - mutuallyExclusive, - reviewers = [], - owner, - } = formData; + const { name, displayName, description, tags, mutuallyExclusive, owner } = + formData; const selectedOwner = owner ?? { id: currentUser?.id, @@ -76,9 +72,7 @@ const AddGlossary = ({ name: name.trim(), displayName: displayName?.trim(), description: description, - reviewers: reviewers - .map((d: EntityReference) => toString(d.fullyQualifiedName)) - .filter(Boolean), + reviewers: reviewersList.filter(Boolean), owner: selectedOwner, tags: tags || [], mutuallyExclusive: Boolean(mutuallyExclusive), @@ -202,7 +196,7 @@ const AddGlossary = ({ id: 'root/reviewers', required: false, label: t('label.reviewer-plural'), - type: FieldTypes.USER_MULTI_SELECT, + type: FieldTypes.USER_TEAM_SELECT, props: { hasPermission: true, popoverProps: { placement: 'topLeft' }, @@ -214,6 +208,9 @@ const AddGlossary = ({ type="primary" /> ), + multiple: { user: true, team: false }, + previewSelected: true, + label: t('label.reviewer-plural'), }, formItemLayout: FormItemLayout.HORIZONTAL, formItemProps: { @@ -244,7 +241,7 @@ const AddGlossary = ({
@@ -261,7 +258,9 @@ const AddGlossary = ({ size={[8, 8]}> {reviewersList.map((d, index) => ( { const { currentUser } = useApplicationStore(); const owner = Form.useWatch('owner', form); - const reviewersList = - Form.useWatch('reviewers', form) ?? []; + const reviewersData = + Form.useWatch('reviewers', form) ?? []; + + const reviewersList = Array.isArray(reviewersData) + ? reviewersData + : [reviewersData]; const getRelatedTermFqnList = (relatedTerms: DefaultOptionType[]): string[] => relatedTerms.map((tag: DefaultOptionType) => tag.value as string); @@ -69,7 +73,6 @@ const AddGlossaryTermForm = ({ id: currentUser?.id ?? '', type: 'user', }; - const style = { color, iconURL, @@ -321,11 +324,14 @@ const AddGlossaryTermForm = ({ id: 'root/reviewers', required: false, label: t('label.reviewer-plural'), - type: FieldTypes.USER_MULTI_SELECT, + type: FieldTypes.USER_TEAM_SELECT, props: { hasPermission: true, filterCurrentUser: true, popoverProps: { placement: 'topLeft' }, + multiple: { user: true, team: false }, + previewSelected: true, + label: t('label.reviewer-plural'), children: (
- {hasEditReviewerAccess && noReviewersSelected && ( - - + )}
@@ -334,7 +363,7 @@ const GlossaryDetailsRightPanel = ({ )} - + {!isGlossary && selectedData && ( { - const { activeGlossary, updateActiveGlossary } = useGlossaryStore(); + const { activeGlossary, glossaryChildTerms, setGlossaryChildTerms } = + useGlossaryStore(); const { theme } = useApplicationStore(); const { t } = useTranslation(); - const glossaryTerms = activeGlossary?.children as ModifiedGlossaryTerm[]; + const glossaryTerms = (glossaryChildTerms as ModifiedGlossaryTerm[]) ?? []; const [movedGlossaryTerm, setMovedGlossaryTerm] = useState(); @@ -293,7 +294,7 @@ const GlossaryTermTab = ({ (item as ModifiedGlossary).children = data; - updateActiveGlossary({ children: terms }); + setGlossaryChildTerms(terms as ModifiedGlossary[]); children = data; } @@ -312,7 +313,7 @@ const GlossaryTermTab = ({ return ; }, }), - [glossaryTerms, updateActiveGlossary, expandedRowKeys] + [glossaryTerms, setGlossaryChildTerms, expandedRowKeys] ); const handleMoveRow = useCallback( @@ -393,9 +394,7 @@ const GlossaryTermTab = ({ limit: API_RES_MAX_SIZE, fields: 'children,owner,parent', }); - updateActiveGlossary({ - children: buildTree(data) as ModifiedGlossary['children'], - }); + setGlossaryChildTerms(buildTree(data) as ModifiedGlossary[]); const keys = data.reduce((prev, curr) => { if (curr.children?.length) { prev.push(curr.fullyQualifiedName ?? ''); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.test.tsx index 554b9971671c..3fdc791128f0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTermTab/GlossaryTermTab.test.tsx @@ -103,6 +103,7 @@ describe('Test GlossaryTermTab component', () => { ...mockedGlossaryTerms[0], children: mockedGlossaryTerms, }, + glossaryChildTerms: mockedGlossaryTerms, updateActiveGlossary: jest.fn(), })); const { container } = render(, { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx index 87564c0a7f09..70924401ff11 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/GlossaryTermsV1.component.tsx @@ -13,6 +13,7 @@ import { Col, Row, Tabs } from 'antd'; import { t } from 'i18next'; +import { isEmpty } from 'lodash'; import React, { useCallback, useEffect, @@ -77,13 +78,13 @@ const GlossaryTermsV1 = ({ const { fqn: glossaryFqn } = useFqn(); const history = useHistory(); const assetTabRef = useRef(null); - const [assetModalVisible, setAssetModelVisible] = useState(false); + const [assetModalVisible, setAssetModalVisible] = useState(false); const [feedCount, setFeedCount] = useState( FEED_COUNT_INITIAL_DATA ); const [assetCount, setAssetCount] = useState(0); - const { activeGlossary } = useGlossaryStore(); - const childGlossaryTerms = activeGlossary?.children ?? []; + const { glossaryChildTerms } = useGlossaryStore(); + const childGlossaryTerms = glossaryChildTerms ?? []; const assetPermissions = useMemo(() => { const glossaryTermStatus = glossaryTerm.status ?? Status.Approved; @@ -227,7 +228,7 @@ const GlossaryTermsV1 = ({ isSummaryPanelOpen={isSummaryPanelOpen} permissions={assetPermissions} ref={assetTabRef} - onAddAsset={() => setAssetModelVisible(true)} + onAddAsset={() => setAssetModalVisible(true)} onAssetClick={onAssetClick} onRemoveAsset={handleAssetSave} /> @@ -247,6 +248,8 @@ const GlossaryTermsV1 = ({ @@ -341,7 +344,7 @@ const GlossaryTermsV1 = ({ selectedData={{ ...glossaryTerm, displayName, name }} updateVote={updateVote} onAddGlossaryTerm={onAddGlossaryTerm} - onAssetAdd={() => setAssetModelVisible(true)} + onAssetAdd={() => setAssetModalVisible(true)} onDelete={handleGlossaryTermDelete} onUpdate={onTermUpdate} /> @@ -365,7 +368,7 @@ const GlossaryTermsV1 = ({ glossaryTerm.fullyQualifiedName )} type={AssetsOfEntity.GLOSSARY} - onCancel={() => setAssetModelVisible(false)} + onCancel={() => setAssetModalVisible(false)} onSave={handleAssetSave} /> )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx index 3e8c077080f6..a66a1d62c5e0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx @@ -53,7 +53,7 @@ import GlossaryTermsV1 from './GlossaryTerms/GlossaryTermsV1.component'; import { GlossaryV1Props } from './GlossaryV1.interfaces'; import './glossaryV1.less'; import ImportGlossary from './ImportGlossary/ImportGlossary'; -import { useGlossaryStore } from './useGlossary.store'; +import { ModifiedGlossary, useGlossaryStore } from './useGlossary.store'; const GlossaryV1 = ({ isGlossaryActive, @@ -96,7 +96,7 @@ const GlossaryV1 = ({ const [editMode, setEditMode] = useState(false); - const { activeGlossary, updateActiveGlossary } = useGlossaryStore(); + const { activeGlossary, setGlossaryChildTerms } = useGlossaryStore(); const { id, fullyQualifiedName } = activeGlossary ?? {}; @@ -125,11 +125,11 @@ const GlossaryV1 = ({ const { data } = await getFirstLevelGlossaryTerms( params?.glossary ?? params?.parent ?? '' ); - updateActiveGlossary({ - children: data.map((data) => - data.childrenCount ?? 0 > 0 ? { ...data, children: [] } : data - ), - }); + const children = data.map((data) => + data.childrenCount ?? 0 > 0 ? { ...data, children: [] } : data + ); + + setGlossaryChildTerms(children as ModifiedGlossary[]); } catch (error) { showErrorToast(error as AxiosError); } finally { @@ -257,9 +257,6 @@ const GlossaryV1 = ({ try { await addGlossaryTerm({ ...formData, - reviewers: formData.reviewers.map( - (item) => item.fullyQualifiedName || '' - ), glossary: activeGlossaryTerm?.glossary?.name || (selectedData.fullyQualifiedName ?? ''), diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryVersion/GlossaryVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryVersion/GlossaryVersion.component.tsx index 31bc8575ddec..724e3414d36a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryVersion/GlossaryVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryVersion/GlossaryVersion.component.tsx @@ -34,6 +34,7 @@ import Loader from '../../common/Loader/Loader'; import EntityVersionTimeLine from '../../Entity/EntityVersionTimeLine/EntityVersionTimeLine'; import PageLayoutV1 from '../../PageLayoutV1/PageLayoutV1'; import GlossaryV1Component from '../GlossaryV1.component'; +import { ModifiedGlossary, useGlossaryStore } from '../useGlossary.store'; interface GlossaryVersionProps { isGlossary?: boolean; @@ -51,6 +52,7 @@ const GlossaryVersion = ({ isGlossary = false }: GlossaryVersionProps) => { ); const [selectedData, setSelectedData] = useState(); const [isVersionLoading, setIsVersionLoading] = useState(true); + const { setActiveGlossary } = useGlossaryStore(); const fetchVersionsInfo = async () => { try { @@ -72,6 +74,7 @@ const GlossaryVersion = ({ isGlossary = false }: GlossaryVersionProps) => { : await getGlossaryTermsVersion(id, version); setSelectedData(res); + setActiveGlossary(res as ModifiedGlossary); } catch (error) { showErrorToast(error as AxiosError); } finally { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/useGlossary.store.ts b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/useGlossary.store.ts index 8f067fdb1ede..81ec1d9bef89 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/useGlossary.store.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/useGlossary.store.ts @@ -21,13 +21,16 @@ export type ModifiedGlossary = Glossary & { export const useGlossaryStore = create<{ glossaries: Glossary[]; activeGlossary: ModifiedGlossary; + glossaryChildTerms: ModifiedGlossary[]; setGlossaries: (glossaries: Glossary[]) => void; setActiveGlossary: (glossary: ModifiedGlossary) => void; updateGlossary: (glossary: Glossary) => void; updateActiveGlossary: (glossary: Partial) => void; + setGlossaryChildTerms: (glossaryChildTerms: ModifiedGlossary[]) => void; }>()((set, get) => ({ glossaries: [], activeGlossary: {} as ModifiedGlossary, + glossaryChildTerms: [], setGlossaries: (glossaries: Glossary[]) => { set({ glossaries }); @@ -45,8 +48,26 @@ export const useGlossaryStore = create<{ set({ activeGlossary: glossary }); }, updateActiveGlossary: (glossary: Partial) => { - const { activeGlossary } = get(); + const { activeGlossary, glossaries } = get(); - set({ activeGlossary: { ...activeGlossary, ...glossary } as Glossary }); + const updatedGlossary = { + ...activeGlossary, + ...glossary, + } as ModifiedGlossary; + + // Update the active glossary + set({ activeGlossary: updatedGlossary }); + + // Update the corresponding glossary in the glossaries list + const index = glossaries.findIndex( + (g) => g.fullyQualifiedName === updatedGlossary.fullyQualifiedName + ); + + if (index !== -1) { + glossaries[index] = updatedGlossary; + } + }, + setGlossaryChildTerms: (glossaryChildTerms: ModifiedGlossary[]) => { + set({ glossaryChildTerms }); }, })); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Modals/WhatsNewModal/whatsNewData.ts b/openmetadata-ui/src/main/resources/ui/src/components/Modals/WhatsNewModal/whatsNewData.ts index ae67de68765b..b47a8ddca2c8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Modals/WhatsNewModal/whatsNewData.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Modals/WhatsNewModal/whatsNewData.ts @@ -1738,195 +1738,30 @@ API: { id: 27, version: 'v1.4.2', - description: `Released on 29th May 2024.`, - note: "In 1.4.2, Fix table partition migration for Redshift tables with diststyle different than AUTO or KEY. Don't miss out the release highlights!", - features: [ - { - title: 'Automations', - description: - 'We have introduced Automations to easily maintain high-quality metadata at scale. The Automations streamline governance processes from ownership assignments to tagging, ensuring compliance and consistency. We have added support for the following actions: adding and removing owner, tier, domain, tags, glossary terms and descriptions, ML PII tagging, and propagation of tags and glossary terms through lineage.', - isImage: false, - path: 'https://www.youtube.com/embed/zdh4yzHw4w0', - }, - { - title: 'Bulk Upload Data Assets', - description: - 'We have added support for bulk uploading data assets. Users can bulk upload database, schema, and table entities from a CSV file for quick edition or creation. The inline editor helps to validate and update the data assets before importing. Save time by bulk uploading data assets.', - isImage: false, - path: 'https://www.youtube.com/embed/CXxDdS6AifY', - }, - { - title: 'Data Quality Widget', - description: - 'A new Data Quality Widget has been added. It lists the summary of data quality tests belonging to a user or their team. Customize your Collate landing page to suit your requirements.', - isImage: false, - path: 'https://www.youtube.com/embed/Kakfa-lYGOU', - }, - { - title: 'Lineage Layers', - description: - 'The lineage view in OpenMetadata has been improved. All the nodes are expanded by default. A new ‘Layers’ button has been introduced. Users can choose to view the column level lineage. In the Data Observability View, the data quality results are displayed, such as Success, Aborted, or Failed. The pipeline status displays the last execution run.', - isImage: false, - path: 'https://www.youtube.com/embed/wtBMeLvA6Sw', - }, - { - title: 'Column Lineage Search', - description: - 'You can search lineage by column names. You can accurately trace the upstream and downstream nodes by column. OpenMetadata helps you to easily trace and visualize how data is transformed and where it is used in your organization.', - isImage: false, - path: 'https://www.youtube.com/embed/KZdVb8DiHJs', - }, - { - title: 'Custom Properties', - description: - 'OpenMetadata has been empowering users to enrich the data assets by extending their attributes with custom properties. Custom Properties now allow linking other assets in the platform, such as Tables, Dashboards, etc. To enable this, create a Custom Property as an Entity Reference or Entity Reference List.', - isImage: false, - path: 'https://www.youtube.com/embed/lZoSeKkErBk', - }, - { - title: 'Custom Theme', - description: - "OpenMetadata previously supported uploading your company logo, monogram, and favicon to customize the platform's appearance according to your brand identity. Now, you can take it a step further by customizing the theme with colors that perfectly align with your company's branding.", - isImage: false, - path: 'https://www.youtube.com/embed/-NiU1flBHs0', - }, - { - title: 'Data Quality Filters', - description: - 'We have improved the filters for data quality. Now you have additional filtering options for test suites and test cases.', - isImage: false, - path: 'https://www.youtube.com/embed/UNOHvBMVcYM', - }, - { - title: 'Data Profiler', - description: - 'A global profiler configuration page has been implemented for the data profiler. This allows Admins to exclude certain metric computations for specific data types. Navigate to Settings > Preferences > Profiler Configuration to define the metrics to compute based on column data types.', - isImage: true, - path: profilerConfigPage, - }, - { - title: 'Incident Manager', - description: - 'Based on the latest failed test cases, a sample of failed rows will be displayed in the Incident Manager. Users can quickly verify the cause of failure based on this sample data. The failed sample data will be deleted once the issue is resolved. This is a Collate only feature.', - isImage: true, - path: incidentManagerSampleData, - }, - ], + description: `Released on 10th June 2024.`, + features: [], changeLogs: { - ['Backward Incompatible Changes']: ` -Tooling: -- Metadata Backup/Recovery is deprecated. No further support will be provided. -- Users are advised to use database native tools to backup and store it in their object store for recovery. -- bootstrap/bootstrap_storage.sh has been deprecated in favor of bootstrap/openmetadata-ops.sh - -UI: -- Activity has been improved. New update specific cards display critical information such as data quality test case updates, description, tag update or removal. -- For Lineage, the Expand All button has been removed. A new Layers button is introduced at the bottom left corner. With the Layers button, you can add Column Level Lineage or Data Observability details to your Lineage view. -- View Definition is now renamed as Schema Definition. -- Adding Glossary Term view is improved. Now we show glossary terms hierarchically enabling a better understanding of how the terms are setup while adding it to a table or dashboard. -- For Classification, users can set classification to be mutually exclusive only **at the time of creation**. Once created, you cannot change it back to mutually non-exclusive or vice-versa. This is to prevent conflicts of adding multiple tags that belong to same classification and later turning the mutually exclusive flag back to true. - -API: -- Table Schema's ViewDefinition is now renamed to SchemaDefinition to capture Tables' Create Schema. -- Bulk Import API now creates entities if they are not present during the import. -- Table's TestSuite is migrated to EntityReference. Previously it used to store entire payload of TestSuite. -`, - [`Automations ${CollateIconWithLinkMD}`]: `- Easily maintain high-quality metadata at scale with automations. The Automations streamline governance processes from ownership assignments to tagging, ensuring compliance and consistency. -- You can update the properties of your assets by filtering by service, owner, domain, or any other supported property from the advanced search. -- Easily see which assets have been selected by jumping to the Explore page in one click. -- For tables, data models, topics, and search indexes, you can apply the action to their columns or fields. -- We added support for the following actions: adding and removing owner, tier, domain, tags, glossary terms and descriptions, ML PII tagging, and propagation of tags and glossary terms through lineage.`, - - [`Bulk Upload Data Assets ${CollateIconWithLinkMD}`]: `- Bulk upload/download database, schema, and table entities from/into a CSV file for quick edition or creation. -- Supports an inline editor to validate/update assets before performing the upload.`, - - 'Data Quality Improvements': `- The Table schema page now shows the Data Quality tests for each column. -- Improved filtering options for test suite and test cases. -- We have improved how the UI fetches the Data Quality details for improved performance. -- We now compute Unique and Count in the same query to avoid inconsistency due to the high frequency of data insertion. -- Fixed the issue with removing the test case description upon the test case display name change. -- Support has been added for an empty string as a missing count.`, - - 'Data Profiler': `- Implemented a global profiler configuration page, allowing admin to exclude certain metric computations for specific data types. -- Added profiler support for Redshift complex types and DynamoDB. -- Fixed an issue with performing sum operations for large values in profiler ingestion. -- Fixed the histogram unit's issues with scientific notation.`, - - 'Incident Manager': `- We now display a sample of failed rows for the latest failed test cases. Once the issue is resolved, the failed sample will be deleted. ${CollateIconWithLinkMD} -- Fixed the Date time filter for the Incident Manager. -- Notifications are sent for the tasks created by the Incident Manager.`, - - 'Lineage Improvements': `- OpenMetadata already supports Column-level lineage, and now we have introduced Task-level lineage for Pipelines, Chart-level lineage for Dashboards, Feature-level lineage for ML Models, Field-level lineage for Topics, and columns for dashboard Data Models. -- Automated column-level lineage is now supported for Tableau, Superset, QlikCloud, and QlikSense between Data Models and Tables. -- The child nodes in a lineage graph are sorted in alphabetical order. -- Improved the log of failed-to-parse queries. -- Fixed an issue with automated column-level lineage overwriting the pipeline lineage and manual column lineage. -- Snowflake & Databricks now supports automated lineage between external tables and their origin storage container. -- Lineage can be exported as a CSV file. -- OpenMetadata spark agent now supports automated lineage between tables and their origin storage container. -- Fixed an issue with parsing lineage queries for Redshift. -- Now, we support pipeline as an edge between any two entity types. -- We now parse PowerBi DAX files for lineage. -- Support has been added for dynamic tables.`, - - 'Data Insights': `- Previously, the data insights reports displayed only the percentage coverage of ownership and description. Now, users can drill down to view the data assets with no owner or description. -- Improved the UX for data insight filters.`, - - [`Cost Analysis ${CollateIconWithLinkMD}`]: `- Lifecycle data for Cost Analysis has been implemented for BigQuery, Snowflake, and Redshift.`, - - 'Custom Theme': `- Previously supported adding logo, monogram, and favicon to your OpenMetadata instance. -- Now, it supports customizing the theme with colors to suit your company branding.`, - - [`Landing Page Widgets ${CollateIconWithLinkMD}`]: `- Added a Data Quality Widget to list the summary of data quality tests belonging to a user or their team.`, - - 'Ingestion Performance Improvements': `- Bigquery, Redshift, and Snowflake now support incremental metadata ingestions by scanning DML operations on the query history. -- Database Services now support parallelizing the metadata ingestion at each schema.`, - - Connectors: `- Now supports a new connector for [QlikCloud](https://www.qlik.com/us/products/qlik-cloud). -- New Kafka Connect connector -- We now parse complex protobuf schemas for Kafka -- Improved model storage ingestion for Sagemaker and Mlflow. -- Added an option to include or exclude drafts from dashboards. -- Added an option to include or exclude paused pipelines in Airflow. -- Revamped SSL support to allow users to upload the required certificates directly in the UI. -- The character support has been enhanced for tag ingestion to include /. -- In the Oracle connector, we rolled back to use all_ tables instead of dba_. -- Added support for Azure auth in Trino. -- For QlikSense, we have added an option to disable SSL validation.`, - - 'Custom Properties': `- Custom Properties now allow linking other assets in the platform, such as Tables, Dashboards, etc. To enable this, create a Custom Property as an Entity Reference or Entity Reference List.`, - - 'Health Check': `- Introduced the OpenMetadata Status page to do a Health Check on the setup information. - -- Helps identify missing or outdated credential information for ingestion pipeline, SSO, migration, and related issues. - -- Validates JWT authentication tokens for ingestion bots.`, - - Glossary: `- The glossary term parent can now be changed from the Details page. -- On the data assets page, glossary terms are displayed by hierarchy.`, - - 'Alerts & Notification Improvements': `- The Activity Feed provides more contextual information, removing the need to move to entity pages. -- Alerts give more accurate information about the entity, as well as conversations and tasks.`, - - Localization: `- Fixed localization issues in the confirmation logic for the delete function. -- Fixed the search index language configuration.`, - - Roles: ` -- Now, roles can be inherited from the user configuration in SSO.`, - - Search: `- You can now filter by assets without a description or an owner. -- Improved the match results for search results.`, - - Others: `- The description is auto-expanded when the data asset has no data and has the space to accommodate a lengthy description. -- User email IDs have been masked and are only visible to Admins. -- Users can filter Queries by owner, tag, and creation date in the UI. -- Added a button in the Query Editor to copy the Query. -- Improved Elasticsearch re-indexing. -- Improved the charts based on custom metrics. -- Improved the usage of the refresh token. -- Redundant scroll bars have been removed from the UI. -- Improved the bot role binding to provide more control over which roles are passed to the system bots. -- Implemented a fix for SSL migration.`, + Enhancements: `- In OpenMetadata, we support connecting the data assets to the knowledge articles. The knowledge articles that are pulled from the Alation connector have image URLs. We have enhanced the Alation connector to download and display the images in the Knowledge Articles. +- Test cases can now be filtered by Service, Tag, and Tier.`, + Changes: `- One team or multiple users can be selected as reviewers for a Glossary term., +- Updated the openmetadata.yaml to remove WebAnalyticsHandler., +- Add appType as part of the schema in the ingestion pipeline., +- We now sanitize the Activity Feed editor content.`, + Improvements: `- Fixed the lineage view for tables with many columns. +- Fixed an issue with updating the lineage edge descriptions. +- Fixed an issue with Null Schema Field. +- Fixed the glossary term review process issues. +- Fixed the Kafka SSL connection arguments. +- Fixed an issue with dbt ingestion pipeline that was occurring due to non enum values. +- Fixed an issue with Announcements. +- Fixed redirection issues for Tags and Glossary Terms. +- Fixed a minor issue with filtering the Profiler. +- Fixed the registration Issue with Event Handlers. +- Fixed the sign-in issues with SAML. +- Fixed issues with partition migration with Redshift services. +- Fixed an issue with the Quicksight connector. +- Fixed some minor issues with the user Profile page. +- Fixed some issues with the Teams page.`, }, }, ]; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.test.tsx index 109e8a1428fa..e3372ca12f8f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Applications/AppSchedule/AppSchedule.test.tsx @@ -105,7 +105,7 @@ jest.mock('../../../../context/LimitsProvider/useLimitsStore', () => ({ limits: { config: { featureLimits: [ - { name: 'application', pipelineSchedules: ['daily', 'weekly'] }, + { name: 'app', pipelineSchedules: ['daily', 'weekly'] }, ], }, }, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.test.tsx index 6072023b3159..2b22880370f3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotDetails/BotDetails.test.tsx @@ -142,6 +142,7 @@ jest.mock('../../Users/AccessTokenCard/AccessTokenCard.component', () => { jest.mock('../../../../context/LimitsProvider/useLimitsStore', () => ({ useLimitStore: jest.fn().mockImplementation(() => ({ getResourceLimit: mockGetResourceLimit, + config: { enable: true }, })), })); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/Ingestion.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/Ingestion.test.tsx index f7d2924b159a..c3dbae44a90e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/Ingestion.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Services/Ingestion/Ingestion.test.tsx @@ -123,6 +123,12 @@ jest.mock('../../../../hooks/useAirflowStatus', () => ({ }), })); +jest.mock('../../../../hoc/LimitWrapper', () => { + return jest + .fn() + .mockImplementation(({ children }) => <>LimitWrapper{children}); +}); + describe('Test Ingestion page', () => { it('Page Should render', async () => { const { container } = render( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/Users.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/Users.component.tsx index 3972298ef92c..e994936f490d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/Users.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/Users.component.tsx @@ -370,7 +370,8 @@ const Users = ({ userData, queryFilters, updateUserDetails }: Props) => { } + entityType={EntityType.PERSONA} + icon={} noDataPlaceholder={t('message.no-persona-assigned')} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.component.tsx index 1027a56fe06d..92bfe1a46782 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.component.tsx @@ -115,12 +115,17 @@ const UserProfileDetails = ({ setIsDisplayNameEdit(false); }, [userData.displayName, displayName, updateUserDetails]); + const handleCloseEditDisplayName = useCallback(() => { + setDisplayName(userData.displayName); + setIsDisplayNameEdit(false); + }, [userData.displayName]); + const displayNameRenderComponent = useMemo( () => - isDisplayNameEdit && hasEditPermission ? ( + isDisplayNameEdit ? ( setIsDisplayNameEdit(false)} + onCancel={handleCloseEditDisplayName} onSave={handleDisplayNameSave}> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.test.tsx index 1036c315882d..dca3ee95a2a8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Users/UsersProfile/UserProfileDetails/UserProfileDetails.test.tsx @@ -63,13 +63,16 @@ jest.mock('../UserProfileImage/UserProfileImage.component', () => { }); jest.mock('../../../../common/InlineEdit/InlineEdit.component', () => { - return jest.fn().mockImplementation(({ onSave, children }) => ( + return jest.fn().mockImplementation(({ onSave, onCancel, children }) => (
InlineEdit {children} +
)); }); @@ -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/AsyncSelectList/AsyncSelectList.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/AsyncSelectList.tsx index 59aad2d1877b..ffb560dfdcff 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/AsyncSelectList.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/AsyncSelectList.tsx @@ -296,7 +296,6 @@ const AsyncSelectList: FC = ({ return (