Building Your First Tableau Extension using ReactJs
For the last one month I have been looking at Tableau extensions as a way to add more interesting analytics for a client's dashboards. They are great tools to enhance the functionality of existing dashboards by adding visualizations that are not provided by Tableau. In this blog I will explain how to build your first Tableau extension using ReactJs
Development Environment Setup
Before starting with Tableau extension development, you will need to setup your development environment.
Following are some of the tools you will need to setup :
- NodeJs : I will be using NodeJs version >= 12 for this tutorial. You can download NodeJS package for your operating from the below link.
- Yarn (Optional) : I prefer yarn over npm for package management. Just a personal preference, you can use either.
- Code Editor : I use WebStorm to setup and edit my NodeJS code. You can any editor which has support for JavaScript code editing (e.g. Sublime Text, Atom, Visual Studio Code)
Once you have all these tools setup on your machine, we are all set to start building our first Tableau extension.
Create a new ReactJs App
Run the following command in a terminal to create a new ReactJs app
npx create-react-app first-tableau-extension
Once the script finishes running go into the new first-tableau-extension directory
cd first-tableau-extension/
yarn start
The yarn command should give an output similar to the one below
Compiled successfully!
You can now view first-tableau-extension in the browser.
Local: http://localhost:3000
On Your Network: http://172.31.98.101:3000
Note that the development build is not optimized.
To create a production build, use yarn build.
Open a browser and enter the url http://localhost:3000 in it. You should see the following output in the browser.
Now that we have successfully setup the ReactJs app we will go ahead and start adding features for Tableau extension.
Add copyfiles node package to the project
We will need the copyfiles node package for adding the tableau extension library when we build the project
yarn add copyfiles
Adding Tableau extension library to the project
Create a new folder called lib under the tableau extension root directory.
Download tableau.extensions.1.latest.min.js from https://github.com/tableau/extensions-api/tree/master/lib and place it in the lib directory
Update the package.json file to change the start and build scripts, to copy the tableau extension library JS into the public folder before starting the server/building the package. Below are the changes that need to be done to start and build scripts.
"start": "copyfiles -f lib/tableau.extensions.1.latest.js ./public && react-scripts start"
"build": "copyfiles -f lib/tableau.extensions.1.latest.js ./public && react-scripts build"
The updated package.json should look like the one below.
{
"name": "first-tableau-extension",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"copyfiles": "^2.2.0",
"react": "^16.13.0",
"react-bootstrap": "^1.0.0-beta.17",
"react-dom": "^16.13.0",
"react-scripts": "3.4.0"
},
"scripts": {
"start": "copyfiles -f lib/tableau.extensions.1.latest.min.js ./public && react-scripts start",
"build": "copyfiles -f lib/tableau.extensions.1.latest.min.js ./public && react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Finally go public/index.html and add the below under the head tag, to import the tableau extension library when serving/building the project.
<script src="%PUBLIC_URL%/tableau.extensions.1.latest.min.js"></script>
Creating extension configuration file
Tableau extensions require a .trex file to load the configuration of the extension into the Tableau dashboard. We will now create the .trex file for our extension. Create a file named first-tableau-extension.trex in your extension project directory.
Copy the below contents into your trex file. You can then customize properties like author, extension id etc.
<?xml version="1.0" encoding="utf-8"?>
<manifest manifest-version="0.1" xmlns="http://www.tableau.com/xml/extension_manifest">
<dashboard-extension id="com.nerdyandnoisy.tableau.extension.first" extension-version="0.1.0">
<default-locale>en_US</default-locale>
<name resource-id="name"/>
<description>First Tableau Extension</description>
<author name="Sunny Dave" email="sunny.dave@nerdyandnoisy.com" organization="Nerdy And Noisy"
website="https://www.nerdyandnoisy.com"/>
<min-api-version>1.0</min-api-version>
<source-location>
<url>http://localhost:3000/index.html</url>
</source-location>
<icon>iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAlhJREFUOI2Nkt9vy1EYh5/3bbsvRSySCZbIxI+ZCKsN2TKtSFyIrV2WuRCJuBiJWxfuxCVXbvwFgiEtposgLFJElnbU1SxIZIIRJDKTrdu+53Uhra4mce7Oe57Pcz7JOULFisViwZ+29LAzOSjQYDgz1ZcCvWuXV11MJpN+OS/lm6179teqH0yDqxPTCyKSA8DcDsyOmOprnCaeP7459pdgy969i0LTC3IO/RQMyoHcQN+3cnljW3dNIFC47qDaK3g7BwdTkwBaBELT4ZPOUVWgKl4ZBnjxJPUlMDnTDrp0pmr6RHFeEjjcUUXPDGeSEwDN0Xg8sivxMhJNjGzbHd8PkM3eHRfkrBM5NkcQaY2vUnTlrDIA0NoaX+KLXFFlowr14tvVpqb2MICzmQcKqxvbumv+NAhZGCCIPwEw6QWXKYRL/VUXO0+rAUJiPwAk5MIlgVfwPjjHLCL1APmHN94ZdqeYN+NW/mn6I4BvwQYchcLnwFhJMDiYmlRxAzjpKWZkYkUCcZ2I61wi37tLbYyjiN0fHk5Oz3nGSLSzBbNHCF35R7f6K1/hN9PRhek11FrymfQQQKB4+Gl05P2qNRtmETlXW7e+b2z01dfycGNbfFMAbqNyKp9Jp4rzOT8RYFs0njJkc2iqsCObvTsOsDWWqA5C1uFy+Uz/oXJeKwVT4h0RmPUXhi79vuC0Ku6yOffTK3g9lfxfDQAisY516sg5kfOCiJk7HoLt2cf9b/9LANAc7dznm98PagG1fUOZ9IP5uMB8Q4CPoyNvausapkTt3rNMuvdf3C/o6+czhtdwmwAAAABJRU5ErkJggg==</icon>
</dashboard-extension>
<resources>
<resource id="name">
<text locale="en_US">First Tableau Extensions</text>
</resource>
</resources>
</manifest>
Update the ReactJS code to interact with the Tableau dashboard
For the UI of this extension we will be using the react-bootstrap package. You can use the below yarn command to install the react-bootstrap package.
yarn add react-bootstrap
The thing to note when creating extensions is that the only way to access data is through an existing sheet on the dashboard. So, next we will need to create a component which can be used to select the sheet which will be used to access to the data.
Let's start by creating a JavaScript file named SheetListComponent.js. In the code below we are iterating through all the sheets in the dashboard and creating buttons for each sheet, which can then be used by the user to select the sheet they want to use for getting the data for our extension.
import React from 'react';
import { Button } from 'react-bootstrap';
function SheetListComponent(props) {
const makeSheetButton = (sheetName) => {
return (
<Button key={sheetName} variant='light' block
onClick={() => props.onSelectSheet(sheetName)}>
{sheetName}
</Button>
);
}
const sheetButtons = props.sheetNames.map(sheetName => makeSheetButton(sheetName));
return (
<div>
{sheetButtons}
</div>
);
}
export default SheetListComponent;
Next up we will creating a small component which will be used as a loading screen while the extension is loading configuration or data.
import React from 'react';
import Spinner from 'react-spinkit';
import './styles/LoadingIndicator.css';
function LoadingIndicatorComponent(props) {
return (
<div className='loadingIndicator'>
<h3>{props.msg}</h3>
<Spinner name='three-bounce' fadeIn='none' />
</div>
);
}
export default LoadingIndicatorComponent;
We will need a CSS file along with the LoadingIndicator just to add little style to the loading indicator.
.loadingIndicator {
width: 200px;
height: 200px;
text-align: center;
position: absolute;
left: calc(50vw - 100px);
top: calc(50vh - 100px);
}
Now that all of our basic components are ready, we can start wiring them in. Let's create a new JS file called Extension.js. The first thing we will do here is initialize the tableau extension, and get selected
tableau.extensions.initializeAsync().then(() => {
//Here you can write code that needs to be executed post the extension initialization
})
The first thing we will do inside the initializeAsync promise is to get all the sheet names from the dashboard on which this extension has been added.
tableau.extensions.initializeAsync().then(() => {
const sheetNames = tableau.extensions.dashboardContent.dashboard.worksheets.map(worksheet => worksheet.name);
})
Now let's create a state variable to store these sheet names
const [sheetNames, setSheetNames] = useState([]);
We can now add the sheetNames to the state variable
tableau.extensions.initializeAsync().then(() => {
const sheetNames = tableau.extensions.dashboardContent.dashboard.worksheets.map(worksheet => worksheet.name);
setSheetNames(sheetNames);
})
Next step we need to create a sheet variable to store the selected sheet. We will also use this variable to determine if a sheet has been selected or not. Along with it, we are also going to determine a state variable to manage the loading state of the extension.
const [isLoading, setIsLoading] = useState(true);
const [selectedSheet, setSelectedSheet] = useState(undefined);
Now let's add the logic to show the Sheet List component we created in case no sheet is selected. For this we will use the Modal component in react-bootstrap, and the SheetListComponent we created above. So make sure you import those dependencies.
import {Modal} from "react-bootstrap";
import {SheetListComponent} from "./SheetListComponent";
We will now create a variable that we will use for setting what needs to be shown on the extension UI based on our conditions, and set it to show a modal window to select the sheet when no sheet is selected.
let output = <div>Sheet Selected : {selectedSheet}</div>;
if(!selectedSheet){
output =
<Modal show>
<Modal.Header>
<Modal.Title>Choose a Sheet</Modal.Title>
</Modal.Header>
<Modal.Body>
<SheetListComponent sheetNames={sheetNames}/>
</Modal.Body>
</Modal>
};
return (
<div>{output}</div>
);
Now that we have this in place, let's try to test if this works fine. One last step before we can test this in Tableau is to edit the App.js to add the new Extension component we have created to it. The updated App.js should like the one below
import React from 'react';
import './App.css';
import Extension from "./Extension";
function App() {
return (
<Extension/>
);
}
export default App;
Launch Tableau desktop, and open your Tableau workbook. On a dashboard view use the left Dashboard Objects pane to select Extension. Once done drag and drop the extension into the dashboard. You should see a popup like the one below.
Select My Extensions, and then select the trex file we created above, to load the extension. Once you open the trex file Tableau should give you a popup asking to allow running the extension.
Once you click ok the extension should load and show you a list of all the sheets that are in your current dashboard
Wallah !! The extension is working fine.
Save settings
Now we are going to look at how we can save some of the extension settings so that when the dashboard loads the extension does not ask the end user for configurations, and hence making the interaction feel more native.
We are going to save the selected sheet into the extension settings and pull it from the settings the next time the extension reloads.
First we will create a function to store selected sheet that is returned by the SelectSheetComponent.
const onSelectSheet = (sheet) => {
tableau.extensions.settings.set('sheet',sheet);
setIsLoading(true);
tableau.extensions.settings.saveAsync().then(() => {
setSelectedSheet(sheet);
});
};
In the above code we are using the settings property in tableau extension. Settings is a key value pair object where you can save string properties as values. Once done you can save the setting, using the saveAsync function which returns a promise, inside which you can do other operations post saving of the setting. We set the selected sheet in the state object once the setting has been saved.
Next we will update the SheetListComponent tag inside the modal window to pass this function to the component
<SheetListComponent sheetNames={sheetNames} onSelectSheet={onSelectSheet}/>
Finally we will update logic within the promise for initializeAsync function of the extension to pick up the selected sheet from the settings.
useEffect(() => {
tableau.extensions.initializeAsync().then(() => {
const sheetNames = tableau.extensions.dashboardContent.dashboard.worksheets.map(worksheet => worksheet.name);
setSheetNames(sheetNames);
const selectedSheet = tableau.extensions.settings.get('sheet');
setSelectedSheet(selectedSheet);
})
},[]);
Now reload your extension and select a sheet from the dashboard.
You should see something like this in the output.
Now try reloading the extension again, it should pick up the selected sheet from the setting and directly take you to the above view without you have to select the sheet again.
Get data from the selected tableau sheet
So finally, we can now pull our data from our selected sheet.
First we need to get the selectedSheet object using the tableau extension API. We will create a function for this.
const getSelectedSheet = (sheet) => {
const sheetName = sheet || selectedSheet;
return tableau.extensions.dashboardContent.dashboard.worksheets.find(worksheet => worksheet.name === sheetName);
};
Then we need to know what rows (marks) are selected, and also when that selection is changed. Tableau extension API has a function
Let's create a function to load the selected marks from the selected sheet.
const loadSelectedMarks = (sheet) => {
if(unregisterEventFn){
unregisterEventFn();
}
const worksheet = getSelectedSheet(sheet);
worksheet.getSelectedMarksAsync().then(marks => {
const worksheetData = marks.data[0];
const rows = worksheetData.data.map(row => row.map(cell => cell.value));
const headers = worksheetData.columns.map(column => column.fieldName);
setRows(rows);
setHeaders(headers);
setIsLoading(false);
});
unregisterEventFn = worksheet.addEventListener(tableau.TableauEventType.MarkSelectionChanged, () => {
setIsLoading(true);
loadSelectedMarks(sheet);
})
};
One interesting thing in the above code is the line where we add an event listener to the worksheet. Tableau extension API provides different events that we can tap into to update our extension UI, for this example we have used the MarkSelectionChanged event.
We will also have to update initial loading code (initializeAsync) and the sheet selection code to add a call to the loadSelectedMarks function.
useEffect(() => {
tableau.extensions.initializeAsync().then(() => {
const sheetNames = tableau.extensions.dashboardContent.dashboard.worksheets.map(worksheet => worksheet.name);
setSheetNames(sheetNames);
const selectedSheet = tableau.extensions.settings.get('sheet');
setSelectedSheet(selectedSheet);
const sheetSelected = !!selectedSheet;
setIsLoading(sheetSelected);
if(!!sheetSelected){
loadSelectedMarks(selectedSheet);
}
})
},[]);
const onSelectSheet = (sheet) => {
tableau.extensions.settings.set('sheet',sheet);
setIsLoading(true);
tableau.extensions.settings.saveAsync().then(() => {
setSelectedSheet(sheet);
loadSelectedMarks();
});
};
Now we will create a simple grid to display the selected data. For this we will use a react library called react-virtualized.
yarn add react-virtualized
Next we will create a simple data table to display the selected data.
import React from 'react';
import { AutoSizer, MultiGrid } from 'react-virtualized';
import './styles/DataTable.css';
function DataTableComponent(props) {
const cellRenderer = ({ columnIndex, key, rowIndex, style }) => {
if (rowIndex === 0) {
return (<div className='cell header' key={key} style={style}>
<button type='button' className='link-button'>{props.headers[columnIndex]}</button>
</div>);
} else {
return (<div className={'cell ' + (rowIndex % 2 === 1 ? 'odd' : 'even')} key={key} style={style}>
{props.rows[rowIndex - 1][columnIndex]}
</div>);
}
};
return (
<div className='dataTable'>
<AutoSizer>
{({ height, width }) => (
<MultiGrid
key={props.dataKey || -1}
fixedRowCount={1}
className='grid'
cellRenderer={cellRenderer}
columnCount={props.headers.length}
columnWidth={150}
height={height}
rowCount={props.rows.length + 1}
rowHeight={30}
width={width}
/>
)}
</AutoSizer>
</div>
);
}
export default DataTableComponent;
Let's add the DataTableComponent to the Extension component and add as part of out output.
const mainContent = (rows.length > 0)
? (<DataTableComponent rows={rows} headers={headers}/>)
: (<h4>No Data Found</h4>);
let output = <div>{mainContent}</div>;
Reload the extension and you should see a message saying "No Data Found"
Now try selecting some marks from your selected sheet, and the data should start appearing in your extension view.
And there we have our first tableau extension, which can interact with the tableau dashboard and pull data from the sheets.
The code for this sample extension is available in the below GitHub repository.