From 0fe6d139c8b502e9fc1797f9604f495a4e86f319 Mon Sep 17 00:00:00 2001 From: Fabrizzio Araya <37148755+fabrizzio-dotCMS@users.noreply.github.com> Date: Mon, 4 Nov 2024 20:23:06 -0600 Subject: [PATCH 1/3] fix(Unable to run IT from the IDE) Refs: #30564 (#30565) Normally, the rule for running an integration test is to annotate it with the JUnit4WeldRunner. Additionally, annotate the test as @ApplicationScoped. However, there was an issue with a bean that was instantiated deep within the core of the CMS, causing a failure that impacted integration tests. Particularly on `UniqueFieldValidationStrategyResolver`which has an injection problem I resolved the problem by moving the injection to the constructor Injecting private fields as follows is problematic : ``` @Inject private DBUniqueFieldValidationStrategy dbUniqueFieldValidationStrategy; ``` This problem did not surface in other scenarios, but it became visible when running directly from the integration test. Additionally, there was a public field with an @Rule in JUnit 4 that was also causing issues, which has been corrected as well. --- .../business/ESContentletAPIImpl.java | 8 +- ...UniqueFieldValidationStrategyResolver.java | 18 ++--- .../java/com/dotcms/IntegrationTestBase.java | 18 +++-- .../track/collectors/FilesCollectorTest.java | 5 ++ .../collectors/PageDetailCollectorTest.java | 6 ++ .../track/collectors/PagesCollectorTest.java | 5 ++ .../collectors/SyncVanitiesCollectorTest.java | 5 ++ .../WebEventsCollectorServiceImplTest.java | 5 +- .../ContentTypeDestroyAPIImplTest.java | 8 +- .../business/ContentTypeInitializerTest.java | 3 + .../contenttype/test/ContentResourceTest.java | 79 ++++++++++--------- 11 files changed, 90 insertions(+), 70 deletions(-) diff --git a/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java b/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java index 8f27e6eb9454..6d9a01da8fb9 100644 --- a/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java @@ -323,13 +323,7 @@ public ESContentletAPIImpl(final ElasticReadOnlyCommand readOnlyCommand) { } private static UniqueFieldValidationStrategyResolver getUniqueFieldValidationStrategyResolver() { - final Optional uniqueFieldValidationStrategyResolver = - CDIUtils.getBean(UniqueFieldValidationStrategyResolver.class); - - if (!uniqueFieldValidationStrategyResolver.isPresent()) { - throw new DotRuntimeException("Could not instance UniqueFieldValidationStrategyResolver"); - } - return uniqueFieldValidationStrategyResolver.get(); + return CDIUtils.getBeanThrows(UniqueFieldValidationStrategyResolver.class); } public ESContentletAPIImpl() { diff --git a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldValidationStrategyResolver.java b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldValidationStrategyResolver.java index 2db8a7bc7e55..f59fc8564d52 100644 --- a/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldValidationStrategyResolver.java +++ b/dotCMS/src/main/java/com/dotcms/contenttype/business/uniquefields/UniqueFieldValidationStrategyResolver.java @@ -1,14 +1,9 @@ package com.dotcms.contenttype.business.uniquefields; -import com.dotcms.cdi.CDIUtils; import com.dotcms.content.elasticsearch.business.ESContentletAPIImpl; import com.dotcms.contenttype.business.uniquefields.extratable.DBUniqueFieldValidationStrategy; -import com.dotmarketing.exception.DotRuntimeException; -import com.google.common.annotations.VisibleForTesting; - -import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Dependent; import javax.inject.Inject; -import java.util.Optional; /** * Utility class responsible for returning the appropriate {@link UniqueFieldValidationStrategy} @@ -17,17 +12,14 @@ * an {@link ESUniqueFieldValidationStrategy} is used. * */ -@ApplicationScoped +@Dependent public class UniqueFieldValidationStrategyResolver { - @Inject - private ESUniqueFieldValidationStrategy esUniqueFieldValidationStrategy; - @Inject - private DBUniqueFieldValidationStrategy dbUniqueFieldValidationStrategy; + private final ESUniqueFieldValidationStrategy esUniqueFieldValidationStrategy; - public UniqueFieldValidationStrategyResolver(){} + private final DBUniqueFieldValidationStrategy dbUniqueFieldValidationStrategy; - @VisibleForTesting + @Inject public UniqueFieldValidationStrategyResolver(final ESUniqueFieldValidationStrategy esUniqueFieldValidationStrategy, final DBUniqueFieldValidationStrategy dbUniqueFieldValidationStrategy){ this.esUniqueFieldValidationStrategy = esUniqueFieldValidationStrategy; diff --git a/dotcms-integration/src/test/java/com/dotcms/IntegrationTestBase.java b/dotcms-integration/src/test/java/com/dotcms/IntegrationTestBase.java index 427dee4fd2ea..270ec1540e84 100644 --- a/dotcms-integration/src/test/java/com/dotcms/IntegrationTestBase.java +++ b/dotcms-integration/src/test/java/com/dotcms/IntegrationTestBase.java @@ -29,10 +29,10 @@ import java.io.File; import java.io.PrintStream; import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.util.List; import org.apache.commons.io.FileUtils; import org.junit.After; -import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; @@ -51,11 +51,13 @@ public abstract class IntegrationTestBase extends BaseMessageResources { private static Boolean debugMode = Boolean.FALSE; - private final static PrintStream stdout = System.out; - private final static ByteArrayOutputStream output = new ByteArrayOutputStream(); + private static final PrintStream stdout = System.out; + private static final ByteArrayOutputStream output = new ByteArrayOutputStream(); @Rule - public TestName name = new TestName(); + public TestName getTestName() { + return new TestName(); + } @BeforeClass public static void beforeInit() throws Exception { @@ -64,12 +66,12 @@ public static void beforeInit() throws Exception { Config.setProperty("SYSTEM_EXIT_ON_STARTUP_FAILURE", false); } - protected static void setDebugMode(final boolean mode) throws UnsupportedEncodingException { + protected static void setDebugMode(final boolean mode) { debugMode = mode; if (debugMode) { - System.setOut(new PrintStream(output, true, "UTF-8")); + System.setOut(new PrintStream(output, true, StandardCharsets.UTF_8)); } } @@ -126,12 +128,12 @@ public void after() { if (DbConnectionFactory.inTransaction()) { Logger.error(IntegrationTestBase.class, - "Test " + name.getMethodName() + " has open transaction after"); + "Test " + getTestName().getMethodName() + " has open transaction after"); } if (DbConnectionFactory.connectionExists()) { Logger.error(IntegrationTestBase.class, - "Test " + name.getMethodName() + " has open connection after"); + "Test " + getTestName().getMethodName() + " has open connection after"); } //Closing the session diff --git a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/FilesCollectorTest.java b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/FilesCollectorTest.java index 99db1867f220..827b923619ea 100644 --- a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/FilesCollectorTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/FilesCollectorTest.java @@ -1,6 +1,7 @@ package com.dotcms.analytics.track.collectors; import com.dotcms.IntegrationTestBase; +import com.dotcms.JUnit4WeldRunner; import com.dotcms.LicenseTestUtil; import com.dotcms.analytics.track.matchers.FilesRequestMatcher; import com.dotcms.datagen.ContentletDataGen; @@ -18,6 +19,7 @@ import com.dotmarketing.util.PageMode; import com.dotmarketing.util.UUIDUtil; import com.dotmarketing.util.UtilMethods; +import javax.enterprise.context.ApplicationScoped; import org.junit.BeforeClass; import org.junit.Test; @@ -26,6 +28,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import org.junit.runner.RunWith; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -36,6 +39,8 @@ * @author Jose Castro * @since Oct 16th, 2024 */ +@ApplicationScoped +@RunWith(JUnit4WeldRunner.class) public class FilesCollectorTest extends IntegrationTestBase { private static final String PARENT_FOLDER_1_NAME = "parent-folder"; diff --git a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/PageDetailCollectorTest.java b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/PageDetailCollectorTest.java index 17b67a72dbf2..4586f863dde6 100644 --- a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/PageDetailCollectorTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/PageDetailCollectorTest.java @@ -1,6 +1,7 @@ package com.dotcms.analytics.track.collectors; import com.dotcms.IntegrationTestBase; +import com.dotcms.JUnit4WeldRunner; import com.dotcms.LicenseTestUtil; import com.dotcms.analytics.track.matchers.PagesAndUrlMapsRequestMatcher; import com.dotcms.contenttype.model.type.ContentType; @@ -23,6 +24,7 @@ import com.dotmarketing.util.PageMode; import com.dotmarketing.util.UUIDUtil; import com.dotmarketing.util.UtilMethods; +import javax.enterprise.context.ApplicationScoped; import org.junit.BeforeClass; import org.junit.Test; @@ -31,6 +33,7 @@ import java.net.UnknownHostException; import java.util.HashMap; import java.util.Map; +import org.junit.runner.RunWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -42,6 +45,9 @@ * @author Jose Castro * @since Oct 14th, 2024 */ + +@ApplicationScoped +@RunWith(JUnit4WeldRunner.class) public class PageDetailCollectorTest extends IntegrationTestBase { private static final String PARENT_FOLDER_1_NAME = "news"; diff --git a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/PagesCollectorTest.java b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/PagesCollectorTest.java index 245b4a54d28d..ff1de6696bf0 100644 --- a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/PagesCollectorTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/PagesCollectorTest.java @@ -1,6 +1,7 @@ package com.dotcms.analytics.track.collectors; import com.dotcms.IntegrationTestBase; +import com.dotcms.JUnit4WeldRunner; import com.dotcms.LicenseTestUtil; import com.dotcms.analytics.track.matchers.PagesAndUrlMapsRequestMatcher; import com.dotcms.contenttype.model.type.ContentType; @@ -23,6 +24,7 @@ import com.dotmarketing.util.UUIDUtil; import com.dotmarketing.util.UtilMethods; import io.vavr.API; +import javax.enterprise.context.ApplicationScoped; import org.junit.BeforeClass; import org.junit.Test; @@ -31,6 +33,7 @@ import java.net.UnknownHostException; import java.util.HashMap; import java.util.Map; +import org.junit.runner.RunWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -43,6 +46,8 @@ * @author Jose Castro * @since Oct 9th, 2024 */ +@ApplicationScoped +@RunWith(JUnit4WeldRunner.class) public class PagesCollectorTest extends IntegrationTestBase { private static final String TEST_PAGE_NAME = "index"; diff --git a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/SyncVanitiesCollectorTest.java b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/SyncVanitiesCollectorTest.java index a5626f6398dd..0c6779786881 100644 --- a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/SyncVanitiesCollectorTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/SyncVanitiesCollectorTest.java @@ -1,6 +1,7 @@ package com.dotcms.analytics.track.collectors; import com.dotcms.IntegrationTestBase; +import com.dotcms.JUnit4WeldRunner; import com.dotcms.LicenseTestUtil; import com.dotcms.datagen.ContentletDataGen; import com.dotcms.datagen.FileAssetDataGen; @@ -24,6 +25,7 @@ import com.dotmarketing.util.PageMode; import com.dotmarketing.util.UUIDUtil; import com.dotmarketing.util.UtilMethods; +import javax.enterprise.context.ApplicationScoped; import org.junit.BeforeClass; import org.junit.Test; @@ -33,6 +35,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import org.junit.runner.RunWith; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -44,6 +47,8 @@ * @author Jose Castro * @since Oct 21st, 2024 */ +@ApplicationScoped +@RunWith(JUnit4WeldRunner.class) public class SyncVanitiesCollectorTest extends IntegrationTestBase { private static final String TEST_PAGE_NAME = "index"; diff --git a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/WebEventsCollectorServiceImplTest.java b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/WebEventsCollectorServiceImplTest.java index e0953235c0a1..9798517ddb81 100644 --- a/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/WebEventsCollectorServiceImplTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/analytics/track/collectors/WebEventsCollectorServiceImplTest.java @@ -1,5 +1,6 @@ package com.dotcms.analytics.track.collectors; +import com.dotcms.DataProviderWeldRunner; import com.dotcms.IntegrationTestBase; import com.dotcms.LicenseTestUtil; import com.dotcms.analytics.app.AnalyticsApp; @@ -45,6 +46,7 @@ import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; +import javax.enterprise.context.ApplicationScoped; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -76,7 +78,8 @@ * @author Jose Castro * @since Oct 3rd, 2024 */ -@RunWith(DataProviderRunner.class) +@ApplicationScoped +@RunWith(DataProviderWeldRunner.class) public class WebEventsCollectorServiceImplTest extends IntegrationTestBase { private static final String PARENT_FOLDER_1_NAME = "parent-folder"; diff --git a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/ContentTypeDestroyAPIImplTest.java b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/ContentTypeDestroyAPIImplTest.java index d36f32207874..8de5ac5a8e9c 100644 --- a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/ContentTypeDestroyAPIImplTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/ContentTypeDestroyAPIImplTest.java @@ -1,6 +1,7 @@ package com.dotcms.contenttype.business; import com.dotcms.IntegrationTestBase; +import com.dotcms.JUnit4WeldRunner; import com.dotcms.business.CloseDBIfOpened; import com.dotcms.content.elasticsearch.business.ESSearchResults; import com.dotcms.contenttype.business.ContentTypeDestroyAPIImpl.ContentletVersionInfo; @@ -14,7 +15,6 @@ import com.dotcms.datagen.SiteDataGen; import com.dotcms.datagen.TagDataGen; import com.dotcms.datagen.TestDataUtils; -import com.dotcms.datagen.TestWorkflowUtils; import com.dotcms.util.IntegrationTestInitService; import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; @@ -37,14 +37,18 @@ import java.util.Optional; import java.util.Random; import java.util.Set; +import javax.enterprise.context.ApplicationScoped; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.junit.runner.RunWith; /** * Here we test the {@link ContentTypeDestroyAPIImpl} * @author Fabrizzio */ +@ApplicationScoped +@RunWith(JUnit4WeldRunner.class) public class ContentTypeDestroyAPIImplTest extends IntegrationTestBase { @BeforeClass @@ -140,7 +144,7 @@ public void Destroy_General_Test() throws DotDataException, DotSecurityException Assert.assertNull(deletedContentType); //Test no copy structure is left hang around - final String likeName = String.format("%s_disposed_*", name); + final String likeName = String.format("%s_disposed_*", getTestName()); int count = new DotConnect().setSQL("select count(*) as x from structure where velocity_var_name like ? ").addParam(likeName).getInt("x"); Assert.assertEquals(0, count); diff --git a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/ContentTypeInitializerTest.java b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/ContentTypeInitializerTest.java index 6fe0b8dd39ca..778e0e7c75f6 100644 --- a/dotcms-integration/src/test/java/com/dotcms/contenttype/business/ContentTypeInitializerTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/contenttype/business/ContentTypeInitializerTest.java @@ -1,10 +1,12 @@ package com.dotcms.contenttype.business; import com.dotcms.IntegrationTestBase; +import com.dotcms.JUnit4WeldRunner; import com.dotcms.content.elasticsearch.constants.ESMappingConstants; import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.datagen.ContentletDataGen; import com.dotcms.datagen.UserDataGen; +import com.dotcms.repackage.org.directwebremoting.guice.ApplicationScoped; import com.dotcms.util.IntegrationTestInitService; import com.dotmarketing.beans.Permission; import com.dotmarketing.business.APILocator; @@ -21,6 +23,7 @@ import org.junit.Test; import java.util.List; +import org.junit.runner.RunWith; /** * Test for the {@link ContentTypeInitializer} diff --git a/dotcms-integration/src/test/java/com/dotcms/contenttype/test/ContentResourceTest.java b/dotcms-integration/src/test/java/com/dotcms/contenttype/test/ContentResourceTest.java index a4a454c4d4b2..6f37b9257b24 100644 --- a/dotcms-integration/src/test/java/com/dotcms/contenttype/test/ContentResourceTest.java +++ b/dotcms-integration/src/test/java/com/dotcms/contenttype/test/ContentResourceTest.java @@ -1,5 +1,17 @@ package com.dotcms.contenttype.test; +import static com.dotcms.rest.api.v1.workflow.WorkflowTestUtil.DM_WORKFLOW; +import static com.dotmarketing.business.Role.ADMINISTRATOR; +import static com.dotmarketing.portlets.workflows.business.BaseWorkflowIntegrationTest.createContentTypeAndAssignPermissions; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.dotcms.DataProviderWeldRunner; import com.dotcms.IntegrationTestBase; import com.dotcms.contenttype.business.ContentTypeAPI; import com.dotcms.contenttype.business.FieldAPI; @@ -62,35 +74,8 @@ import com.liferay.portal.util.WebKeys; import com.liferay.util.StringPool; import com.tngtech.java.junit.dataprovider.DataProvider; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; import io.vavr.Tuple2; -import org.glassfish.jersey.internal.util.Base64; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - -import javax.servlet.ReadListener; -import javax.servlet.ServletInputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -107,19 +92,35 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.enterprise.context.ApplicationScoped; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import org.glassfish.jersey.internal.util.Base64; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; -import static com.dotcms.rest.api.v1.workflow.WorkflowTestUtil.DM_WORKFLOW; -import static com.dotmarketing.business.Role.ADMINISTRATOR; -import static com.dotmarketing.portlets.workflows.business.BaseWorkflowIntegrationTest.createContentTypeAndAssignPermissions; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@RunWith(DataProviderRunner.class) +@ApplicationScoped +@RunWith(DataProviderWeldRunner.class) public class ContentResourceTest extends IntegrationTestBase { final static String REQUIRED_NUMERIC_FIELD_NAME = "numeric"; From 70b861ad45cbda03e89fa592c410b6f096ae36c7 Mon Sep 17 00:00:00 2001 From: spbolton Date: Tue, 5 Nov 2024 14:19:24 +0000 Subject: [PATCH 2/3] chore(deployment): Update glowroot default temp and logging configuration (#30024) (#30560) This pull request introduces several changes to the `dotCMS` project, focusing on configuration updates, new development commands, and improvements to the build process. The most important changes include the addition of a new configuration file for Glowroot, updates to development commands in the `justfile`, and enhancements to the `parent/pom.xml` file to support dynamic port configurations. ### Configuration Updates: * Added a new Glowroot configuration file `glowroot.logback.xml` to the `dotCMS/pom.xml` file. * Introduced the `glowroot.logback.xml` configuration file with detailed logging settings. ### Development Commands: * Updated `justfile` to update `dev-run` command for running the application with Glowroot enabled and renamed the existing `dev-run` that added debug without suspend to `dev-run-debug`. * Removed the `dev-start` command from the `justfile` as was confusing between dev-start and dev-run ### Build Process Enhancements: * Modified `parent/pom.xml` to dynamically output the Glowroot port if the profile is enabled. ### Docker Environment Updates: * Made glowroot use the `CATALINA_TMPDIR` environment variable by default for consistent behaviour. We may consider later changing to use a DOTCMS_TMPDIR and set CATALINA_TMPDIR and others to this at a later time. when using maven and the just commands specifying -Ddocker.glowroot.enabled=true will enable glowroot on a dynamic local port when starting up to prevent port conflicts. Maven will report the full url to glowroot ui. This has been added by default in the just file to the dev-run command but can be added to any maven command that starts up the dotcms image. The local docker port mapping can be fixed using -Dglowroot.port=4000 if required. Whe using a development docker-compose use the following. GLOWROOT_WEB_UI_ENABLED is required to set the listen address to 0.0.0.0 to allow access. GLOWROOT_ENABLED: 'true' GLOWROOT_WEB_UI_ENABLED: 'true' In the K8s env we need to set the remote collector address and GLOWROOT_AGENT_ID e.g. ``` - name: GLOWROOT_ENABLED value: 'true' - name: GLOWROOT_AGENT_ID value: dotcms-qa::master - name: GLOWROOT_COLLECTOR_ADDRESS value: http://glowrootcentral.dotcmscloud.com:8181 ``` Related to #30024 (Need to ship with the updated glowroot.jar). ### Screenshots Original | Updated :-------------------------:|:-------------------------: ** original screenshot ** | ** updated screenshot ** --- .../single-node/docker-compose.yml | 4 ++ dotCMS/pom.xml | 4 ++ .../resources/container/tomcat9/bin/setenv.sh | 10 ++-- .../tomcat9/glowroot/glowroot.logback.xml | 54 +++++++++++++++++++ justfile | 10 ++-- parent/pom.xml | 22 +++++--- 6 files changed, 88 insertions(+), 16 deletions(-) create mode 100755 dotCMS/src/main/resources/container/tomcat9/glowroot/glowroot.logback.xml diff --git a/docker/docker-compose-examples/single-node/docker-compose.yml b/docker/docker-compose-examples/single-node/docker-compose.yml index 5b2f2e50318c..0362cd4aadde 100644 --- a/docker/docker-compose-examples/single-node/docker-compose.yml +++ b/docker/docker-compose-examples/single-node/docker-compose.yml @@ -57,6 +57,9 @@ services: DOT_ES_ENDPOINTS: 'https://opensearch:9200' DOT_INITIAL_ADMIN_PASSWORD: 'admin' DOT_DOTCMS_CLUSTER_ID: 'dotcms-production' + GLOWROOT_ENABLED: 'true' + GLOWROOT_WEB_UI_ENABLED: 'true' # Enable glowroot web ui on localhost. do not use in production + #CUSTOM_STARTER_URL: 'https://repo.dotcms.com/artifactory/libs-release-local/com/dotcms/starter/20240719/starter-20240719.zip' depends_on: - db @@ -70,6 +73,7 @@ services: ports: - "8082:8082" - "8443:8443" + - "4000:4000" # Glowroot web ui if enabled networks: db_net: diff --git a/dotCMS/pom.xml b/dotCMS/pom.xml index 5cb123e632cd..79800156bf7a 100644 --- a/dotCMS/pom.xml +++ b/dotCMS/pom.xml @@ -1992,6 +1992,10 @@ ${tomcat9-overrides}/glowroot/local-web/admin.json glowroot/local-web + + ${tomcat9-overrides}/glowroot/glowroot.logback.xml + glowroot + ${tomcat9-overrides}/bin/build.conf bin diff --git a/dotCMS/src/main/resources/container/tomcat9/bin/setenv.sh b/dotCMS/src/main/resources/container/tomcat9/bin/setenv.sh index 7abdddc52cbd..302fd96abedd 100644 --- a/dotCMS/src/main/resources/container/tomcat9/bin/setenv.sh +++ b/dotCMS/src/main/resources/container/tomcat9/bin/setenv.sh @@ -62,10 +62,14 @@ fi # GLOWROOT_AGENT_ID: If set, specifies the agent ID for Glowroot and enables multi-directory mode. # GLOWROOT_COLLECTOR_ADDRESS: If set, specifies the collector address for Glowroot. +if [ -z "$CATALINA_TMPDIR" ]; then + CATALINA_TMPDIR="$CATALINA_HOME/temp" +fi + add_glowroot_agent() { if ! echo "$CATALINA_OPTS" | grep -q '\-javaagent:.*glowroot\.jar'; then - echo "Adding Glowroot agent to CATALINA_OPTS" if [ "$GLOWROOT_ENABLED" = "true" ]; then + echo "Adding Glowroot agent to CATALINA_OPTS" export CATALINA_OPTS="$CATALINA_OPTS -javaagent:$CATALINA_HOME/glowroot/glowroot.jar" export GLOWROOT_SHARED_FOLDER="/data/shared/glowroot" @@ -76,8 +80,8 @@ add_glowroot_agent() { fi CATALINA_OPTS="$CATALINA_OPTS -Dglowroot.conf.dir=$GLOWROOT_CONF_DIR" # We may want to modify these defaults - CATALINA_OPTS="$CATALINA_OPTS -Dglowroot.log.dir=${GLOWROOT_LOG_DIR:=$GLOWROOT_CONF_DIR/logs}" - CATALINA_OPTS="$CATALINA_OPTS -Dglowroot.tmp.dir=${GLOWROOT_TMP_DIR:=$GLOWROOT_CONF_DIR/tmp}" + CATALINA_OPTS="$CATALINA_OPTS -Dglowroot.tmp.dir=${GLOWROOT_TMP_DIR:=$CATALINA_TMPDIR}" + # Only used if when not using a collector CATALINA_OPTS="$CATALINA_OPTS -Dglowroot.data.dir=${GLOWROOT_DATA_DIR:=$GLOWROOT_SHARED_FOLDER/data}" # Set GLOWROOT_AGENT_ID and enable multi-directory mode if defined diff --git a/dotCMS/src/main/resources/container/tomcat9/glowroot/glowroot.logback.xml b/dotCMS/src/main/resources/container/tomcat9/glowroot/glowroot.logback.xml new file mode 100755 index 000000000000..f5b4ac530aee --- /dev/null +++ b/dotCMS/src/main/resources/container/tomcat9/glowroot/glowroot.logback.xml @@ -0,0 +1,54 @@ + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/justfile b/justfile index de5b8380e59a..0730a5308de5 100644 --- a/justfile +++ b/justfile @@ -59,10 +59,12 @@ build-select-module-deps module=":dotcms-core": ./mvnw install -pl {{ module }} --am -DskipTests=true # Development Commands +dev-run: + ./mvnw -pl :dotcms-core -Pdocker-start -Ddocker.glowroot.enabled=true # Starts the dotCMS application in a Docker container on a dynamic port, running in the foreground -dev-run: - ./mvnw -pl :dotcms-core -Pdocker-start,debug-suspend +dev-run-debug: + ./mvnw -pl :dotcms-core -Pdocker-start,debug # Maps paths in the docker container to local paths, useful for development dev-run-map-dev-paths: @@ -72,10 +74,6 @@ dev-run-map-dev-paths: dev-run-debug-suspend port="8082": ./mvnw -pl :dotcms-core -Pdocker-start,debug-suspend -Dtomcat.port={{ port }} -# Starts the dotCMS Docker container in the background, running on random port -dev-start: - ./mvnw -pl :dotcms-core -Pdocker-start - # Starts the dotCMS Docker container in the background dev-start-on-port port="8082": ./mvnw -pl :dotcms-core -Pdocker-start -Dtomcat.port={{ port }} diff --git a/parent/pom.xml b/parent/pom.xml index 455583000540..e453be075d69 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -196,7 +196,6 @@ false n 5005 - false -agentlib:jdwp=transport=dt_socket,server=y,suspend=${debug.suspend.flag},address=*:${debug.port} @@ -1478,14 +1477,24 @@ validate - + - + + - + - + + + + + + + + + + @@ -1584,8 +1593,7 @@ true - - + From 7a2d6bcf8e3329afb9b24c283ab1834daca10422 Mon Sep 17 00:00:00 2001 From: Humberto Morera <31667212+hmoreras@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:31:46 -0600 Subject: [PATCH 3/3] implementation (Content Analytics Search): #30233 Display a "wall" if the analytics app is not running (#30555) ### Proposed Changes * Implement a wall message display that appears when, Analytics configuration is missing and Configuration parameters are invalid. ### Screenshots ![image (5)](https://github.com/user-attachments/assets/b9bd52d0-03a6-4669-aa76-472d3b638bb9) --- .../dotcms-ui/src/app/app-routing.module.ts | 9 +- .../dot-analytics-search.component.html | 84 ++++---- .../dot-analytics-search.component.spec.ts | 153 ++++++++++---- .../dot-analytics-search.component.ts | 31 +-- .../store/dot-analytics-search.store.spec.ts | 189 ++++++++++++++++-- .../lib/store/dot-analytics-search.store.ts | 90 +++++++-- .../lib/{dot-analytics-search => }/utils.ts | 0 core-web/libs/ui/src/index.ts | 1 + ...tics-health-check.resolver.service.spec.ts | 40 ++++ ...analytics-health-check.resolver.service.ts | 10 + .../WEB-INF/messages/Language.properties | 8 +- 11 files changed, 458 insertions(+), 157 deletions(-) rename core-web/libs/portlets/dot-analytics-search/portlet/src/lib/{dot-analytics-search => }/utils.ts (100%) create mode 100644 core-web/libs/ui/src/lib/resolvers/dot-analytics-health-check.resolver.service.spec.ts create mode 100644 core-web/libs/ui/src/lib/resolvers/dot-analytics-health-check.resolver.service.ts diff --git a/core-web/apps/dotcms-ui/src/app/app-routing.module.ts b/core-web/apps/dotcms-ui/src/app/app-routing.module.ts index 4da37e11f302..3ee7c759dfc4 100644 --- a/core-web/apps/dotcms-ui/src/app/app-routing.module.ts +++ b/core-web/apps/dotcms-ui/src/app/app-routing.module.ts @@ -14,8 +14,8 @@ import { DotLogOutContainerComponent } from '@components/login/dot-logout-contai import { DotLoginPageComponent } from '@components/login/main/dot-login-page.component'; import { MainCoreLegacyComponent } from '@components/main-core-legacy/main-core-legacy-component'; import { MainComponentLegacyComponent } from '@components/main-legacy/main-legacy.component'; -import { EmaAppConfigurationService } from '@dotcms/data-access'; -import { DotEnterpriseLicenseResolver } from '@dotcms/ui'; +import { DotExperimentsService, EmaAppConfigurationService } from '@dotcms/data-access'; +import { dotAnalyticsHealthCheckResolver, DotEnterpriseLicenseResolver } from '@dotcms/ui'; import { DotCustomReuseStrategyService } from '@shared/dot-custom-reuse-strategy/dot-custom-reuse-strategy.service'; import { AuthGuardService } from './api/services/guards/auth-guard.service'; @@ -75,9 +75,10 @@ const PORTLETS_ANGULAR: Route[] = [ path: 'analytics-search', canActivate: [MenuGuardService], canActivateChild: [MenuGuardService], - providers: [DotEnterpriseLicenseResolver], + providers: [DotEnterpriseLicenseResolver, DotExperimentsService], resolve: { - isEnterprise: DotEnterpriseLicenseResolver + isEnterprise: DotEnterpriseLicenseResolver, + healthCheck: dotAnalyticsHealthCheckResolver }, data: { reuseRoute: false diff --git a/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/dot-analytics-search/dot-analytics-search.component.html b/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/dot-analytics-search/dot-analytics-search.component.html index 4662fefafe90..2a3118c5f369 100644 --- a/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/dot-analytics-search/dot-analytics-search.component.html +++ b/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/dot-analytics-search/dot-analytics-search.component.html @@ -1,44 +1,48 @@ - - -
-
-

