Tableau Extensions Addons Introduction: Synchronized Scrollbars
By: Tamas Foldi
Tags: Dataviz, Tableau Server, Extensions Api, Tableau

At this year’s Tableau Conference, I tried to stay on the safe side and show mostly Tableau supported solutions: visualizations on real-time data, Jupyer Notebook like experiences and so on. I had only one exception, my “Tableau Extensions Add-ons” microframework that allowed me to show use cases like synchronized scrolling of dashboard elements, getting and validating user names and sessions and building user interaction heatmaps. People loved all of these, so let me show you the synchronized scrolling magic.

Basic Concepts

Tableau Extension API is basically a simple HTML object in the dashboard — basically an iframe - that can access some of the vizql functionality by window messages. When your extension asks for summary data or sets parameter values, it simply notifies its parent frame - the Viz frame - to return with the data or execute the tabdoc:set-parameter-value vizql command. Since the extension is hosted on a different host - you cannot deploy extensions theoretically on Server - there is no other way to interact with the Viz parent frame. If you try to access the Viz parent frame via window.parent , you will get DOMException: Blocked a frame with origin for good reasons. This is great and secure - the only commands an extension can execute are the ones that are handled in the Viz frame by the Extension API server-side code.

But what if we need more functionality that the Viz frame exports?

Adding our own event listener to the Viz frame

The solution is as easy as it sounds, we need to add our own window message handlers to the parent frame to support additional functionality while staying as secure as possible. The quickest way to add our event listener that receives the command from our Extension and sets up everything inside the Viz frame is to modify one of the existing javascript files in Tableau Server and append our code to it. Sounds pretty hacky? Well, until Tableau Software provides a better way to add site-specific javascript and CSS files, I don’t know a better way and besides, it’s fun to hack Tableau. My usual place to store these additions is vqlweb.js , located in C:\Program Files\Tableau\Tableau Server\packages\vizqlserver.20194.19.0923.1135\public\v_201941909231135\javascripts directory on Windows and the matching directory in Linux. You need to add the following code (details later) to the top of this file:

This is how the “patched”, addons ready vqlweb.js looks like

Don’t forget to repack or rename the .br and .gz files (compressed version of this javascript)

And now let’s what we added exactly. As I mentioned, this is really a microframework, this is everything practically:



const EXTENSION_ADDON_URL_PREFIX = '

const LOAD_MODULE_REQUEST_MESSAGE = 'LoadModule-Request';

const LOAD_MODULE_RESPONSE_MESSAGE = 'LoadModule-Response';



window.addEventListener('message', function (event) {

if (event.data && event.data.event_id === LOAD_MODULE_REQUEST_MESSAGE) {

// load our addons external js file

var addon = event.data.data.replace(/[^a-z0-9\-]/gi, '_').toLowerCase();

var script = document.createElement('script');

script.src = EXTENSION_ADDON_URL_PREFIX + "/tableau-extensions-" + addon + "-server.js";

script.onload = function () {

event.source.postMessage({ event_id: LOAD_MODULE_RESPONSE_MESSAGE, data: event.data.data }, "*");

}

document.head.appendChild(script);

}

});

}

