Visualize All the How-To Articles in a Space

Overview

We wanted to get a sense of the structure of our knowledge base by creating a graphical display of all "how to" articles in a space. To address this need, I wrote a quick script to display all the how-to articles in a space as a mind map. I decided to also add the name of the page that the "how to" article is a child of, and a hyperlink to the how-to article and its parent.

How to examples

The user interface I created for end-users requires only a space name (or key).

How to 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 pages with the key that matches the parameter and a label of "kb-how-to-article" (which is a label assigned by Confluence when a page is created as a "how to" article). 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. I added the "expand=ancestors" option to request that ancestor information is returned as well so that I can find the parent of each how-to article.

The handleResults function is called when the results are returned from the server.

// Using the space, query for the the how to articles VSCallFunctionForConfluenceQuery(localhost, "space.key = \"" + params + "\" AND label = \"kb-how-to-article\"", handleResults, "expand=ancestors");

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, "space.title ~ \"" + params + "\" AND label = \"kb-how-to-article\"", handlePageInformationResponse, "expand=ancestors"); } }

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, process that model into VisualScript, and finally to display it as a visual.

// Handle the response to the page information request. Build the model from the results, then build the VisualScript from the model function handlePageInformationResponse(results) { var model = buildModel(results); var visualScript = buildVisualScript(model); if (visualScript) { // Render the VisualScript callback(visualScript.toJSON()) ; } else { callback(null); alert("No information available"); } }

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

Building the Model

Building the model is pretty straightforward. I just loop through the results, building a model entry with the information I want to show for each how-to article. If I received ancestor information, I also add information about the last entry in the ancestor list (which is the immediate parent).

// Build a model from the query results function buildModel(results) { var model = []; if (!results || (results.length == 0)) { return; } for (var i = 0; i < results.length; i++) { var modelEntry = {id: results[i].id, title: results[i].title, webui: results[i]._links.webui}; if (results[i].ancestors && (results[i].ancestors.length > 0)) { var ancestorIndex = results[i].ancestors.length - 1; modelEntry.parentId = results[i].ancestors[ancestorIndex].id; modelEntry.parentTitle = results[i].ancestors[ancestorIndex].title; modelEntry.parentWebui = results[i].ancestors[ancestorIndex]._links.webui; } model.push(modelEntry); } 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() and specifying a "mind map" template, I get the "main shape", then add a shape connector to hold all the how-to article shapes. I then iterate through my model adding a shape for each entry in my model. Each shape contains a table with 2 rows (1 column).The top row contains the title of the how-to article and a hyperlink to that article; the bottom row contains the parent title and its hyperlink.

// Build VisualScript from model function buildVisualScript(model) { var vs = {}; if (!model) { return null; } vs = new VS.Document() .SetTemplate(VS.Templates.Mindmap); var mainShape = vs.GetTheShape() .SetLabel("How-to Articles") .SetFillColor("#FBD585"); var connector = mainShape.AddShapeConnector(); for (var i = 0; i < model.length; i++) { var shapeTable = connector.AddShape() .SetFillColor("#EEEEEE") .AddTable(2,1); shapeTable.AddCell(1,1) .SetLabel(model[i].title) .SetTextTruncate(50) .SetTextSize(10) .SetHyperlink(localhost + model[i].webui); shapeTable.AddCell(2,1) .SetLabel(model[i].parentTitle) .SetTextTruncate(50) .SetTextSize(8) .SetHyperlink(localhost + model[i].parentWebui); } return vs; }

Rendering the VisualScript

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

if (visualScript) {
    // Render the VisualScript
    callback(visualScript.toJSON()) ;
}
else {
     callback(null);
    alert("No information available");          
}

Creating the UI

After the script is complete, I set up the UI so users can easily provide 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:

How to form builder

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

How to 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 targetInfo;
      
    //debugger;
     
     
// Using the space, query for the the how to articles VSCallFunctionForConfluenceQuery(localhost, "space.key = \"" + params + "\" AND label = \"kb-how-to-article\"", handleResults, "expand=ancestors"); function handleResults(results) { if (results) { handlePageInformationResponse(results); } else { // Retry the same query, but treat the parameter as a space title VSCallFunctionForConfluenceQuery(localhost, "space.title ~ \"" + params + "\" AND label = \"kb-how-to-article\"", handlePageInformationResponse, "expand=ancestors"); } } // Handle the response to the page information request. Build the model from the results, then build the VisualScript from the model function handlePageInformationResponse(results) { var model = buildModel(results); var visualScript = buildVisualScript(model); if (visualScript) { // Render the VisualScript callback(visualScript.toJSON()) ; } else { callback(null); alert("No information available"); } }
// Build a model from the query results function buildModel(results) { var model = []; if (!results || (results.length == 0)) { return; } for (var i = 0; i < results.length; i++) { var modelEntry = {id: results[i].id, title: results[i].title, webui: results[i]._links.webui}; if (results[i].ancestors && (results[i].ancestors.length > 0)) { var ancestorIndex = results[i].ancestors.length - 1; modelEntry.parentId = results[i].ancestors[ancestorIndex].id; modelEntry.parentTitle = results[i].ancestors[ancestorIndex].title; modelEntry.parentWebui = results[i].ancestors[ancestorIndex]._links.webui; } model.push(modelEntry); } return model; }
// Build VisualScript from model function buildVisualScript(model) { var vs = {}; if (!model) { return null; } vs = new VS.Document() .SetTemplate(VS.Templates.Mindmap); var mainShape = vs.GetTheShape() .SetLabel("How-to Articles") .SetFillColor("#FBD585"); var connector = mainShape.AddShapeConnector(); for (var i = 0; i < model.length; i++) { var shapeTable = connector.AddShape() .SetFillColor("#EEEEEE") .AddTable(2,1); shapeTable.AddCell(1,1) .SetLabel(model[i].title) .SetTextTruncate(50) .SetTextSize(10) .SetHyperlink(localhost + model[i].webui); shapeTable.AddCell(2,1) .SetLabel(model[i].parentTitle) .SetTextTruncate(50) .SetTextSize(8) .SetHyperlink(localhost + model[i].parentWebui); } return vs; } }