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) }
Create dynamic menus and navigation
Rather than embedding the ThoughtSpot application’s home or other pages, an embedding app can create dynamic menus or other navigation elements by requesting the content a user has access to via the ThoughtSpot REST API.
The embedding app will need routes for pages that dynamically load the ThoughtSpot embed components.
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.
Please see the code example on GitHub for a complete flow of REST API requests powering various navigation components to be rendered into an embedding application’s page.
Create menus to embedded ThoughtSpot pages🔗
Once you have the URL routes defined within the embedding application to load ThoughtSpot content, you can write code that generates links to these ThoughtSpot routes.
Navigation elements often take the form of a sidebar menu, a dropdown selector, a set of tiles, or individual buttons.
The examples in the following sections use plain JavaScript to generate unstyled HTML elements.
The real code for generating the proper links and the navigation elements will be written in your web application framework. Your code will use the same REST API requests and responses shown in the examples.
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 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 render 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 menu items and the 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 backend 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 implementation 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 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 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 backend 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 in the preceding sections, 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 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 mentioned in the anchor tag for the name column in the above example is a placeholder. It represents the code required to that type of ThoughtSpot content in the web application. The actual design you choose for your application will determine the code needed 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)
});
}