const EXTENSION_ADDON_URL_PREFIX = ' https://your-addons-server/extension-lib' const LOAD_MODULE_REQUEST_MESSAGE = 'LoadModule-Request';const LOAD_MODULE_RESPONSE_MESSAGE = 'LoadModule-Response';window.addEventListener('message', function (event) {if (event.data && event.data.event_id === LOAD_MODULE_REQUEST_MESSAGE) {// load our addons external js filevar addon = event.data.data.replace(/[^a-z0-9\-]/gi, '_').toLowerCase();var script = document.createElement('script');script.src = EXTENSION_ADDON_URL_PREFIX + "/tableau-extensions-" + addon + "-server.js";script.onload = function () {event.source.postMessage({ event_id: LOAD_MODULE_RESPONSE_MESSAGE, data: event.data.data }, "*");document.head.appendChild(script);});

What is happening here? If we receive a LoadModule-Request message from our extension, we will load the js file https://your-addons-server/extension-lib/tableau-extensions-<modulename>- server.js to the Viz frame. We don’t have to deal with the same domain policy, window messages working well between domains.

Is this secure?

As secure as the files you are loading are secure. The code only loads javascript files from a predefined folder in a single server. This is basically a whitelist method, administrators control what additional JS files can be loaded as extension add-ons. Also, the code runs with the logged users’ authorization — no one without appropriate user permissions can access or perform actions.

TL;DR — it’s secure.

Now let’s see a real use case.

Synchronized Scrolling

Let’s consider the example of synchronized scrolling. It’s a quite popular request, you can vote for this feature to be supported out of the box here. Basically what we want is:

Add code to the Viz frame that gets two or more zone ids or div selectors and establish synchronized scrolling Add code to the extension API that exposes the synchronize scroll functions We want to make this a repeatable process, to support other use cases in the future

From a visual perspective, we should achieve something like this:

Synchronized Scrolling on Superstore, based on Klaus Schulte blog post

Implementation

I wanted to create a really clean API to load Extension add-ons, trying to be as native as I could. I ended up with the following code for this Sync Scroll use case:



<html lang="en">



<head>

<script text="text/javascript"

src="

<script text="text/javascript" src="../extension-lib/tableau-extensions-addons.js"></script>

<script type="text/javascript">

document.addEventListener("DOMContentLoaded", function (event) {



tableau.extensions.initializeAsync().then(function () {



tableau.extensions.initalizeAddonsAsync("sync-scrollbar").then(function () {



tableau.extensions.dashboardContent.dashboard.syncScrollbars(0, 1);

});



});

});

</script>

</head>



<body></body>



</html> https://cdn.jsdelivr.net/gh/tableau/extensions-api/lib/tableau.extensions.1.latest.js</a> "> document.addEventListener("DOMContentLoaded", function (event) {tableau.extensions.initializeAsync().then(function () {tableau.extensions.initalizeAddonsAsync("sync-scrollbar").then(function () {tableau.extensions.dashboardContent.dashboard.syncScrollbars(0, 1);});});});

In order of events, this is what happens:

Load the usual Extension API javascript file Load the tableau-extensions-addons.js file. This will add new functions like initalizeAddonsAsync and dashboard.syncScrollbars Call dashboard.syncScrollbars with the two dashboard items

Step 2 is quite interesting, this is where the magic happens (code is here: https://github.com/tfoldi/tc19/blob/master/extension-lib/tableau-extensions-addons.js). It first sends a message to the parent frame to load the sync-scrollbar-server, then it loads the sync-scrollbar-client to the current frame (which adds the syncScrollbars to the dashboard extension object). This sounds might complex, but it is painfully simple: we load one file to our frame, one file to the parent, and that's it.

Chain of events, we load the extensions API, then the extension adds API, that loads our client addon files in the current frame and the server addon in the Viz frame

And how the views are synchronized? When we call dashboard.syncScrollbars it sends a message to window.parent :

{

const SCROLLBAR_SYNC_REQUEST_MESSAGE = "ScrollbarSync-Request";



tableau.extensions.dashboardContent.dashboard.__proto__.syncScrollbars = function (idx0, idx1) {

console.log("Sync request from client");



window.parent.postMessage(

{

event_id: SCROLLBAR_SYNC_REQUEST_MESSAGE,

data: [idx0, idx1]

},

"*");

}

}

And on the Viz frame we just capture this message and set the syncing:

{

const SCROLLBAR_SYNC_REQUEST_MESSAGE = "ScrollbarSync-Request";

if (event.data && event.data.event_id === SCROLLBAR_SYNC_REQUEST_MESSAGE) {

const left = event.data.data[0];

const right = event.data.data[1];

// ... actual sync scroll implementation

// window.addEventListener('message', function (event) {if (event.data && event.data.event_id === SCROLLBAR_SYNC_REQUEST_MESSAGE) {const left = event.data.data[0];const right = event.data.data[1];// ... actual sync scroll implementation// https://github.com/tfoldi/tc19/blob/master/extension-lib/tableau-extensions-sync-scrollbar-server.js

Pretty slick, it’s a shame that we have to change a file on the Server to make it work. All the code from above are here: https://github.com/tfoldi/tc19/

Deployment

To deploy this example on your own environment all you have to this:

Have a Tableau Server when you have admin/root rights on OS level.

Add this code to vqlweb.js as described earlier in this post.

as described earlier in this post. Whitelist https://tfoldi.github.io/tc19/scroll-sync-extension/ URL as extension.

Deploy this workbook.

Enjoy.

Questions? Feedback?

Does it work — yes or no? Do you like this? Drop me a line, feedback is always appreciated.