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

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>