{{ 'analytics.search.query' | dm }}

- -
- -
- +@if (store.wallEmptyConfig()) { + +} @else { + + +
+
+

{{ 'analytics.search.query' | dm }}

- -
-
-
- - @if ($results() === null) { - - } @else { -
+ icon="pi pi-question-circle"> +
+ [(ngModel)]="queryEditor" + [options]="ANALYTICS_MONACO_EDITOR_OPTIONS" + (ngModelChange)="handleQueryChange($event)" + data-testId="query-editor"> +
+ + + +
- } -
-
+ + + @if ($results() === null) { + + } @else { +
+ +
+ } +
+ +} diff --git a/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/dot-analytics-search/dot-analytics-search.component.spec.ts b/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/dot-analytics-search/dot-analytics-search.component.spec.ts index 5ca4d3de00a1..c5207fdbb57a 100644 --- a/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/dot-analytics-search/dot-analytics-search.component.spec.ts +++ b/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/dot-analytics-search/dot-analytics-search.component.spec.ts @@ -6,7 +6,16 @@ import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ActivatedRoute } from '@angular/router'; -import { DotAnalyticsSearchService, DotHttpErrorManagerService } from '@dotcms/data-access'; +import { Splitter } from 'primeng/splitter'; + +import { + DotAnalyticsSearchService, + DotHttpErrorManagerService, + DotMessageService +} from '@dotcms/data-access'; +import { HealthStatusTypes } from '@dotcms/dotcms-models'; +import { DotEmptyContainerComponent } from '@dotcms/ui'; +import { MockDotMessageService } from '@dotcms/utils-testing'; import { DotAnalyticsSearchComponent } from './dot-analytics-search.component'; @@ -25,53 +34,111 @@ describe('DotAnalyticsSearchComponent', () => { providers: [ provideHttpClient(), provideHttpClientTesting(), + + mockProvider(DotHttpErrorManagerService), { - provide: ActivatedRoute, - useValue: { - snapshot: { - data: { - isEnterprise: true - } - } - } - }, - mockProvider(DotHttpErrorManagerService) + provide: DotMessageService, + useValue: new MockDotMessageService({ + 'analytics.search.no.configured': 'No configuration found', + 'analytics.search.no.configured.subtitle': + 'Please configure the analytics search', + 'analytics.search.config.error': 'Configuration error', + 'analytics.search.config.error.subtitle': + 'There was an error in the configuration', + 'analytics.search.no.licence': 'No license found', + 'analytics.search.no.license.subtitle': 'Please provide a valid license', + 'analytics.search.no.results': 'No results', + 'analytics.search.execute.results': 'Execute a query to get results' + }) + } ] }); - beforeEach(() => { - spectator = createComponent(); - store = spectator.inject(DotAnalyticsSearchStore, true); - }); - - it('should initialize store with enterprise state on init', () => { - const initLoadSpy = jest.spyOn(store, 'initLoad'); - spectator.component.ngOnInit(); - - expect(initLoadSpy).toHaveBeenCalledWith(true); - }); - - it('should call getResults with valid JSON', () => { - const getResultsSpy = jest.spyOn(store, 'getResults'); - - spectator.component.queryEditor = '{"measures": ["request.count"]}'; - spectator.component.handleQueryChange('{"measures": ["request.count"]}'); - spectator.detectChanges(); - - const button = spectator.query(byTestId('run-query')) as HTMLButtonElement; - spectator.click(button); - - expect(getResultsSpy).toHaveBeenCalledWith({ measures: ['request.count'] }); + describe('when healthCheck is "OK"', () => { + beforeEach(() => { + spectator = createComponent({ + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + data: { + isEnterprise: true, + healthCheck: HealthStatusTypes.OK + } + } + } + } + ] + }); + store = spectator.inject(DotAnalyticsSearchStore, true); + }); + + it('should render dot-empty-container with the correct configuration', () => { + spectator.detectChanges(); + const dotEmptyContainer = spectator.query(DotEmptyContainerComponent); + expect(dotEmptyContainer).toExist(); + expect(dotEmptyContainer.configuration).toEqual({ + subtitle: 'Execute a query to get results', + icon: 'pi-search', + title: 'No results' + }); + }); + + it('should call getResults with valid JSON', () => { + const getResultsSpy = jest.spyOn(store, 'getResults'); + + spectator.component.queryEditor = '{"measures": ["request.count"]}'; + spectator.component.handleQueryChange('{"measures": ["request.count"]}'); + spectator.detectChanges(); + + const button = spectator.query(byTestId('run-query')) as HTMLButtonElement; + spectator.click(button); + + expect(getResultsSpy).toHaveBeenCalledWith({ measures: ['request.count'] }); + }); + + it('should not call getResults with invalid JSON', () => { + spectator.component.queryEditor = 'invalid json'; + spectator.component.handleQueryChange('invalid json'); + spectator.detectChanges(); + + const button = spectator.query(byTestId('run-query')) as HTMLButtonElement; + spectator.click(button); + + expect(button).toBeDisabled(); + }); + + it('should render the Splitter when healthCheck is "OK"', () => { + spectator.detectChanges(); + expect(spectator.query(Splitter)).toExist(); + }); }); - it('should not call getResults with invalid JSON', () => { - spectator.component.queryEditor = 'invalid json'; - spectator.component.handleQueryChange('invalid json'); - spectator.detectChanges(); - - const button = spectator.query(byTestId('run-query')) as HTMLButtonElement; - spectator.click(button); - - expect(button).toBeDisabled(); + describe('when healthCheck is "NOT_CONFIGURED"', () => { + beforeEach(() => { + spectator = createComponent({ + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + data: { + isEnterprise: true, + healthCheck: HealthStatusTypes.NOT_CONFIGURED + } + } + } + } + ] + }); + store = spectator.inject(DotAnalyticsSearchStore, true); + }); + + it('should render dot-empty-container', () => { + spectator.detectChanges(); + const dotEmptyContainer = spectator.query(DotEmptyContainerComponent); + expect(dotEmptyContainer).toExist(); + }); }); }); diff --git a/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/dot-analytics-search/dot-analytics-search.component.ts b/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/dot-analytics-search/dot-analytics-search.component.ts index ce3349a5ebb8..2dd5084e039a 100644 --- a/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/dot-analytics-search/dot-analytics-search.component.ts +++ b/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/dot-analytics-search/dot-analytics-search.component.ts @@ -4,23 +4,21 @@ import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'; import { CommonModule } from '@angular/common'; import { Component, computed, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; import { ButtonDirective } from 'primeng/button'; import { DropdownModule } from 'primeng/dropdown'; import { SplitterModule } from 'primeng/splitter'; import { TooltipModule } from 'primeng/tooltip'; -import { DotAnalyticsSearchService, DotMessageService } from '@dotcms/data-access'; -import { DotEmptyContainerComponent, DotMessagePipe, PrincipalConfiguration } from '@dotcms/ui'; +import { DotAnalyticsSearchService } from '@dotcms/data-access'; +import { DotEmptyContainerComponent, DotMessagePipe } from '@dotcms/ui'; +import { DotAnalyticsSearchStore } from '../store/dot-analytics-search.store'; import { ANALYTICS_MONACO_EDITOR_OPTIONS, ANALYTICS_RESULTS_MONACO_EDITOR_OPTIONS, isValidJson -} from './utils'; - -import { DotAnalyticsSearchStore } from '../store/dot-analytics-search.store'; +} from '../utils'; @Component({ selector: 'lib-dot-analytics-search', @@ -41,31 +39,16 @@ import { DotAnalyticsSearchStore } from '../store/dot-analytics-search.store'; styleUrl: './dot-analytics-search.component.scss' }) export class DotAnalyticsSearchComponent { - private readonly route = inject(ActivatedRoute); ANALYTICS_MONACO_EDITOR_OPTIONS = ANALYTICS_MONACO_EDITOR_OPTIONS; ANALYTICS__RESULTS_MONACO_EDITOR_OPTIONS = ANALYTICS_RESULTS_MONACO_EDITOR_OPTIONS; readonly store = inject(DotAnalyticsSearchStore); - /** - * Represents the DotMessageService instance. - */ - readonly #dotMessageService = inject(DotMessageService); - /** * The content of the query editor. */ queryEditor = ''; - /** - * Signal representing the empty state configuration. - */ - $emptyState = signal({ - title: this.#dotMessageService.get('analytics.search.no.results'), - icon: 'pi-search', - subtitle: this.#dotMessageService.get('analytics.search.execute.results') - }); - /** * Signal representing whether the query editor content is valid JSON. */ @@ -80,12 +63,6 @@ export class DotAnalyticsSearchComponent { return results ? JSON.stringify(results, null, 2) : null; }); - ngOnInit() { - const { isEnterprise } = this.route.snapshot.data; - - this.store.initLoad(isEnterprise); - } - /** * Handles the request to get results based on the query editor content. * Validates the JSON and calls the store's getResults method if valid. diff --git a/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/store/dot-analytics-search.store.spec.ts b/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/store/dot-analytics-search.store.spec.ts index 77bb6427e3d9..5c96eb93941d 100644 --- a/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/store/dot-analytics-search.store.spec.ts +++ b/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/store/dot-analytics-search.store.spec.ts @@ -7,9 +7,15 @@ import { import { of, throwError } from 'rxjs'; import { HttpErrorResponse } from '@angular/common/http'; +import { ActivatedRoute } from '@angular/router'; -import { DotAnalyticsSearchService, DotHttpErrorManagerService } from '@dotcms/data-access'; -import { AnalyticsQueryType, ComponentStatus } from '@dotcms/dotcms-models'; +import { + DotAnalyticsSearchService, + DotHttpErrorManagerService, + DotMessageService +} from '@dotcms/data-access'; +import { AnalyticsQueryType, ComponentStatus, HealthStatusTypes } from '@dotcms/dotcms-models'; +import { MockDotMessageService } from '@dotcms/utils-testing'; import { DotAnalyticsSearchStore } from './dot-analytics-search.store'; @@ -34,37 +40,85 @@ describe('DotAnalyticsSearchStore', () => { let store: InstanceType; let dotAnalyticsSearchService: SpyObject; let dotHttpErrorManagerService: SpyObject; + const createService = createServiceFactory({ service: DotAnalyticsSearchStore, providers: [ mockProvider(DotAnalyticsSearchService), - mockProvider(DotHttpErrorManagerService) + mockProvider(DotHttpErrorManagerService), + { + provide: DotMessageService, + useValue: new MockDotMessageService({ + 'analytics.search.no.configured': 'No configuration found', + 'analytics.search.no.configured.subtitle': + 'Please configure the analytics search', + 'analytics.search.config.error': 'Configuration error', + 'analytics.search.config.error.subtitle': + 'There was an error in the configuration', + 'analytics.search.no.licence': 'No license found', + 'analytics.search.no.license.subtitle': 'Please provide a valid license', + 'analytics.search.no.results': 'No results', + 'analytics.search.execute.results': 'Execute a query to get results' + }) + } ] }); - beforeEach(() => { - spectator = createService(); - store = spectator.service; - dotAnalyticsSearchService = spectator.inject(DotAnalyticsSearchService); - dotHttpErrorManagerService = spectator.inject(DotHttpErrorManagerService); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); + describe('initial state', () => { + beforeEach(() => { + spectator = createService({ + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + data: { + isEnterprise: true, + healthCheck: HealthStatusTypes.OK + } + } + } + } + ] + }); + store = spectator.service; + }); - it('should initialize with default state', () => { - expect(store.isEnterprise()).toEqual(false); - expect(store.results()).toEqual(null); - expect(store.query()).toEqual({ value: null, type: AnalyticsQueryType.CUBE }); - expect(store.state()).toEqual(ComponentStatus.INIT); - expect(store.errorMessage()).toEqual(''); + it('should initialize with default state', () => { + expect(store.isEnterprise()).toEqual(true); + expect(store.results()).toEqual(null); + expect(store.query()).toEqual({ value: null, type: AnalyticsQueryType.CUBE }); + expect(store.state()).toEqual(ComponentStatus.INIT); + expect(store.healthCheck()).toEqual(HealthStatusTypes.OK); + expect(store.wallEmptyConfig()).toEqual(null); + expect(store.emptyResultsConfig()).toEqual({ + icon: 'pi-search', + subtitle: 'Execute a query to get results', + title: 'No results' + }); + }); }); describe('withMethods', () => { - it('should set initial state', () => { - store.initLoad(true); - expect(store.isEnterprise()).toEqual(true); + beforeEach(() => { + spectator = createService({ + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + data: { + isEnterprise: true, + healthCheck: HealthStatusTypes.OK + } + } + } + } + ] + }); + store = spectator.service; + dotAnalyticsSearchService = spectator.inject(DotAnalyticsSearchService); + dotHttpErrorManagerService = spectator.inject(DotHttpErrorManagerService); }); it('should perform a POST request to the base URL and return results', () => { @@ -90,4 +144,95 @@ describe('DotAnalyticsSearchStore', () => { expect(dotHttpErrorManagerService.handle).toHaveBeenCalled(); }); }); + + describe('onInit', () => { + describe('when isEnterprise is true', () => { + describe('and healthCheck is "NOT_CONFIGURED"', () => { + beforeEach(() => { + spectator = createService({ + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + data: { + isEnterprise: true, + healthCheck: HealthStatusTypes.NOT_CONFIGURED + } + } + } + } + ] + }); + store = spectator.service; + }); + + it('should initialize with default state', () => { + expect(store.wallEmptyConfig()).toEqual({ + icon: 'pi-search', + subtitle: 'Please configure the analytics search', + title: 'No configuration found' + }); + }); + }); + + describe('and healthCheck is "CONFIGURATION_ERROR"', () => { + beforeEach(() => { + spectator = createService({ + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + data: { + isEnterprise: true, + healthCheck: HealthStatusTypes.CONFIGURATION_ERROR + } + } + } + } + ] + }); + store = spectator.service; + }); + + it('should set the wall empty configuration', () => { + expect(store.wallEmptyConfig()).toEqual({ + icon: 'pi-search', + subtitle: 'There was an error in the configuration', + title: 'Configuration error' + }); + }); + }); + }); + + describe('when isEnterprise is false', () => { + beforeEach(() => { + spectator = createService({ + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + data: { + isEnterprise: false, + healthCheck: HealthStatusTypes.NOT_CONFIGURED + } + } + } + } + ] + }); + store = spectator.service; + }); + + it('should set the wall empty configuration', () => { + expect(store.wallEmptyConfig()).toEqual({ + icon: 'pi-search', + subtitle: 'Please provide a valid license', + title: 'No license found' + }); + }); + }); + }); }); diff --git a/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/store/dot-analytics-search.store.ts b/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/store/dot-analytics-search.store.ts index 75339a88a8bf..912ba89ec8be 100644 --- a/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/store/dot-analytics-search.store.ts +++ b/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/store/dot-analytics-search.store.ts @@ -1,16 +1,27 @@ import { JsonObject } from '@angular-devkit/core'; import { tapResponse } from '@ngrx/component-store'; -import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; +import { patchState, signalStore, withHooks, withMethods, withState } from '@ngrx/signals'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { pipe } from 'rxjs'; import { HttpErrorResponse } from '@angular/common/http'; import { inject } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; import { switchMap, tap } from 'rxjs/operators'; -import { DotAnalyticsSearchService, DotHttpErrorManagerService } from '@dotcms/data-access'; -import { AnalyticsQueryType, ComponentStatus } from '@dotcms/dotcms-models'; +import { + DotAnalyticsSearchService, + DotHttpErrorManagerService, + DotMessageService +} from '@dotcms/data-access'; +import { AnalyticsQueryType, ComponentStatus, HealthStatusTypes } from '@dotcms/dotcms-models'; +import { PrincipalConfiguration } from '@dotcms/ui'; + +interface RouteData { + isEnterprise: boolean; + healthCheck: HealthStatusTypes; +} /** * Type definition for the state of the DotContentAnalytics. @@ -23,7 +34,9 @@ export type DotContentAnalyticsState = { type: AnalyticsQueryType; }; state: ComponentStatus; - errorMessage: string; + healthCheck: HealthStatusTypes; + wallEmptyConfig: PrincipalConfiguration | null; + emptyResultsConfig: PrincipalConfiguration | null; }; /** @@ -37,7 +50,9 @@ export const initialState: DotContentAnalyticsState = { type: AnalyticsQueryType.CUBE }, state: ComponentStatus.INIT, - errorMessage: '' + healthCheck: HealthStatusTypes.NOT_CONFIGURED, + wallEmptyConfig: null, + emptyResultsConfig: null }; /** @@ -51,17 +66,6 @@ export const DotAnalyticsSearchStore = signalStore( analyticsSearchService = inject(DotAnalyticsSearchService), dotHttpErrorManagerService = inject(DotHttpErrorManagerService) ) => ({ - /** - * Initializes the state with the given enterprise status. - * @param isEnterprise - Boolean indicating if the user is an enterprise user. - */ - initLoad: (isEnterprise: boolean) => { - patchState(store, { - ...initialState, - isEnterprise - }); - }, - /** * Fetches the results based on the current query. * @param query - The query to fetch results for. @@ -84,8 +88,7 @@ export const DotAnalyticsSearchStore = signalStore( }, error: (error: HttpErrorResponse) => { patchState(store, { - state: ComponentStatus.ERROR, - errorMessage: 'Error loading data' + state: ComponentStatus.ERROR }); dotHttpErrorManagerService.handle(error); @@ -96,5 +99,54 @@ export const DotAnalyticsSearchStore = signalStore( ) ) }) - ) + ), + withHooks({ + /** + * Hook that runs on initialization of the store. + * Sets the initial state based on the route data and messages. + * @param store - The store instance. + */ + onInit: (store) => { + const activatedRoute = inject(ActivatedRoute); + const dotMessageService = inject(DotMessageService); + + const { isEnterprise, healthCheck } = activatedRoute.snapshot.data as RouteData; + + const configurationMap = { + [HealthStatusTypes.NOT_CONFIGURED]: { + title: dotMessageService.get('analytics.search.no.configured'), + icon: 'pi-search', + subtitle: dotMessageService.get('analytics.search.no.configured.subtitle') + }, + [HealthStatusTypes.CONFIGURATION_ERROR]: { + title: dotMessageService.get('analytics.search.config.error'), + icon: 'pi-search', + subtitle: dotMessageService.get('analytics.search.config.error.subtitle') + }, + [HealthStatusTypes.OK]: null, + ['noLicense']: { + title: dotMessageService.get('analytics.search.no.licence'), + icon: 'pi-search', + subtitle: dotMessageService.get('analytics.search.no.license.subtitle') + } + }; + + const emptyResultsConfig = { + title: dotMessageService.get('analytics.search.no.results'), + icon: 'pi-search', + subtitle: dotMessageService.get('analytics.search.execute.results') + }; + + const wallEmptyConfig = isEnterprise + ? configurationMap[healthCheck] + : configurationMap['noLicense']; + + patchState(store, { + isEnterprise, + healthCheck, + wallEmptyConfig, + emptyResultsConfig + }); + } + }) ); diff --git a/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/dot-analytics-search/utils.ts b/core-web/libs/portlets/dot-analytics-search/portlet/src/lib/utils.ts similarity index 100% rename from core-web/libs/portlets/dot-analytics-search/portlet/src/lib/dot-analytics-search/utils.ts rename to core-web/libs/portlets/dot-analytics-search/portlet/src/lib/utils.ts diff --git a/core-web/libs/ui/src/index.ts b/core-web/libs/ui/src/index.ts index eedde6905ae6..2748fb591165 100644 --- a/core-web/libs/ui/src/index.ts +++ b/core-web/libs/ui/src/index.ts @@ -64,6 +64,7 @@ export * from './lib/pipes/dot-diff/dot-diff.pipe'; export * from './lib/resolvers/dot-portlet-have-license.resolver'; export * from './lib/resolvers/dot-enterprise-license-resolver.service'; export * from './lib/resolvers/dot-push-publish-enviroments-resolver.service'; +export * from './lib/resolvers/dot-analytics-health-check.resolver.service'; // Validators export * from './lib/validators/dotValidators'; diff --git a/core-web/libs/ui/src/lib/resolvers/dot-analytics-health-check.resolver.service.spec.ts b/core-web/libs/ui/src/lib/resolvers/dot-analytics-health-check.resolver.service.spec.ts new file mode 100644 index 000000000000..271546428922 --- /dev/null +++ b/core-web/libs/ui/src/lib/resolvers/dot-analytics-health-check.resolver.service.spec.ts @@ -0,0 +1,40 @@ +// dot-analytics-health-check.resolver.service.spec.ts + +import { of } from 'rxjs'; + +import { EnvironmentInjector, runInInjectionContext } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { RouterStateSnapshot } from '@angular/router'; + +import { DotExperimentsService } from '@dotcms/data-access'; +import { HealthStatusTypes } from '@dotcms/dotcms-models'; +import { dotAnalyticsHealthCheckResolver } from '@dotcms/ui'; + +describe('dotAnalyticsHealthCheckResolver', () => { + let dotExperimentsService: DotExperimentsService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { + provide: DotExperimentsService, + useValue: { healthCheck: () => of(HealthStatusTypes.OK) } + } + ] + }); + + dotExperimentsService = TestBed.inject(DotExperimentsService); + }); + + it('should return HealthStatusTypes.OK when healthCheck is successful', () => { + const resolver = runInInjectionContext(TestBed.inject(EnvironmentInjector), () => + dotAnalyticsHealthCheckResolver(null, {} as RouterStateSnapshot) + ); + + spyOn(dotExperimentsService, 'healthCheck').and.returnValue(of(HealthStatusTypes.OK)); + + resolver.subscribe((healthStatus) => { + expect(healthStatus).toBe(HealthStatusTypes.OK); + }); + }); +}); diff --git a/core-web/libs/ui/src/lib/resolvers/dot-analytics-health-check.resolver.service.ts b/core-web/libs/ui/src/lib/resolvers/dot-analytics-health-check.resolver.service.ts new file mode 100644 index 000000000000..968c117997b4 --- /dev/null +++ b/core-web/libs/ui/src/lib/resolvers/dot-analytics-health-check.resolver.service.ts @@ -0,0 +1,10 @@ +import { inject } from '@angular/core'; +import { RouterStateSnapshot } from '@angular/router'; + +import { DotExperimentsService } from '@dotcms/data-access'; + +export const dotAnalyticsHealthCheckResolver = (_route, _state: RouterStateSnapshot) => { + const dotExperimentsService = inject(DotExperimentsService); + + return dotExperimentsService.healthCheck(); +}; diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 58f5a8d9a657..5a33735b9352 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -5825,5 +5825,9 @@ analytics.search.results=Results analytics.search.no.results=No Results analytics.search.execute.results=Execute a query to get results analytics.search.valid.json=The query must be a valid JSON - - +analytics.search.no.licence=Content Analytics Not Enabled +analytics.search.no.licence.subtitle=Content Analytics is an Enterprise feature. Please contact dotCMS to upgrade to an Enterprise license and enable Content Analytics. +analytics.search.no.configured=Content Analytics Not Enabled +analytics.search.no.configured.subtitle=Please contact your dotCMS representative to enable the Content Analytics feature. +analytics.search.config.error=Content Analytics App not Configured Properly +analytics.search.config.error.subtitle=Please check your Content Analytics configuration, or contact dotCMS Support for assistance