Confluence Meta-Macros and the joy of being organized
Let’s talk about meta-macros. That is, macros that examine other macros. I just made up the term, so don’t be concerned if you can’t find other examples on the internet.
If you wanted some insight into which pages in your Confluence Instance were using a specific macro, how would you find that information?
You could certainly check each page manually, but that sounds dreadful.
One option to get Macro information is this ScriptRunner script that I wrote, which examines the latest version of each page in each Space for references to the specified macro:
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.confluence.spaces.SpaceManager
import com.atlassian.confluence.pages.PageManager
import com.atlassian.confluence.pages.Page
def pageManager = ComponentLocator.getComponent(PageManager)
def spaceManager = ComponentLocator.getComponent(SpaceManager)
def spaces = spaceManager.getAllSpaces()
def macroRef = 'ac:name="info"'
spaces.each {
spaceObj ->
def pages = pageManager.getPages(spaceObj, false)
pages.each {
page ->
if (page.getBodyContent().properties.toString().contains(macroRef) && page.version == page.latestVersion.version) {
log.warn("'${page.title}' (version ${page.version}) is the latest version of the page, and contains the target macro")
}
}
}
But what if you wanted MORE information? What if you wanted to know every macro running on every page in the system, and you didn’t have ScriptRunner to do it for you? In that case, we need a macro that catalogues other macros.
Macros are useful because they can be purely JavaScript, and can access the Confluence REST API. Therefor, they can do anything that ScriptRunner could have done with the REST API. It’s just a lot less convenient.
Even in a Confluence instance that doesn’t have ScriptRunner, there is generally the opportunity to add a user macro to the system. You’ll need to be a Confluence Admin to do so, but I assume most people reading my blog will already have that access.
I set out to write a Macro that would provide me with information about every macro running on every page in Confluence, and present it in an easily-readable format. Behold, the results:
Each page name is a link to the page, each Space name is a link to the space, and if you click the name of the macro you get a little info box:
Neat!
Here’s the full macro code. I am new to JavaScript, and I tried my best, therefor nobody can criticize me:
## @noparams
<script type = "text/javascript"> //Declare global variables //Declare global variables
let macroDetails = [];
var pageInfoArr = [];
var tableGenerated = false;
//Start manin function loop
AJS.toInit(function() {
//Declare CSS styles to be applied
$('<style>').text(".modal-body { padding: 10px;}"+
"table {border-collapse: collapse; width: 60%; color: #333; font-family: Arial, sans-serif; font-size: 14px; text-align: left; border-radius: 10px; overflow: hidden; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); margin: auto; margin-top: 50px; margin-bottom: 50px;} " +
"th {background-color: #1E90FF; color: #fff; font-weight: bold; padding: 10px; text-transform: uppercase; letter-spacing: 1px; border-top: 1px solid #fff; border-bottom: 1px solid #ccc;} " +
"td {background-color: #fff; padding: 20px; border-bottom: 1px solid #ccc; font-weight: bold;} " +
"table tr:nth-child(even) td {background-color: #f2f2f2;}" + ".modal {display: none;position: fixed;top: 0;left: 0;right: 0;bottom: 0;background-color: rgba(0, 0, 0, 0.5);z-index: 9999; /* Ensure the dialog is on top of other elements */}"+
".modal-message {text-align: left;font-size: 16px;line-height: 1.6;}"+
".modal-header {padding: 10px;background-color: #1E90FF;color: #fff;font-weight: bold;text-transform: uppercase;letter-spacing: 1px;border-top-left-radius: 5px;border-top-right-radius: 5px;}"+
".modal-content {display: flex;flex-direction: column;justify-content: center;width: 30%;background-color: #fff;border-radius: 5px;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);margin: auto;}"
).appendTo(document.head);
//Query Confluence for all content objects that are a page
jQuery.ajax({
url: "/rest/api/content?type=page&start=0&limit=99999",
type: "GET",
contentType: "application/json",
//Upon successfully querying for the pages in Confluence
success: function(data) {
//Iterate through all of the pages
data.results.forEach(function(page) {
//For each page, query the REST API for more details about it
jQuery.ajax({
url: "/rest/api/content/" + page.id + "?expand=body.storage.value,version,space",
type: "GET",
contentType: "application/json",
//If the page details query was successful, parse the page for references to macros
success: function(data) {
var html = data.body.storage.value;
var re = /<ac:structured-macro\s+ac:name="([^"]+)"/g;
//When a match is found, add the macro to the array
var match;
while ((match = re.exec(html)) !== null) {
var name = match[1];
console.log("Found structured-macro with name: " + name);
pageInfoArr.push({
name: data.title,
space: data.space.name,
macro: name
});
}
//Check to ensure that the array has at least one value in it
if (pageInfoArr.length > 0) {
//If the array has a value, check to see if the table has been generated
//If it hasn't, create the table and add the headers
if (!tableGenerated) {
let table = document.createElement("table");
let row = table.insertRow();
let headers = ["Page Name", "Page Space", "Macro Name (click for info)"];
for (let i = 0; i < headers.length; i++) {
let header = document.createElement("th");
let text = document.createTextNode(headers[i]);
header.appendChild(text);
row.appendChild(header);
}
document.getElementById("main-content").appendChild(table);
tableGenerated = true;
}
//If the table HAS been generated, append the information to it
if (tableGenerated) {
let table = document.getElementsByTagName("table")[0];
let row = table.insertRow();
let nameCell = row.insertCell();
let spaceCell = row.insertCell();
let macroCell = row.insertCell();
//Get the name of the macro
var macroName = pageInfoArr[pageInfoArr.length - 1].macro
//Set the ID of the modal info box
let modalID = 'myModal' + pageInfoArr.length; // unique ID for each modal
//Log some information about what's happening
console.log(`modal #: ${modalID} macroname: ${macroName}`);
//Add the page name and space name to the table
nameCell.innerHTML = `<a href="/pages/viewpage.action?pageId=${data.id}" target="_blank">${data.title}</a>`;
spaceCell.innerHTML = `<a href="/display/${data.space.key}" target="_blank">${data.space.name}</a>`;
//Now we're querying the REST API for more information about the macro in question
jQuery.ajax({
url: `/plugins/macrobrowser/browse-macros-details.action?id=${macroName.toString()}`,
type: "GET",
contentType: "application/json",
//If we successfully returned information about the macro, confirm that the information we need is available
success: function(macroData) {
let description = macroData.details ? macroData.details.description : 'No description available';
macroDetails.push({details: description});
//Populate the macro info cell with information
//This includes the information that will be displayed in the modal info box
macroCell.innerHTML = '<span class="word" onclick="showDialog(\'' + modalID + '\')">' + macroName + '</span> <div id="'+modalID+'" class="modal"> <div class="modal-content"><div class="modal-header">Macro Details</div><div class="modal-body"><p class="modal-message">'+
`Macro Name: <i>${macroData.details.macroName}</i><br>
Plugin Key: <i>${macroData.details.pluginKey}</i><br>
Description: <i>${macroData.details.description}</i><br>`
+'</p></div></div></div>';
},//Handle errors for GET macro details request
error: function(xhr, status, error) {
console.log("Macro detail request failed. Error message: " + error);
}
});//End GET macro details request
}//End pageArr.length check loo;
}//End success loop for GET page details request
},//Handle errors for GET page details request
error: function(xhr, status, error) {
console.log("GET request for page object failed. Error message: " + error);
}
});//End for-each page loop
});
},//End success loop of first ajax query, the GET content request
//Handle errors in the first ajax query, the GET content request
error: function(xhr, status, error) {
console.log("Content GET request failed. Error message: " + error);
}
});//End first ajax query, the GET content request
});//End main toInit function loop
//Declare the functions we use to view, hide, and access the modal info boxes
function showDialog(modalID) {
var modal = document.getElementById(modalID);
modal.style.display = 'flex';
}
function hideDialog() {
var modal = document.getElementById('myModal');
modal.style.display = 'none';
}
window.onclick = function(event) {
var modals = document.getElementsByClassName('modal');
Array.from(modals).forEach(function(modal) {
if (event.target === modal) {
modal.style.display = 'none';
}
});
};
</script>
Leave a Reply