const answerRequestObject = { "metadata": [{"type": "ANSWER"}], "permissions" : [ { "principal": { "type": "USER", "identifier": "{username}" }, "share_mode": "READ_ONLY" } ] 'record_offset': 0, // Adjust to do pagination 'record_size': 100000 // Adjust to do pagination (or handle in browser with table component) }
Build navigation to ThoughtSpot content
This documentation page covers two related aspects of integrating ThoughtSpot content into another application:
-
Creating URLs that load various ThoughtSpot objects correctly within the embedding app
-
Retrieving ThoughtSpot objects available to a given web application user
The embedding application can retrieve the available content for a ThoughtSpot user via REST API, then render the response as menu items or buttons that link to the URLs within the web app that load the proper ThoughtSpot embedded component with the desired object ID.
If you are using the ThoughtSpot AppEmbed component to embed any of the pages from the full ThoughtSpot application, please see the documentation for customizing navigation within the full app embed.
Create web app URLs for embedded ThoughtSpot pages๐
Modern web applications have various architectures for how URLs are translated into the actual content delivered to the userโs web browser.
The ThoughtSpot Visual Embed SDK uses different components depending on the object type to be loaded, so the URL must include a reference to both the ThoughtSpot object type and the object ID to load it properly.
ThoughtSpot object types and object IDs๐
Every object in ThoughtSpot is referenced by a globally unique identifier (GUID), which appears as the id:
or guid:
property in API responses and TML files.
Visual Embed SDK components require object IDs to load the correct content.
The object ID is part of the URLs to load objects in ThoughtSpot. For example, the GUID of the Liveboard appears at the end of the URL:
{your-domain}.thoughtspot.cloud/#/pinboard/ddd982ea-c7bc-4c00-9b49-c53fab949b34
When hardcoding ThoughtSpot object IDs, you can use the ThoughtSpot application URL or Visual Embed SDK playground to find the object IDs. Most developers switch to using the REST APIs to retrieve the object IDs as their integration project progresses (see below).
Web app URL patterns๐
Within the embedding web app, create one or more URL patterns that include variables for:
-
A reference to the ThoughtSpot object type (does not have to match ThoughtSpot names for the object types)
-
The ThoughtSpot object ID(s)
These two details are necessary to load the correct ThoughtSpot embed component with the desired ThoughtSpot object.
With defined URL patterns, you can customize links within ThoughtSpot so that e-mails and other exported references send users to the embedding web appโs pages rather than directly to the ThoughtSpot instance.
Example URL patterns for various component types:
LiveboardEmbed component for a Liveboard:
{web-app-name}.{your-domain}.com/analytics/dashboard/{liveboardId}
LiveboardEmbed component for a specific Viz on a Liveboard:
{web-app-name}.{your-domain}.com/analytics/dashboard/{liveboardId}/{vizId}
SearchEmbed component to a datasource:
{web-app-name}.{your-domain}.com/analytics/data/{datasourceId}
SearchEmbed component to an existing Answer:
{web-app-name}.{your-domain}.com/analytics/report/{answerId}
Create menus to embedded ThoughtSpot pages๐
Once you have the URL patterns defined within the embedding application to load ThoughtSpot content, you can build out the navigation elements to reach the content a given user has access to.
Navigation typically takes the form of elements in a menu, but could also be a dropdown selector or a set of buttons.
Please see the example on GitHub for a complete flow of REST API requests powering various navigation components to be rendered into an embedding applicationโs page.
Hardcoded links within a menu system๐
If your web application only embeds a limited set of ThoughtSpot objects, without granting users the ability to save their own objects, you can hardcode the object IDs and names of the ThoughtSpot content.
Object IDs in ThoughtSpot are GUIDs and thus are unique on every Org or instance, except separate instances that are intentionally replicated with the same GUIDs via TML import and export.
You may still use the /metadata/search
V2.0 REST API endpoint to retrieve the equivalent object IDs for content on various Orgs and environments in the process of "hardcoding" the menu system. Alternatively, you may keep a mapping file of equivalent objects during your CICD deployment process, that you use to generate the hardcoded URLs in the menu system.
Dynamic requests for content listings via REST API๐
Both the V1 and V2.0 REST APIs have endpoints for retrieving filtered lists of the content a user has access to.
The /metadata/search V2.0 REST API endpoint can be used by the administrators to retrieve content for any given user or can be called as the user by the web browser once they have signed-in via SSO.
The response to /metadata/search
can be parsed and split on tags, author or other properties, or you can make multiple calls to the endpoint with the various filtering arguments in each request. For example, favorite options can be specified to only retrieve objects that are marked as favorites by users.
The /metadata/list
V1 REST API endpoint is identical to the endpoint used within ThoughtSpot to rendering Answers, Liveboards, and Data pages, making it a simple path to replicate and customize the existing content listing pages within ThoughtSpot.
The process for generating a dynamic menu includes the following steps:
-
Request the userโs available content via the REST API
-
(Optional) Cache the results within the embedding application
-
Render the menu items and link by using the REST API response to build the URLs within the web app that will load ThoughtSpot content
If implementing a caching system for the ThoughtSpot content listings, implementing the content requests on the back-end may be preferred for easy access to other parts of the web application.
Requesting content for a user๐
An administrator account can use the permissions parameters of the /metadata/search V2.0 REST API endpoint to retrieve the list of content available for that user at either the READ_ONLY
or MODIFY
permission levels.
By default, the endpoint returns for Liveboards, but the metadata
section of the request can be set to specify LIVEBOARD
, ANSWER
, and LOGICAL_TABLE
in any desired combination.
If the web application has implemented other processes on the back-end using a ThoughtSpot service account with administrator privileges, this may be the simplest implemenation of retrieving the content listings rather than dealing with individual user access tokens for the requests.
Requesting content as a user๐
Rather than using an administrator level account to request content listings, you can instead have the REST API request scoped to the user themselves, and the REST API will always only return the content they have access to.
All REST API requests from the browser are scoped as the signed-in user, as long as the credentials
option of the REST API request has been set properly to include
.
const lbRequestObject = { "metadata": [{"type": "LIVEBOARD"}], 'record_offset': 0, // Adjust to do pagination 'record_size': 100000 // Adjust to do pagination (or handle in browser with table component) }
With cookieless trusted authentication, there is no browser session. Instead, an access token is retrieved for the user and used by the SDK. The same access token for authentication can be used to make REST API requests, or a second access token can be generated for use with REST API requests.
The trusted authentication pattern requires implementing a backend service to generate access tokens for any user. The token request service can instead be used by the back-end of the embedding web application to get an access token to make REST API requests for a user, rather than having it happen at the front-end within the web browser.
Build menu items and web app links๐
The response from the search REST API is an array of header objects, which includes the details needed to build out the menu items and the links within the web application to the pages that display ThoughtSpot content.
The /metadata/search
endpoint returns metadata_name
, metadata_id
and metadata_type
in each response item, which is enough information to build a simple menu and a link to the appropriate URL to display the content.
The V2.0 /metadata/search
endpoint has an additional metadata_header
key within the response, with the object containing the following properties along with many others, while the metadata/list
V1 endpoint contains them in a slightly different structure.
Within the metadata_header
section, name
and id
properties are identical to the metadata_name
and metadata_id
from the outer portion of the response. Additional properties the web application might use for display include:
-
description
Text description added to content by creator
-
authorDisplayName
Display name of the object creator or current owner
-
authorName
Username of the object creator or current owner
-
created
Object creation timestamp (to milliseconds)
-
modified
Last edit timestamp (to milliseconds)
-
tags
Array of tag objects, each with a
name
property among other details
Individual visualizations on a Liveboard can be loaded using the LiveboardEmbed
component by supplying both liveboardId
and vizId
.
The display of a visualization from a Liveboard differs from a saved Answer object, which is loaded via the SearchEmbed
component. The saved answer object always displays the ThoughtSpot search bar and UI actions for editing an Answer, whereas the visualizations display fewer UI elements and show the menu items in the More menu .
Setting the include_visualization_headers
request parameter to true
will bring back the list of all visualization details with any Liveboard response. This request requires a separate API call for each Liveboard in the V1 REST API.
Replicating the ThoughtSpot UI Liveboards or Answers page๐
As mentioned before, the /metadata/list
V1 REST API provides the same details as the internal REST API used to display the pages within the ThoughtSpot UI, making it easy to "replicate" those pages exactly within the embedding web appโs own UI. The V2.0 REST API includes these details within the metadata_headers
section of its response so it can be used for a similar purpose as well (see see the example on GitHub for V2.0 equivalents.
The endpoint can only request one object type at a time:
-
PINBOARD_ANSWER_BOOK
for Liveboards -
QUESTION_ANSWER_BOOK
for answers -
LOGICAL_TABLE
for data objects
Data objects can be filtered using an additional subtype
parameter to limit the query specifically to ThoughtSpot tables, worksheets, or views.
There are additional parameters for sorting and a category
parameter that can filter the response to show only the objects created or marked as favorites by the logged-in user.
REST API calls are asynchronous. The following is an example function returning the response as a JSON object using fetch():
async function metadataListRestApiCall(args){
// args = { 'type', 'category', 'sortOn', 'sortAsc', 'tagnames' }
let type = args['type'].toLowerCase();
const publicApiUrl = 'callosum/v1/tspublic/v1/';
let endpoint = 'metadata/list';
// Easy type names match ThoughtSpot UI names for objects
const typesToApiType = {
'liveboard': 'PINBOARD_ANSWER_BOOK',
'answer': 'QUESTION_ANSWER_BOOK',
'datasource' : 'LOGICAL_TABLE', // datasource doesn't distinguish sub-types
'table' : 'ONE_TO_ONE_LOGICAL',
'view' : 'AGGR_WORKSHEET',
'worksheet' : 'WORKSHEET'
}
// batchsize = -1 gives all results
let apiParams = { 'batchsize' : '-1'};
console.log(type);
// The three datasource types can be specified using 'subtype'
if (type == 'table' || type == 'view' || type == 'worksheet'){
let subtype = [typesToApiType[type]];
apiParams['type'] = 'LOGICAL_TABLE';
apiParams['subtypes'] = `["${subtype}"]`;
}
else {
apiParams['type'] = typesToApiType[type];
}
// Category arguments
let category = 'ALL';
if ('category' in args){
if ( args['category'] == 'MY' || args['category'] == 'ALL' || args['category'] == 'FAVORITE'){
category = args['category'];
apiParams['category'] = category;
}
}
// Sort arguments
if ('sortOn' in args){
if (args['sortOn'] !== null){
apiParams['sort'] = args['sortOn'];
}
}
if ('sortAsc' in args){
if (args['sortAsc'] === true){
apiParams['sortascending'] = 'true';
}
if (args['sortAsc'] === false){
apiParams['sortascending'] = 'false';
}
}
console.log(apiParams);
const searchParams = new URLSearchParams(apiParams);
const apiFullEndpoint = tsURL + publicApiUrl + endpoint + "?" + searchParams.toString();
console.log(apiFullEndpoint);
return await fetch(
apiFullEndpoint, {
method: 'GET',
headers: {
"Accept": "application/json",
"X-Requested-By": "ThoughtSpot"
},
credentials: "include"
})
.then(response => response.json())
.then(data => data['headers']) // metadata/list info is really in the 'headers' property returned
.catch(error => {
console.error("Unable to get the metadata/list response: " + error)
});
}
The results of this REST API request can be directed into a rendering function using .then()
:
metadataListRestApiCall(
{
'type': 'liveboard',
'sortOn': 'NAME',
'sortAsc' : true,
'category': 'ALL'
})
.then(
(listResponse) => renderNavigationFromResponse(listResponse) // Use your own rendering function here
);
Rendering Liveboards or Answers pages similar to ThoughtSpot UI๐
If you want to render something very close to the 'Answers' or 'Liveboards' pages within the ThoughtSpot UI, your rendering function will grab the name
, id
, tags
, modified
and authorDisplayName
properties and make a table in that order (feel free to leave out any undesired elements):
function tableFromList(listResponse){
console.log(listResponse);
let t = document.createElement('table');
// Make table headers
let thead = document.createElement('thead');
t.append(thead);
let thr = document.createElement('tr');
thead.append(thr);
let headers = ['Name', 'Tags', 'Modified', 'Author'];
for (let i=0, len=headers.length; i < len; i++){
let th = document.createElement('th');
th.innerText = headers[i];
thr.append(th);
}
// Go through response and build rows
for (let i=0, len=listResponse.length; i < len; i++){
let tr = document.createElement('tr');
// Name Column
let name_td = document.createElement('td');
name_td.innerHTML = '<a href="#" onclick="loadContent("' + listResponse[i]['id'] + '")>' + listResponse[i]['name'] + '</a>';
//name_td.append(name_text);
console.log(name_td);
tr.append(name_td);
// Tags column
let tags_td = document.createElement('td');
console.log(listResponse[i]['tags']);
// Tags is an Array of Tag objects, with properties ('name' being the important one)
if (listResponse[i]['tags'].length > 0){
let tagNames = [];
for(let k = 0, len = listResponse[i]['tags'].length; k<len; k++){
let tagName = listResponse[i]['tags'][k]['name'];
tagNames.push(tagName);
}
tags_td.innerText = tagNames.join(', ');
}
tr.append(tags_td);
// Modified Date column
let modified_td = document.createElement('td');
modified_td.innerText = listResponse[i]['modified'];
tr.append(modified_td);
let author_td = document.createElement('td');
author_td.innerText = listResponse[i]['authorDisplayName'];
tr.append(author_td);
t.append(tr);
}
return t;
}
The function in the preceding example merely creates the table, it does not place it on the page. You can continue chaining using .then()
to place the table in the appropriate place on your web application page :
metadataListRestApiCall(
{
'type': 'liveboard',
'sortOn': 'NAME',
'sortAsc' : true,
'category': 'ALL'
})
.then(
(response) => tableFromList(response)
).then(
(table) => document.getElementById('main-content-div').append(table)
);
Note that the loadContent()
function referenced in the anchor tag created for the name column in the function above is a placeholder representing whatever is necessary to load that type of ThoughtSpot content in the web application. The actual design you choose for your application will determine the code you need to go from the navigation component to loading the ThoughtSpot content.
Retrieve individual visualizations using the V1 REST API๐
To retrieve a list of visualizations from a Liveboard with the V1 REST API, you can use the get visualization headers REST API endpoint.
async function metadataGetVizHeadersRestApiCall(liveboardGuid){
// args = { 'type', 'category', 'sortOn', 'sortAsc', 'tagnames' }
let type = args['type'].toLowerCase();
const publicApiUrl = 'callosum/v1/tspublic/v1/';
let endpoint = 'metadata/listvizheaders';
// batchsize = -1 gives all results
let apiParams = { 'id' : liveboardGuid};
const searchParams = new URLSearchParams(apiParams);
const apiFullEndpoint = tsURL + publicApiUrl + endpoint + "?" + searchParams.toString();
console.log(apiFullEndpoint);
return await fetch(
apiFullEndpoint, {
method: 'GET',
headers: {
"Accept": "application/json",
"X-Requested-By": "ThoughtSpot"
},
credentials: "include"
})
.then(response => response.json())
//
.then(data => data) // metadata/list info is really in the 'headers' property returned
.catch(error => {
console.error("Unable to get the metadata/listvizheaders response: " + error)
});
}