Sharing Web UI Across Tableau’s Browser Client and Desktop Application

Empowering developers to iterate more quickly and focus on delivering a single, high-quality experience to users.

“We like our UI so much we wrote it twice!” That phrase captured the state of Tableau’s codebase circa 2017 when each new feature required two UI implementations. On one side was Tableau Desktop, using Qt for native UI across Windows and Mac. On the other were our web products: Tableau Online, Tableau Server, and Tableau Public — shipping nearly the same interface for the web built with JavaScript and HTML.

We resolved to empower developers to iterate more quickly and to focus on delivering a single, high-quality experience to our users. A solution for these goals also required that we continue to deliver incremental value in both our web and desktop products — meaning something like a massive migration of our desktop application to Electron was off the table.

We decided the best way forward was by hosting browser-based UI within Tableau Desktop. At that point in time there were already a few features using web-based UI in our desktop product, but truly sharing UI across platforms would take some engineering.

This post will define shared UI in the context of Tableau, our approach to implementing shared UI, and how things look today.

What is Shared UI?

Shared UI is browser-based UI shared across both our browser and native applications. For our server-backed products, the shared UI runs locally alongside the rest of our client code in our users’ favorite web browsers. For Tableau Desktop, we host this UI inside of QWebEngineViews​, a Chromium-based web view widget. The more interesting examples of shared UI meet two specific criteria: two-way communication between the native code and the hosted UI, and side-by-side integration of hosted UI and existing native UI.

Let’s look at a Tableau-specific example: dashboard extensions. These extensions allow customer-authored JavaScript to interact with multiple native visualizations.

Single_Checkbox_Parameter

The Single Checkbox Parameter extension

In the above image of Tableau Desktop, the selected dashboard object is an extension running inside of a web view. When published to a Tableau Server the exact same extension code will run, blissfully unaware of where it’s hosted. This extension blends in seamlessly with the rest of our in-house UI on both platforms thanks to React components available via a GitHub project: Tableau UI. On Desktop, the surrounding UI is native — implemented through QWidgets with Tableau-specific styling or visualizations rendered as images.

Dashboard extensions don’t exist in isolation from the rest of the application; two-way communication with product features is an essential part of their functionality. The Extensions API provides access to properties of the dashboard and for issuing commands to the native code. For example, the API provides functions to change parameters or filters applied to the dashboard.

Shared UI has proved a great fit for extensions, where our customers can easily plug in their own custom content. However, the application of shared UI is not limited to hosting external content; a number of controls built in-house (dialogs, side panes, and even entire tabs) are now implemented with the technique.

Implementing Shared UI

Tableau set the groundwork for adopting shared UI by spinning up a team to focus on the infrastructure which underpins the user-facing components.

The primary focus for this team was developing “platform abstractions”. These APIs provide a common interface to browser-based UI, hiding away the details of which platform (i.e., the browser or desktop client) is currently hosting it. One such API provides access to common UI components like context menus. The UI requests a context menu against the shared interface and the abstraction’s implementation under the hood materializes the correct native menu. Other APIs provide the ability to configure dialogs, drag and drop across the shared/native boundary, or interact with the underlying Tableau Workbook.

These abstractions were straightforward to implement for our browser client: at runtime we dynamically load JavaScript libraries which implement the interfaces. The JavaScript of the shared UI can then call directly into these libraries.

On desktop, the implementation is a bit more complex as native functionality is provided almost exclusively by C++ frameworks. We took advantage of another Qt library, QWebChannel, to provide the browser code near-transparent access to native C++ QObjects. Each of the objects, which we named interop objects in Tableau code, expose a set of methods and notifications roughly corresponding to the surface of each of the abstraction APIs. Thin JavaScript wrappers adapt any rough edges to fit the APIs exposed to the client code. On the C++ side these interop objects call into the appropriate business logic to perform any interesting operations.

Interop_Objects

This approach has worked well for us. The original team’s implementations of the minimal required APIs have been expanded and other teams have since added their own shared abstractions. This layer has freed UI developers to focus on delivering a single experience to customers unified across products.

With the general mechanics of shared UI working, there were plenty of smaller issues to address: packaging the UI content properly for both platforms, pooling web views to present shared dialogs quickly in Desktop, improving steps in this new developer workflow, and educating feature teams on all the pieces. This was an iterative process that built on fruitful partnerships with a number of teams building their UI through this new system.

Having laid the groundwork we still had to figure out how to steer the ship towards this new development paradigm. There are a lot of distinct UI elements, interactions, and dialogs for creating visualizations and dashboards in Tableau. We decided an incremental approach was the best way to achieve our goal of a single UI codebase. We rolled out the following guidelines to encourage org-wide adoption of shared UI:

  1. New features should be built as shared UI.
  2. Large functionality changes to existing, self-contained UI components should be re-written as shared UI. We trusted teams to work through the nuances around the feasibility of converting larger or more strongly coupled UI.

Once teams got their feet wet with a new feature or two it wasn’t hard to convince them to continue building their UI this way. They got to move faster and focus on the fun things — building new features for customers.

Teams at Tableau have now produced dozens of UI features with shared UI, and it is the standard approach for building new features.

Tableau Today

So where are we now? At a high level, we have both shared and native UI living side-by-side in Desktop — can you spot which pieces of UI are which?

Yes, we still have two code bases for viewing and authoring visualizations, but with each new piece of shared UI we continue to reduce the maintenance burden we carried with two versions of everything. Additionally, our developers can move faster — tweaking and improving a single experience rather than attempting to duplicate it in two places.

Our pursuit of a single, shared UI yielded a couple of extra benefits as well. For the Desktop product, where re-compiling C++ code had become an obstacle to fast iteration, developers can now tweak their UI with near instantaneous feedback in a standalone browser environment. Testing its integration with the main product is easier as well — their code can be re-loaded into the hosting web views without even restarting Tableau. Across applications, the strict boundaries enforced by the abstraction APIs have resulted in more modular code cleanly separated along functional and ownership lines.

It’s exciting to make building new features easier for our development team. Ultimately that gets our team back to what we are really here to do: help people see and understand their data.