Visualize Page History as a Timeline

Overview

A Confluence page is often edited multiple times by multiple people.  If you look at the Page History, you'll get a list of the versions created by "publish" events, but having the same information displayed on a timeline makes it much more clear. I wrote a script that would graphically show the version history of a single page.

VisualScript has a very easy-to-use Timeline template, so I used that to describe the version history. I included the name and profile picture of the user who made the change, any "what changed?" message they added when they published the change and its version number. One feature I really like in Confluence is the ability to see what changes were made from version to version, so I included a hyperlink in each "version shape" that takes you to a display of the differences between the version you clicked and the previous version (in other words, "what changed?").

Confluence page history

The user interface I created for end-users requires a space name (or key) and a page title to use as the "root" of the hierarchy. The space is needed to distinguish a page with title "Critical Tasks" in the Development space from the one with the same name in the Marketing space.

Confluence page history UI

How I Implemented It

Obtaining the Data

The first step to visualizing data is to grab the relevant data from Confluence using VSCallFunctionForConfluenceQuery to query the Confluence REST API. The parameter passed is the Confluence Query Language (CQL) query to select the page with the parameter title in the space with the key that matches the parameter. We're allowing the user to enter they space as either a key or as a name, so if this query doesn't produce any results then we'll re-run the same query interpreting the space parameter as a space name. The handleResults function is called when the results are returned from the server.

// Using the page title and space, query for the page information VSCallFunctionForConfluenceQuery(localhost, "title=\"" + paramsObj.title + "\" AND space=" + paramsObj.space, handleResults);

In handleResults, if I didn't receive any results from the query, I retry with a slightly different form of the query which interprets the user's space parameter as a space name (title); in that case, the results of that 2nd query are passed to function handlePageInformationResponse. Otherwise, if there are results, I call handlePageInformationResponse directly to begin to process the data returned from Confluence into the visual output.

