-
Notifications
You must be signed in to change notification settings - Fork 480
Issue 34210 task enable usage dashboard tool by default in #34543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
spbolton
merged 9 commits into
main
from
issue-34210-task-enable-usage-dashboard-tool-by-default-in
Feb 10, 2026
+286
−2
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
47a3457
fix: use COUNT query pattern in forceRun for consistency
dsantos-dcms 5605317
fix: update author to Denis Santos
dsantos-dcms 36db4c2
docs: clarify task adds Usage portlet to System menu if not exists
dsantos-dcms d0cfedf
Merge branch 'main' into issue-34210-task-enable-usage-dashboard-tool…
dcolina 3f3edf6
test: add unit test for Task260206AddUsagePortletToMenu
dcolina 839bddd
test: add Task260206AddUsagePortletToMenuTest to MainSuite3a
dsantos-dcms eb6b232
Merge branch 'main' into issue-34210-task-enable-usage-dashboard-tool…
yolabingo ac8375d
Merge branch 'main' into issue-34210-task-enable-usage-dashboard-tool…
yolabingo 742b80b
Merge branch 'main' into issue-34210-task-enable-usage-dashboard-tool…
yolabingo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
138 changes: 138 additions & 0 deletions
138
dotCMS/src/main/java/com/dotmarketing/startup/runonce/Task260206AddUsagePortletToMenu.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| package com.dotmarketing.startup.runonce; | ||
|
|
||
| import com.dotcms.exception.ExceptionUtil; | ||
| import com.dotmarketing.business.CacheLocator; | ||
| import com.dotmarketing.common.db.DotConnect; | ||
| import com.dotmarketing.exception.DotDataException; | ||
| import com.dotmarketing.startup.StartupTask; | ||
| import com.dotmarketing.util.Logger; | ||
| import com.dotmarketing.util.UUIDUtil; | ||
| import com.dotmarketing.util.UtilMethods; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| import static com.dotmarketing.util.PortletID.SITES; | ||
| import static com.dotmarketing.util.PortletID.USAGE; | ||
|
|
||
| /** | ||
| * Adds the custom 'Usage' portlet to the System menu, if it doesn't exist anywhere in the system. | ||
| * Falls back to Marketing menu, or the menu containing the Sites portlet if System is not available. | ||
| * | ||
| * @author Denis Santos | ||
| * @since Feb 6th, 2026 | ||
| */ | ||
| public class Task260206AddUsagePortletToMenu implements StartupTask { | ||
|
|
||
| @Override | ||
| public boolean forceRun() { | ||
| try { | ||
| final String layoutID = this.getMenuGroupForPortlet(); | ||
| if (UtilMethods.isNotSet(layoutID)) { | ||
| Logger.warn(this, "The 'Usage' portlet could not be automatically added to any of the expected Menu Groups. " + | ||
| "Please add it manually"); | ||
| return false; | ||
| } | ||
| final int count = new DotConnect() | ||
| .setSQL("SELECT COUNT(portlet_id) AS count FROM cms_layouts_portlets WHERE portlet_id = ?") | ||
| .addParam(USAGE.toString()) | ||
| .getInt("count"); | ||
| return count == 0; | ||
| } catch (final DotDataException e) { | ||
| Logger.error(this, String.format("An error occurred when adding the 'Usage' portlet. " + | ||
| "Please add it manually: %s", ExceptionUtil.getErrorMessage(e)), e); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Adds the custom {@code Usage} portlet to the appropriate Menu Group. | ||
| * | ||
| * @throws DotDataException An error occurred when adding the 'Usage' portlet. | ||
| */ | ||
| @Override | ||
| public void executeUpgrade() throws DotDataException { | ||
| Logger.info(this, "Adding the 'Usage' portlet to existing Menu Group(s)"); | ||
| final String layoutID = this.getMenuGroupForPortlet(); | ||
| if (null != layoutID && !layoutID.isEmpty()) { | ||
| final boolean isLayoutMissingUsagePortlet = 0 == new DotConnect() | ||
| .setSQL("SELECT COUNT(portlet_id) AS count FROM cms_layouts_portlets WHERE layout_id = ? AND portlet_id = ?") | ||
| .addParam(layoutID) | ||
| .addParam(USAGE.toString()) | ||
| .getInt("count"); | ||
| if (isLayoutMissingUsagePortlet) { | ||
| final int portletOrder = new DotConnect() | ||
| .setSQL("SELECT max(portlet_order) AS portlet_order FROM cms_layouts_portlets WHERE layout_id = ?") | ||
| .setMaxRows(1) | ||
| .addParam(layoutID) | ||
| .getInt("portlet_order"); | ||
| new DotConnect() | ||
| .setSQL("INSERT INTO cms_layouts_portlets(id, layout_id, portlet_id, portlet_order) VALUES (?, ?, ?, ?)") | ||
| .addParam(UUIDUtil.uuid()) | ||
| .addParam(layoutID) | ||
| .addParam(USAGE.toString()) | ||
| .addParam(portletOrder + 1) | ||
| .loadResult(); | ||
| } | ||
| CacheLocator.getLayoutCache().clearCache(); | ||
| Logger.info(this, "The 'Usage' portlet has been added to the main menu successfully!"); | ||
| } else { | ||
| Logger.error(this, "The 'Usage' portlet could not be added to any Menu Group. " + | ||
| "Please add it manually"); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns the Layout ID; i.e., menu group, for the Menu Groups that are meant to hold the | ||
| * {@code Usage} portlet, depending on whether they're present or not. The order | ||
| * of priority is the following: | ||
| * <ol> | ||
| * <li>Look for the {@code System} group.</li> | ||
| * <li>If not present, look for the {@code Marketing} group.</li> | ||
| * <li>If not present, fall back to the group containing the {@code Sites} portlet.</li> | ||
| * </ol> | ||
| * | ||
| * @return The Layout ID that the new portlet will be added to. | ||
| * | ||
| * @throws DotDataException An error occurred while querying the database. | ||
| */ | ||
| private String getMenuGroupForPortlet() throws DotDataException { | ||
| // Try System layout first | ||
| List<Map<String, Object>> results = new DotConnect() | ||
| .setSQL("SELECT id FROM cms_layout WHERE LOWER(layout_name) = 'system'") | ||
| .loadObjectResults(); | ||
|
|
||
| if (!results.isEmpty()) { | ||
| String layoutId = results.get(0).getOrDefault("id", "").toString(); | ||
| if (UtilMethods.isSet(layoutId)) { | ||
| return layoutId; | ||
| } | ||
| } | ||
|
|
||
| // Try Marketing layout second | ||
| results = new DotConnect() | ||
| .setSQL("SELECT id FROM cms_layout WHERE LOWER(layout_name) = 'marketing'") | ||
| .loadObjectResults(); | ||
|
|
||
| if (!results.isEmpty()) { | ||
| String layoutId = results.get(0).getOrDefault("id", "").toString(); | ||
| if (UtilMethods.isSet(layoutId)) { | ||
| return layoutId; | ||
| } | ||
| } | ||
|
|
||
| // Fall back to layout containing SITES portlet | ||
| results = new DotConnect() | ||
| .setSQL("SELECT layout_id FROM cms_layouts_portlets WHERE portlet_id = ?") | ||
| .addParam(SITES.toString()) | ||
| .loadObjectResults(); | ||
|
|
||
| if (!results.isEmpty()) { | ||
| return results.get(0).getOrDefault("layout_id", "").toString(); | ||
| } | ||
|
|
||
| Logger.warn(this, "No suitable layout found for Usage portlet"); | ||
| return null; | ||
| } | ||
|
|
||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
141 changes: 141 additions & 0 deletions
141
...n/src/test/java/com/dotmarketing/startup/runonce/Task260206AddUsagePortletToMenuTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| package com.dotmarketing.startup.runonce; | ||
|
|
||
| import com.dotcms.util.IntegrationTestInitService; | ||
| import com.dotmarketing.common.db.DotConnect; | ||
| import com.dotmarketing.exception.DotDataException; | ||
| import com.dotmarketing.util.Logger; | ||
| import org.junit.BeforeClass; | ||
| import org.junit.Test; | ||
|
|
||
| import static com.dotmarketing.util.PortletID.USAGE; | ||
| import static org.junit.Assert.assertFalse; | ||
| import static org.junit.Assert.assertTrue; | ||
|
|
||
| /** | ||
| * Verifies that the {@link Task260206AddUsagePortletToMenu} Upgrade Task runs as expected. | ||
| * | ||
| * @author Daniel Colina | ||
| * @since Feb 6th, 2026 | ||
| */ | ||
| public class Task260206AddUsagePortletToMenuTest { | ||
|
dsantos-dcms marked this conversation as resolved.
|
||
|
|
||
| @BeforeClass | ||
| public static void prepare() throws Exception { | ||
| // Setting web app environment | ||
| IntegrationTestInitService.getInstance().init(); | ||
| } | ||
|
|
||
| /** | ||
| * <ul> | ||
| * <li><b>Method to test: | ||
| * </b>{@link Task260206AddUsagePortletToMenu#executeUpgrade()}</li> | ||
| * <li><b>Given Scenario: </b>The Usage portlet does not exist in any layout.</li> | ||
| * <li><b>Expected Result: </b>The task runs, adds the portlet, and subsequent calls | ||
| * return false for forceRun().</li> | ||
| * </ul> | ||
| */ | ||
| @Test | ||
| public void upgradeTaskExecution() throws Exception { | ||
|
|
||
| deleteUsagePortlet(); | ||
|
|
||
| final Task260206AddUsagePortletToMenu upgradeTask = new Task260206AddUsagePortletToMenu(); | ||
|
|
||
| assertTrue("The 'Usage' portlet was explicitly deleted before, so the UT must always run", | ||
| upgradeTask.forceRun()); | ||
| upgradeTask.executeUpgrade(); | ||
| assertFalse("The 'Usage' portlet has already been added, so the UT must NOT run again", | ||
| upgradeTask.forceRun()); | ||
| } | ||
|
|
||
| /** | ||
| * <ul> | ||
| * <li><b>Method to test: | ||
| * </b>{@link Task260206AddUsagePortletToMenu#executeUpgrade()}</li> | ||
| * <li><b>Given Scenario: </b>Runs the UT twice.</li> | ||
| * <li><b>Expected Result: </b>The UT must be executable as many times as desired without | ||
| * failure. This verifies idempotency.</li> | ||
| * </ul> | ||
| */ | ||
| @Test | ||
| public void checkUpgradeTaskIdempotency() throws DotDataException { | ||
|
|
||
| deleteUsagePortlet(); | ||
| final Task260206AddUsagePortletToMenu task = new Task260206AddUsagePortletToMenu(); | ||
|
|
||
| task.executeUpgrade(); | ||
| // Run again to verify idempotency; i.e., no Java exception/error is thrown | ||
| task.executeUpgrade(); | ||
|
|
||
| assertFalse("After running twice, forceRun() should return false", task.forceRun()); | ||
| } | ||
|
|
||
| /** | ||
| * <ul> | ||
| * <li><b>Method to test: | ||
| * </b>{@link Task260206AddUsagePortletToMenu#forceRun()}</li> | ||
| * <li><b>Given Scenario: </b>Test that forceRun() gracefully handles cases where no | ||
| * suitable layout exists (System, Marketing, or Sites portlet layout).</li> | ||
| * <li><b>Expected Result: </b>The method should not throw an exception and should handle | ||
| * the missing layout scenario properly by returning false and logging a warning.</li> | ||
| * </ul> | ||
| */ | ||
| @Test | ||
| public void testForceRunHandlesMissingLayouts() throws Exception { | ||
| // Store original layouts to restore later | ||
| final String backupSystem = new DotConnect() | ||
| .setSQL("SELECT layout_name FROM cms_layout WHERE LOWER(layout_name) = 'system'") | ||
| .getString("layout_name"); | ||
| final String backupMarketing = new DotConnect() | ||
| .setSQL("SELECT layout_name FROM cms_layout WHERE LOWER(layout_name) = 'marketing'") | ||
| .getString("layout_name"); | ||
|
|
||
| try { | ||
| // Temporarily rename layouts to simulate missing layouts | ||
| if (backupSystem != null) { | ||
| new DotConnect() | ||
| .setSQL("UPDATE cms_layout SET layout_name = 'temp_system' WHERE LOWER(layout_name) = 'system'") | ||
| .loadResult(); | ||
| } | ||
| if (backupMarketing != null) { | ||
| new DotConnect() | ||
| .setSQL("UPDATE cms_layout SET layout_name = 'temp_marketing' WHERE LOWER(layout_name) = 'marketing'") | ||
| .loadResult(); | ||
| } | ||
|
|
||
| final Task260206AddUsagePortletToMenu upgradeTask = new Task260206AddUsagePortletToMenu(); | ||
|
|
||
| // This should not throw an exception and should return false or true | ||
| // depending on whether the Sites portlet layout is found | ||
| upgradeTask.forceRun(); | ||
|
|
||
| } finally { | ||
| // Restore original layout names | ||
| if (backupSystem != null) { | ||
| new DotConnect() | ||
| .setSQL("UPDATE cms_layout SET layout_name = 'System' WHERE layout_name = 'temp_system'") | ||
| .loadResult(); | ||
| } | ||
| if (backupMarketing != null) { | ||
| new DotConnect() | ||
| .setSQL("UPDATE cms_layout SET layout_name = 'Marketing' WHERE layout_name = 'temp_marketing'") | ||
| .loadResult(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Attempts to delete the Usage portlet to ensure it doesn't exist before the test begins. | ||
| */ | ||
| private void deleteUsagePortlet() { | ||
| try { | ||
| new DotConnect() | ||
| .setSQL("DELETE FROM cms_layouts_portlets WHERE portlet_id = ?") | ||
| .addParam(USAGE.toString()) | ||
| .loadResult(); | ||
| } catch (final Exception e) { | ||
| Logger.info(this, "Failed deleting the portlet_id " + USAGE.toString()); | ||
| } | ||
| } | ||
|
|
||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.