Create an admin console page in tools
Goal
Create an admin console page in tools section in AEM 6.5. In this case, we will create a section for centralizing data, but there are different ways to use an admin console. For example, in a past project, I used this type of page also to show a report of our data import. It highly depends on your needs.
Procedure
First of all, you should check the video to see a demo of what we are going to develop:
If you are still interested, I’m happy to explain how to get this.
You should download the project because not all code will be presented in the article. The entire source code of the project is available here:
In order to extend the tools section, we need to overlay the existing libs/cq/core/content/nav/tools/operations
nodes. Let me also say that once you’ve read the article, If you are interested to understand how other AEM console pages are created, you should have a look at these nodes in the CRX/DE. The procedure is really really similar, and maybe you can take a cue for other custom pages 😉
Anyway, let’s begin! To overlay any kind of AEM content under “libs”, you have to create the same path under the path “apps” in your project. This is our example:

Under the “tools” folder, create your section (“lab“) and then the page (“promo-codes“). Of course you could have many of them if you want to. This is the result:

Before we forget, update the filter.xml in order to add this path to the package. It’s very important to use the specific path of your folder, because you could overwrite other configurations with a generic path, e.g. “/apps/cq”.
<?xml version="1.0" encoding="UTF-8"?>
<workspaceFilter version="1.0">
<filter root="/apps/Lab2020"/>
<filter root="/apps/sling" />
<filter root="/apps/cq/core/content/nav/tools/lab"/>
</workspaceFilter>
Regarding the file “.content.xml” under the folder “promo-codes”, you can decide your icon here: https://helpx.adobe.com/experience-manager/6-3/sites/developing/using/reference-materials/coral-ui/coralui3/Coral.Icon.html
The href property points to the location of the main page, defined later. You’ll see that a lot of components are defined by a .content.xml file.
Here the entire file:
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:rep="internal"
jcr:primaryType="nt:unstructured"
jcr:title="Promo Codes"
jcr:description="Manage Promo Codes entries"
id="cq-tools-lab2020-promo-codes"
href="/apps/Lab2020/admin/configurations/promo-codes/content.html"
icon="project"/>
Our main page, defined under “/apps/Lab2020/admin/configurations/promo-codes/content”, contains a table, that relies on a datasource to provide data to the list.
You can find more info about the granite table here: https://helpx.adobe.com/experience-manager/6-3/sites/developing/using/reference-materials/granite-ui/api/jcr_root/libs/granite/ui/components/coral/foundation/table/index.html
A few notes:
- A custom clientlib is included. It’s used to activate, deactivate, delete and update the single entry.
- itemResourceType is used to display each entry of the list (datasource) in each row.
- Within actions, the selection nodes (delete, edit, activate, deactivate) are used when selecting an item in the list. Each node has a granite id that is used in the Javascript.
- Within actions, the secondary action always shows and is used for create. It’s uses a plain href property.
- Data are stored under the path “/conf/global/settings/lab2020/configurations/promo-codes”. There is the property “path” in the datasource tag and in the Java code as well. You can choose for sure another path. In this case, the path needs to be created via CRX.
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:mixinTypes="[sling:VanityPath]"
jcr:primaryType="nt:unstructured"
jcr:title="Promo Codes"
sling:resourceType="granite/ui/components/shell/collectionpage"
console-id="cq-tools-lab2020-promo-codes"
contentPath="/content"
currentView="${state["shell.collectionpage.layoutId"].string}"
modeGroup="granite-uptime-collection"
pageURITemplate="/apps/Lab2020/admin/configurations/promo-codes/content.html"
targetCollection=".granite-uptime-collection">
<head jcr:primaryType="nt:unstructured">
<clientlibs
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/includeclientlibs"
categories="[coralui3,granite.ui.coral.foundation,Lab2020.clientlibs-edit-entry]"/>
<viewport
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/admin/page/viewport"/>
<meta
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/meta"
content="chrome=1"
name="X-UA-Compatible"/>
</head>
<views jcr:primaryType="nt:unstructured">
<list
granite:rel="granite-uptime-collection"
jcr:primaryType="nt:unstructured"
jcr:title="List View"
sling:resourceType="granite/ui/components/coral/foundation/table"
draggable="{Boolean}false"
icon="viewList"
layoutId="list"
limit="{Long}40"
modeGroup="granite-uptime-collection"
path="/content"
selectionCount="single"
selectionMode="row"
size="40"
sortMode="local"
src="/libs/granite/backup/content/admin/views/list.html"
stateId="shell.collectionpage">
<datasource
jcr:primaryType="nt:unstructured"
sling:resourceType="/apps/Lab2020/admin/configurations/promo-codes/components/datasource"
itemResourceType="/apps/Lab2020/admin/configurations/promo-codes/components/promo-code-entry"
value="value"
text="text"
enabled="enabled"
promoCode="promoCode"
promoName="promoName"
promoDescription="promoDescription"
validDateFrom="validDateFrom"
validDateTo="validDateTo"
action="action"
path="/conf/global/settings/lab2020/configurations/promo-codes"/>
<columns jcr:primaryType="nt:unstructured">
<select
jcr:primaryType="nt:unstructured"
select="{Boolean}true"/>
<enabled
jcr:primaryType="nt:unstructured"
jcr:title="Enabled"/>
<promoCode
jcr:primaryType="nt:unstructured"
jcr:title="Promo Code"/>
<promoName
jcr:primaryType="nt:unstructured"
jcr:title="Promo Name"/>
<promoDescription
jcr:primaryType="nt:unstructured"
jcr:title="Promo Description"/>
<validDateFrom
jcr:primaryType="nt:unstructured"
jcr:title="Valid Date (From)"/>
<validDateTo
jcr:primaryType="nt:unstructured"
jcr:title="Valid Date (To)"/>
<action
jcr:primaryType="nt:unstructured"
jcr:title="Action"/>
</columns>
</list>
</views>
<actions jcr:primaryType="nt:unstructured">
<selection jcr:primaryType="nt:unstructured">
<delete
granite:id="toggleDeleteEntryDialog"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
icon="delete"
text="Delete"
variant="actionBar"
x-cq-linkchecker="skip"/>
<edit
granite:id="editEntryDialog"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
icon="edit"
text="Edit"
variant="actionBar"
x-cq-linkchecker="skip"/>
<activate
granite:id="activateEntryDialog"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
icon="globe"
text="Publish"
variant="actionBar"
x-cq-linkchecker="skip"/>
<deactivate
granite:id="deactivateEntryDialog"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
icon="globe"
text="Unpublish"
variant="actionBar"
x-cq-linkchecker="skip"/>
</selection>
<secondary jcr:primaryType="nt:unstructured">
<create
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/anchorbutton"
href="/apps/Lab2020/admin/configurations/promo-codes/content/createEntry.html"
text="Create"
variant="primary"
x-cq-linkchecker="skip"/>
</secondary>
</actions>
<title
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/text"
text="Promo Codes"/>
</jcr:root>
The datasource (/apps/Lab2020/admin/configurations/promo-codes/components/datasource) retrieves the list of resources that have been created and are displayed in the table. Each row is rendered with the itemResourceType (/apps/Lab2020/admin/configurations/promo-codes/components/promo-code-entry).
To create an entry, the following .content.xml (apps/Lab2020/admin/configurations/promo-codes/content/createEntry) is used:
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:mixinTypes="[sling:VanityPath]"
jcr:primaryType="nt:unstructured"
jcr:title="Edit Promo Code"
sling:resourceType="granite/ui/components/shell/propertiespage"
backHref="/apps/Lab2020/admin/configurations/promo-codes/content.html"
consoleId="cq-tools-promo-codes-configurations-update-entry"
formId="createPromoCodeEntryForm">
<head jcr:primaryType="nt:unstructured">
<clientlibs
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/includeclientlibs"
categories="[coralui3,granite.ui.coral.foundation]"/>
<viewport
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/admin/page/viewport"/>
<meta
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/meta"
content="chrome=1"
name="X-UA-Compatible"/>
<favicon
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/page/favicon"/>
</head>
<content
granite:class="centered"
granite:id="createPromoCodeEntryForm"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form"
action="/bin/configurations/promo-codes"
method="post"
style="vertical">
<items jcr:primaryType="nt:unstructured">
<general
jcr:primaryType="nt:unstructured"
jcr:title="General"
sling:resourceType="granite/ui/components/foundation/section">
<layout
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/container">
<items jcr:primaryType="nt:unstructured">
<promoCodeForm
jcr:primaryType="nt:unstructured"
sling:resourceType="Lab2020/admin/configurations/promo-codes/components/promo-code-form"/>
</items>
</column>
</items>
</general>
</items>
</content>
<title
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/text"
text="Edit Promo Code Entry"/>
</jcr:root>
It is completely equals to the editEntry file (apps/Lab2020/admin/configurations/promo-codes/content/createEntry) except for some text.
These two files use the HTML file “/apps/Lab2020/admin/configurations/promo-codes/components/promo-code-form” which allows to create/modify our data and its layout is the same of a component dialog. The model you’ll find in the HTML is used to retrieve data in case of an update.
Data are stored/updated with a servlet. You can notice in the “content” tag these two properties:
- action=”/bin/configurations/promo-codes”
- method=”post”
You can find all the java code in project. You can download it at the top of the article.
The servlet is split in more classes because they contain some common methods that can be reused inside your project, but it’s not mandatory of course. Organized them as you prefer
To modify/activate/deactivate/delete an entry, click on the related checkbox to open the menu. The clientlib will do the rest.
Here the clientlib:
(function(document, $) {
"use strict";
//var URL_BASE = "/apps/Lab2020/admin/configurations/promo-codes/content";
var URL_BASE = Granite.HTTP.getPath();
var MAIN_PAGE_PATH = URL_BASE + ".html";
var EDIT_PAGE_PATH = URL_BASE + "/editEntry.html"
var ui = $(window).adaptTo("foundation-ui");
$(document).on("foundation-selections-change", ".granite-uptime-collection", function() {
var deleteButton = $("#toggleDeleteEntryDialog");
var editButton = $("#editEntryDialog");
});
$(document).on("click", "#toggleDeleteEntryDialog", function(e) {
var entryName = $(".foundation-selections-item").data("id");
var dialog = document.querySelector("#deleteEntryDialog");
if(!dialog) {
dialog = new Coral.Dialog().set({
variant: "warning",
id: "deleteEntryDialog",
header: {
innerHTML: Granite.I18n.get("Delete Selected Entry")
},
content: {
innerHTML: Granite.I18n.get("Are you sure you want to entry {0}?", entryName, "0 is the path of the entry to delete")
},
footer: {
innerHTML: "<button is='coral-button' variant='default' coral-close>" + Granite.I18n.get("No") +
"</button><button id='deleteEntryButton' is='coral-button' variant='primary'>" + Granite.I18n.get("Yes") + "</button>"
}
});
document.body.appendChild(dialog);
} else {
dialog.content.innerHTML =
Granite.I18n.get("Are you sure you want to delete entry {0}?", entryName, "0 is the path of the entry to delete");
}
dialog.show();
});
$(document).on("click", "#activateEntryDialog", function(e) {
var selectedItem = $(".foundation-selections-item");
var selectedItemPath = $(".foundation-selections-item").data("path");
var dialog = document.querySelector("#deleteEntryDialog");
var switcherItem = $(".foundation-mode-switcher-item");
$.ajax({
url: Granite.HTTP.externalize("/bin/replicate"),
type: "POST",
data: {
"path":selectedItemPath,
"cmd": "Activate"
},
success: function() {
window.location.href = Granite.HTTP.externalize(MAIN_PAGE_PATH) ;
//window.location.refresh??
},
error: function(xmlhttprequest, textStatus, message) {
ui.notify(Granite.I18n.get("Error"), Granite.I18n.get("An error occurred while publish the entry: {0}.", message, "0 is the error message"), "error")
}
});
});
$(document).on("click", "#deactivateEntryDialog", function(e) {
var selectedItem = $(".foundation-selections-item");
var selectedItemPath = $(".foundation-selections-item").data("path");
var dialog = document.querySelector("#deleteEntryDialog");
var switcherItem = $(".foundation-mode-switcher-item");
$.ajax({
url: Granite.HTTP.externalize("/bin/replicate"),
type: "POST",
data: {
"path":selectedItemPath,
"cmd": "Deactivate"
},
success: function() {
window.location.href = Granite.HTTP.externalize(MAIN_PAGE_PATH) ;
//window.location.refresh??
},
error: function(xmlhttprequest, textStatus, message) {
ui.notify(Granite.I18n.get("Error"), Granite.I18n.get("An error occurred while Unpublish the entry: {0}.", message, "0 is the error message"), "error")
}
});
});
$(document).on("click", "#deleteEntryButton", function(e) {
var selectedItem = $(".foundation-selections-item");
var selectedItemPath = $(".foundation-selections-item").data("path");
var dialog = document.querySelector("#deleteEntryDialog");
var switcherItem = $(".foundation-mode-switcher-item");
$.ajax({
url: Granite.HTTP.externalize(selectedItemPath),
type: "POST",
data: {
":operation": "delete"
},
success: function() {
selectedItem.remove();
if(dialog) {
dialog.hide();
}
if(switcherItem) {
switcherItem.removeClass('foundation-mode-switcher-item-active');
}
window.location.refresh
},
error: function(xmlhttprequest, textStatus, message) {
if(dialog) {
dialog.hide();
}
ui.notify(Granite.I18n.get("Error"), Granite.I18n.get("An error occurred while deleting the entry: {0}.", message, "0 is the error message"), "error")
}
});
});
$(document).on("click", "#editEntryDialog", function(e) {
var selectedEntry = $(".foundation-selections-item").data("path");
window.location.href = Granite.HTTP.externalize(EDIT_PAGE_PATH) + "?path=" + selectedEntry;
});
})(document, Granite.$);
That’s all. Please download the project to have the missing code I haven’t posted here.
A huge thanks and mention to this article (https://experiencemanaged.com/posts/how-to-create-an-aem-62-admin-console-for-touch-ui.html) which I followed a few years ago to develop some admin console pages.
This should work in AEM 6.3+, but can be easily adapted for other AEM versions.
Cheers mates! 🍻🍺
unable to download the package. Can you please provide correct url for downloading the ZIP file.
LikeLike
Hi Mahesh, yep sorry. Try again, please 🙂
LikeLike