function handleResults(results) { if (results) { handlePageInformationResponse(results); } else { // Retry the same query, but treat the parameter as a space title VSCallFunctionForConfluenceQuery(localhost, "title=\"" + paramsObj.title + "\" AND space.title ~ \"" + paramsObj.space + "\"", handlePageInformationResponse); } }

Unfortunately, this isn't all the information I need to build our model. Everything I've done to this point was just to get the page ID of the page the user's parameters describe. Now we use that ID to get the list of pages that have our target page as an ancestor. The Confluence API request I need to make to get the version history is experimental, so I use the VSCallFunctionForConfluenceExperimentalContentCall, passing a REST URL "suffix" of the page's ID and "/version". This call will return results that describe the page's version history to the function processVersionResults.

// Handle the response to the page information request. Need the ID of the page in order to make the version history call function handlePageInformationResponse(results) { if (!results || (results.length == 0)) { callback(null); alert("No results were found"); return; } // Get the version history of the page. Use the results to build a model VSCallFunctionForConfluenceExperimentalContentCall(localhost, results[0].id + "/version", processVersionResults); }

When the results list of pages that have our target page's ID in its ancestor list is passed to the processResults function, we have the information from Confluence required to build our model, then process that model into VisualScript and finally to display the VisualScript.

// Build the model from the results function processVersionResults(results) { var visualScript = null, model = null; model = buildModel(results); visualScript = buildVisualScript(model); callback(visualScript.toJSON()); }

At this point, I have all the information I need to build my model.

Building the Model

Building the model is very straightforward. I just loop through the version results, building a model entry with the information I want to show for each version.

// Build the model from the results function buildModel(results) { var i, len; var model = []; if (!results || (results.size === 0)) { return null; } len = results.length; for (i = 0; i < len; i++) { model.push({ author: results[i].by.displayName, authorAvatar: results[i].by.profilePicture.path, date: new Date(results[i].when), message: results[i].message, number: results[i].number }); } return model; }

Constructing the VisualScript from the Model

Now that the model is built, I use it to construct VisualScript SDK objects. After setting up my new VS.Document(), I get the "main shape", add a timeline to it, then iterate through my model adding a timeline event for each version in my model. Each event is a table with 3 rows and 2 columns. I use the JoinAcross() SDK call to join the cells together in the first and 3rd row so they can hold the longer information (the user's name in row 1, and the "what I changed" optional message in row 3. I then set up the correct URL to show the difference between the current version the previous version. Lastly, I add cells to the table to hold each piece of information I want to display from the model.

// Build the VisualScript from the model function buildVisualScript(model) { var vs = new VS.Document(); if (!model) { return null; } var mainShape = vs.GetTheShape(); var timeline = mainShape.AddTimeline() .SetEventType(VS.Timeline_EventTypes.Bubble) .SetUnits(VS.TimelineUnits.Month); timeline.AddDefaultShape() .SetShapeType(VS.ShapeTypes.RoundedRectangle) .SetFillColor("#EEEEEEEE") .SetTextSize(8); for (var i = 0; i < model.length; i++) { var event = timeline.AddEvent(model[i].date.toISOString().split("T")[0]); var eventTable = event.AddTable(3, 2) .JoinAcross(1, 1, 2) .JoinAcross(3, 1, 2); eventTable.AddColumnProperties(1) .SetWidth(24); eventTable.AddRowProperties(2) .SetHeight(24); // "Diff" hyperlink with previous version if (model[i].number > 1) { prevVersion = model[i].number - 1; event.SetHyperlink(localhost + "/pages/diffpagesbyversion.action?pageId=786434&selectedPageVersions=" + prevVersion.toString() + "&selectedPageVersions=" + model[i].number); } // Author name eventTable.AddCell(1, 1) .SetLabel(model[i].author); // Author image eventTable.AddCell(2, 1) .SetImage(model[i].authorAvatar); //Version eventTable.AddCell(2, 2) .SetLabel("Version: " + model[i].number); // Publish message eventTable.AddCell(3, 1) .SetLabel(model[i].message); } return vs; }

Rendering the VisualScript

The last step is to generate the visual using the callback function.

// Build the model from the results
 function processVersionResults(results) {
     var visualScript = null, model = null;
 
     model = buildModel(results);
 
     visualScript = buildVisualScript(model);
 
     callback(visualScript.toJSON());
 }

Creating the UI

After the script is complete, I set up the UI, so users can easily provide the page title and the space information to the script. In the top section of the VisualScript Editor page, you'll see a field labeled "Parameter Template". The specification string for the UI Builder’s “Parameter Template” is:

How to parameter UI

As soon as I enter the 2nd "}" character at the end of each "parm", a form appears that allows me to customize the way the user sees the UI for these parameters:

Confluence history form builder

I override the displayed name to a more user-friendly string, and I add some description text.

Confluence history form new name

More Information

This is just one example of the useful visuals that can be produced using VisualScript and VisualScript Studio with a minimum of development effort.

The full source for this script can be seen here. To make the code easier to understand, I highlighted the part used for querying the data in green, the part for building the model in orange, and the part that creates VisualScript from the data in blue.

function VisualScript(localhost, params, callback) { var paramsObj = JSON.parse(params); // Using the page title and space, query for the page information VSCallFunctionForConfluenceQuery(localhost, "title=\"" + paramsObj.title + "\" AND space=" + paramsObj.space, handleResults); function handleResults(results) { if (results) { handlePageInformationResponse(results); } else { // Retry the same query, but treat the parameter as a space title VSCallFunctionForConfluenceQuery(localhost, "title=\"" + paramsObj.title + "\" AND space.title ~ \"" + paramsObj.space + "\"", handlePageInformationResponse); } } // Handle the response to the page information request. Need the ID of the page in order to make the version history call function handlePageInformationResponse(results) { if (!results || (results.length == 0)) { callback(null); alert("No results were found"); return; } // Get the version history of the page. Use the results to build a model VSCallFunctionForConfluenceExperimentalContentCall(localhost, results[0].id + "/version", processVersionResults); } // Build the model from the results function processVersionResults(results) { var visualScript = null, model = null; model = buildModel(results); visualScript = buildVisualScript(model); callback(visualScript.toJSON()); }
// Build the model from the results function buildModel(results) { var i, len; var model = []; if (!results || (results.size === 0)) { return null; } len = results.length; for (i = 0; i < len; i++) { model.push({ author: results[i].by.displayName, authorAvatar: results[i].by.profilePicture.path, date: new Date(results[i].when), message: results[i].message, number: results[i].number }); } return model; }
// Build the VisualScript from the model function buildVisualScript(model) { var vs = new VS.Document(); if (!model) { return null; } var mainShape = vs.GetTheShape(); var timeline = mainShape.AddTimeline() .SetEventType(VS.Timeline_EventTypes.Bubble) .SetUnits(VS.TimelineUnits.Month); timeline.AddDefaultShape() .SetShapeType(VS.ShapeTypes.RoundedRectangle) .SetFillColor("#EEEEEEEE") .SetTextSize(8); for (var i = 0; i < model.length; i++) { var event = timeline.AddEvent(model[i].date.toISOString().split("T")[0]); var eventTable = event.AddTable(3, 2) .JoinAcross(1, 1, 2) .JoinAcross(3, 1, 2); eventTable.AddColumnProperties(1) .SetWidth(24); eventTable.AddRowProperties(2) .SetHeight(24); // "Diff" hyperlink with previous version if (model[i].number > 1) { prevVersion = model[i].number - 1; event.SetHyperlink(localhost + "/pages/diffpagesbyversion.action?pageId=786434&selectedPageVersions=" + prevVersion.toString() + "&selectedPageVersions=" + model[i].number); } // Author name eventTable.AddCell(1, 1) .SetLabel(model[i].author); // Author image eventTable.AddCell(2, 1) .SetImage(model[i].authorAvatar); //Version eventTable.AddCell(2, 2) .SetLabel("Version: " + model[i].number); // Publish message eventTable.AddCell(3, 1) .SetLabel(model[i].message); } return vs; } }