# Advanced Add-ons
While the most common use-case for Sisense add-ons is developing new widget types, there are many more advanced custom features that can be achieved with an add-on. The goal of this tutorial is to demonstrate various advanced techniques useful in developing this type of advanced add-ons.
# Intro
In this tutorial, you will create an add-on that adds a new feature to Sisense. You will modify the Sisense user interface, utilize various events, compose and execute JAQL queries with asynchronous HTTP requests, and modify dashboard metadata at run-time to control what data is displayed to the user.
Please Note
This tutorial will cover the key parts of the add-on's development, but does not cover 100% of the required code.
You can find a full implementation of this add-on in this github repository (opens new window)
Add-on Requirements
These are the made-up product requirements for the add-on you will be developing:
We want to display a dashboard on a wall-mounted TV, that will show stats for different company departments. We would like it to cycle between all departments every few seconds.
- Add a toggle for dashboard filters of the "single select list" type called "rotate values"
- Only one filter in the dashboard can have this toggle on at a type
- When the dashboard has a "rotating values" filter, automatically update the selected value for that filter every 15 seconds, cycling through all available values
# Steps
# 1. Creating a new add-on
In this step, you will create a blank new add-on to start working on
Follow the instructions in the Building JavaScript Add-ons for Sisense tutorial to create a new add-on called RotatingFilter
, in a folder with the same name.
Before the next step, you should have a plugin.json
file, a main.6.js
file for your code, and a config.6.js
file for configuration values.
What you should have in plugin.json
:
{
"name": "RotatingFilter",
"source": [
"main.6.js"
],
"style": [
],
"pluginInfraVersion": 2
}
What you should have in config.6.js
:
module.exports = {
rotationInterval: 15000, // Modify this if you need to have a different interval. (in milliseconds)
maxValuesToLoad: 100 // Modify this if you need to load more/fewer values to rotate on.
}
What you should have in main.6.js
:
import { rotationInterval, maxValuesToLoad } from './config.6';
In all subsequent steps, you will add your code to main.6.js
.
# 2. Adding a menu item
In this step, you will add a "toggle" type menu item to the dashboard filter menu
You can modify various menus in the Sisense UI using the beforemenu
global event. In this case, you'll want the dashboard-filter
menu name:
prism.on('beforemenu', (ev, args) => {
if (args.settings.name === 'dashboard-filter') {
// <<Code for the next step will go here>>
}
}
Create a new menu item of "toggle" type using the command pattern:
const rotateFilterToggle = {
id: 'rotate-filter',
type: 'toggle',
command: {
title: 'Rotate Values',
desc: 'Rotate the filter\'s selected value at an interval',
canExecute: (args) => {
},
execute: (args) => {
},
isChecked: (args) => {
}
},
commandArgs: {
}
};
Append the menu item to the menu:
args.settings.items.push(rotateFilterToggle);
At this stage, this toggle should appear in the menu for any dashboard filter, and it won't do anything.
# 3. Working with metadata
In this step you will implement the canExecute
method to make the toggle only appear for "single select" filters
First, write a helper function to determine if a given filter object is a "single select" filter or not:
function checkSingleFilter(filter) {
// Can't be a cascading (dependant) filter
if (filter.isCascading) return false;
// Must be a "member" filter
if (!filter.jaql.filter.members) return false;
// Must be "single select" filter
return !filter.jaql.filter.multiSelection;
}
Now, make sure the filter object is available to the command's canExecute
method by adding it to commandArgs
:
commandArgs: {
filter: args.settings.scope.item
}
Lastly, implement canExecute
to return true
only for "single select" filters:
canExecute: (args) => {
return checkSingleFilter(args.filter);
}
At this stage, only "single select" type filters should have the new toggle in their menu.
# 4. Storing custom variables on the dashboard
In this step, you will store the toggle's state on the dashboard, and make sure the toggle corresponds to that state at all times.
First, you'll need access to the dashboard object when the command is evaluated and executed. Add the dashboard object to commandArgs
:
commandArgs: {
filter: args.settings.scope.item,
dashboard: args.settings.scope.dashboard
}
Now, implement the execute
method to toggle a custom dashboard property (for example, xRotatingValuesFilter
using the x
prefix to denote an extension).
When the feature is on, store the relevant filter name. When the feature is turned off, clear this property:
execute: (args) => {
// using the filter's dimension as an ID
const filterKey = args.filter.jaql.dim;
// Toggle the dashboard's rotating filter
if (args.dashboard.xRotatingValuesFilter && args.dashboard.xRotatingValuesFilter === filterKey) {
delete args.dashboard.xRotatingValuesFilter;
}
else {
args.dashboard.xRotatingValuesFilter = filterKey;
}
}
This code also takes care of only having one filter rotating at a time, as toggling it on for another filter will simply override the property with a new ID.
You'll now want to update the dashboard so that this property is persisted:
args.dashboard.$dashboard.updateDashboard(args.dashboard, "xRotatingValuesFilter");
Lastly, implement the isChecked
method so that the toggle correctly shows its state, by checking if the xRotatingValuesFilter
property contains the ID of the filter for which the menu is currently displayed:
isChecked: (args) => {
return args.dashboard.xRotatingValuesFilter === args.filter.jaql.dim;
}
At this stage, you should be able to toggle "Rotate Values" on for any single-select filter, and upon refreshing the page the same filter should still have the toggle in the "on" state until cleared by you.
# 5. Working with the dashboard loaded event
In this step, you will detect the feature's state when a new dashboard loads, so that you can initiate the rotation when it is turned on.
Add another global event, dashboardloaded
, to execute your code whenever a new dashboard is loaded. In this event, make sure that the custom property xRotatingValuesFilter
is set before continuing on. If its not set, simply exit the method; and if it is, grab the appropriate filter object so it can be used in the next steps:
prism.on('dashboardloaded', (ev, args) => {
// If no rotating value filter is defined, do nothing
if (!args.dashboard.xRotatingValuesFilter) return;
// Find the appropriate filter
const rotatingFilter = args.dashboard.filters.$$items.find((item) => {
return args.dashboard.xRotatingValuesFilter === item.jaql.dim;
});
// Next steps will add more code here
});
Notice how in the last few steps, the same approach was used to identify a filter (based on its .jaql.dim
property compared to the xRotatingValuesFilter
).
Lastly, to make sure that the dashboardloaded
event is triggered when the user toggles the feature on, you can automatically reload the page for the user by adding the following code to the end of the execute
method:
window.location.reload();
This is optional - if left out, the user will have to manually refresh the page before the rotation begins.
At this stage, the user can toggle rotation on for a filter, and the dashboard will automatically reload, keeping the desired state.
# 6. Running JAQL queries
In this step, you will retrieve the values that can be selected in the filter by executing an HTTP request to the JAQL endpoint with a valid query.
JAQL Syntax
Learning the JAQL syntax used by Sisense for querying can be useful for add-on development, as well as for a better overall understanding of how Sisense works.
Refer to the JAQL Tutorial and JAQL Syntax Reference to learn more!
First, write a function to compose a JAQL query that retrieves all the unique values of a single column, based on the dimension of the filter. It's a good idea to limit how many results are returned, just in case this is applied to a column that has too many values to load at once (those probably won't make sense for this feature anyway).
You will need the filter's dimension ID and data source:
async function getFilterValues(filter, dashboard) {
// Get the filter's dimension id
const dim = filter.jaql.dim;
// Get the filter's datasource, default to dashboard's primary source
const ds = filter.jaql.datasource || dashboard.datasource;
// Construct a JAQL query
const jaql = {
datasource: ds,
metadata: [{
dim,
sort: 'asc'
}],
offset: 0,
count: 100
};
}
Now, execute this query by sending an HTTP request to
// Construct the correct URL for the JAQL query
const url = `/api/datasources/${ds.title}/jaql`;
// Execute the JAQL query and return the values
try {
const response = await executeAjaxPost(url, jaql);
return response.values;
}
catch (err) {
throw err;
}
You can implement executeAjaxPost
with a variety of JavaScript libraries capable of executing async HTTP requests. Note that as this request will be executed from the Sisense client application, you don't need to worry about authentication - the user's cookie will be automatically appended to the request.
Lastly, invoke the function getFilterValues
within the dashboardloaded
event, and store the values in a variable for use later:
let values, idx = 0;
getFilterValues(rotatingFilter, args.dashboard).then((result) => {
// cache the results for further use
values = result;
});
At this stage, when a dashboard with filter rotation is loaded, you should see the JAQL request execute (via the browser's network view) and have the values ready to start cycling in the filter.
Sisense REST API
The endpoint used in this example is part of Sisense's vast array of REST APIs, many of which can come in handy when developing add-ons.
Refer to the REST API Documentation and REST API Reference to learn more!
# 7. Modify filters via code
In this step, you will kick off an interval that sets a new value in the filter every few (15) seconds, to complete the feature's development.
Add a setInterval
to the code executed after getFilterValues
:
getFilterValues(rotatingFilter, args.dashboard).then((result) => {
values = result;
setInterval(() => {
// Code will be added here
}, 15000);
});
With every tick of the interval, progress the idx
variable by 1 and grab the relevant value:
if (idx >= values.length - 1) idx = 0;
else idx++;
const newValue = values[idx];
Modify the filter's value:
rotatingFilter.jaql.filter.members = [newValue];
And finally, update the dashboard filter without persisting it so that the dashboard is refreshed with new data, but the client doesn't execute an HTTP request to save it.
Since the value keeps rotating indefinitely, it would be a waste of network traffic to save the new value every 15 seconds
args.dashboard.filters.update(rotatingFilter, {
refresh: true,
save: false,
unionIfSameDimensionAndSameType: true
});
At this stage, when a dashboard with filter rotation is loaded, you should see filter's value start changing every 15 seconds, with the dashboard refreshing to new data every time.
Note that it might take a few seconds for the rotation to start, while the available values are being retrieved.
# Conclusion
By following the steps above, you have interacted with various APIs and capabilities of Sisense add-ons beyond adding new widget types, such as:
- Global events (
beforemenu
anddashboardloaded
) - Modifying UI elements (context menus)
- Utilizing and modifying viewmodels (
filter
anddashboard
) - Causing Sisense to pick up changes to viewmodels and persist them (
$dashboard.updateDashboard()
andfilters.update()
) - Composing and executing JAQL queries, and using the Sisense REST API from client side add-ons
With these new skills, you'll be able to develop other advanced add-ons to truly customize Sisense for your users.
# Things to note
You should now have a nearly complete add-on that you can install on a development environment and try out, but note that the code snippets in this tutorial were simplified for brevity, and do not encompass 100% of the code required for a production grade solution.
Below are a few modifications you may consider to make this add-on more robust:
- Storing the key returned by
setInterval
and using it to clear the interval when the rotation is toggled off, instead of reloading the page. - Handling
datetime
filters/dimensions, which require alevel
property in addition to the dimension ID. - Using the
config.6.js
file to store configurable values for settings such as the rotation interval and maximum number of values. - Writing a detailed
readme.md
file for the add-on. - Improving error handling and friendly error messages.
# Appendix
# Single Select Filter
A single-select filter is a "List" type filter, with "single select" mode turned on - it shows radio-buttons instead of check-boxes, allowing only one value to be selected at a time:
# AJAX
This implementation uses native browser capabilities (XHR, JSON, Promise) only:
async function executeAjaxPost(url, body) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader('Content-type', 'application/json');
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const response = xhr.responseText;
resolve(JSON.parse(response));
}
else {
reject(xhr);
}
}
};
xhr.send(JSON.stringify(body));
});
}