const answerRequestObject = {
"metadata": [{ "type": "ANSWER" }],
"permissions": [
{
"principal": {
"type": "USER",
"identifier": "{username}"
},
"share_mode": "READ_ONLY"
}
],
"record_offset": 0,
"record_size": 10000
};
const response = await fetch('/api/rest/2.0/metadata/search', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(answerRequestObject),
credentials: 'include'
});
// V2.0 returns a top-level array directly
const contentList = await response.json();
Custom menu and navigation elements
If you want to embed specific pages of the ThoughtSpot application, you can embed individual components such as LiveboardEmbed, SpotterEmbed, or SearchEmbed rather than full application embedding. The full application embed is a relatively fixed and unified application shell, while individual component embeds offer more granular and isolated control.
If you choose to embed individual components with specific pages, your embedding app may require custom menus, navigation elements with routes for pages and ThoughtSpot REST API integration to programmatically manage interaction and navigation between these pages.
|
Note
|
Mixing full application embedding with other embed components, such as |
See the code example on GitHub for a complete implementation using the V2.0 REST API to power navigation components in an embedded application.
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 drop-down 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 are unique on every Org or instance, except for separate instances that are intentionally replicated with the same GUIDs via TML import and export.
You can use the /api/rest/2.0/metadata/search V2.0 REST API endpoint to retrieve the equivalent object IDs for content on various Orgs and environments when building your hardcoded menu system. Alternatively, you can maintain a mapping file of equivalent objects during your CI/CD deployment process to generate the hardcoded URLs in the menu system.
Dynamic requests for content listings via REST APIπ
The /api/rest/2.0/metadata/search V2.0 REST API endpoint retrieves filtered lists of the content a user has access to.
The endpoint can be used by administrators to retrieve content for any given user, or called as the signed-in user directly from the web browser after signing in via SSO.
The response to /api/rest/2.0/metadata/search can be parsed and filtered on tags, author, or other properties. You can also make multiple calls to the endpoint with different filtering arguments in each request. For example, favorite options can be specified to retrieve only objects marked as favorites by a user.
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 uses a ThoughtSpot service account with administrator privileges on the backend, this approach may be simpler than managing individual user access tokens for each REST API request.
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,
"record_size": 10000
};
const response = await fetch('/api/rest/2.0/metadata/search', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(lbRequestObject),
credentials: 'include'
});
// V2.0 returns a top-level array directly
const contentList = await response.json();
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 /api/rest/2.0/metadata/search is a top-level array. Each item in the array exposes metadata_name, metadata_id, and metadata_type at the top level β enough to build a simple navigation list or sidebar menu. For richer rendering (descriptions, tags, modified date, author), use the nested metadata_header sub-object within each item.
The following properties within metadata_header are useful for building navigation UI:
-
idβ Object GUID (matchesmetadata_idat the top level) -
nameβ Object display name (matchesmetadata_nameat the top level) -
descriptionβ Text description added by the creator -
tagsβ Array of tag objects; each has anameproperty -
modifiedβ Last edit timestamp (milliseconds since epoch) -
authorDisplayNameβ Display name of the object owner
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, which is loaded via the SearchEmbed component. The saved Answer always displays the ThoughtSpot search bar and UI actions for editing, whereas a visualization displays fewer UI elements and shows the More menu for chart-level actions.
To retrieve the visualization details for a Liveboard in the same API call, set the include_visualization_headers parameter to true in your request (see undefined).
Replicating the ThoughtSpot Liveboards or Answers pageπ
The /api/rest/2.0/metadata/search endpoint provides the metadata required to replicate the Liveboards or Answers pages from the ThoughtSpot UI within your embedding application.
The following metadata.type values are supported:
-
LIVEBOARDβ Liveboards -
ANSWERβ Saved Answers -
LOGICAL_TABLEβ Data objects (Tables, Models, and Views)
For LOGICAL_TABLE objects, you can add a subtype filter to narrow results:
-
ONE_TO_ONE_LOGICALβ Tables -
WORKSHEETβ Models -
AGGR_WORKSHEETβ Views
The following example fetches all three content types, caches the responses, and allows switching between them with a content-type selector β matching the pattern used in the reference implementation:
// Generic V2.0 REST API wrapper β equivalent to restApiCallV2() in the reference example
async function restApiCallV2(endpoint, httpVerb, apiRequestObj) {
const baseUrl = `${tsHost}/api/rest/2.0/`;
const apiFullEndpoint = baseUrl + endpoint;
const fetchArgs = {
method: httpVerb.toUpperCase(),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
credentials: 'include'
};
if (apiRequestObj !== null) {
fetchArgs['body'] = JSON.stringify(apiRequestObj);
}
const response = await fetch(apiFullEndpoint, fetchArgs);
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`);
}
return response.status === 204 ? true : response.json(); }
async function callMetadataSearchApi(searchRequestObject) { return await restApiCallV2('metadata/search', 'POST', searchRequestObject); }
const lbRequestObject = { "metadata": [{ "type": "LIVEBOARD" }], "record_offset": 0, "record_size": 10000 };
const answerRequestObject = { "metadata": [{ "type": "ANSWER" }], "record_offset": 0, "record_size": 10000 };
const tableRequestObject = { "metadata": [{ "type": "LOGICAL_TABLE" }], "record_offset": 0, "record_size": 10000 };
const [lbResponse, answerResponse, tableResponse] = await Promise.all([ callMetadataSearchApi(lbRequestObject), callMetadataSearchApi(answerRequestObject), callMetadataSearchApi(tableRequestObject) ]);
const metadataResponses = { 'lb': lbResponse, 'answer': answerResponse, 'table': tableResponse };
=== Rendering Liveboards or Answers pages similar to the ThoughtSpot UI To render a table similar to the Answers or Liveboards page in the ThoughtSpot UI, use the `tableFromMetadataSearch()` function pattern from the reference implementation. This function reads `metadata_header` sub-object fields (`id`, `name`, `description`, `tags`, `modified`, `authorDisplayName`) from each item in the V2.0 response array to populate the table columns: [source,javascript]
function tableFromMetadataSearch(searchResponse) { const table = document.createElement('table');
// Table headers
const thead = table.createTHead();
const headerRow = thead.insertRow();
['Name', 'Description', 'Tags', 'Modified', 'Author'].forEach(label => {
const th = document.createElement('th');
th.innerText = label;
headerRow.appendChild(th);
});
// Table rows β V2.0 response is a top-level array
searchResponse.forEach(item => {
// Rich detail fields are inside metadata_header
const headers = item['metadata_header'];
const id = headers['id'];
const name = headers['name'];
const desc = headers['description'];
const tags = headers['tags']; // Array of { name: string } objects
const modified = headers['modified']; // Milliseconds since epoch
const authorDisplayName = headers['authorDisplayName'];
const row = table.insertRow();
// Name column β link triggers loadContent() with the object GUID
const nameCell = row.insertCell();
nameCell.innerHTML =
`<a href="#" onclick="loadContent({type:'liveboard', guid:'${id}'})">${name}</a>`;
// Description column
const descCell = row.insertCell();
if (desc !== null) { descCell.innerText = desc; }
// Tags column β tags is an array of tag objects
const tagsCell = row.insertCell();
if (tags.length > 0) {
tagsCell.innerText = tags.map(t => t['name']).join(', ');
}
// Modified date column const modifiedCell = row.insertCell(); modifiedCell.innerText = new Date(modified).toUTCString();
// Author column
const authorCell = row.insertCell();
authorCell.innerText = authorDisplayName;
});
return table; }
const table = tableFromMetadataSearch(metadataResponses['lb']); document.getElementById('main-content-div').appendChild(table);
To render a trellis of tiles split by tag, or a sidebar menu, use the `drawTrellisOfTiles()` and `drawSidebarMenu()` functions. The sidebar functions use the top-level `metadata_name` and `metadata_id` fields (not `metadata_header`) for simple list rendering: [source,javascript]
function drawSidebarMenu(metadataResponses) { const lbs = metadataResponses['lb']; const menuUl = document.getElementById('liveboards-menu');
lbs.forEach(item => {
const li = document.createElement('li');
// Top-level fields for sidebar display and navigation
li.innerText = item['metadata_name'];
li.onclick = () => loadContent({ type: 'liveboard', guid: item['metadata_id'] });
menuUl.appendChild(li);
});
}
[NOTE] ==== `metadata_name` and `metadata_id` at the top level are equivalent to `metadata_header.name` and `metadata_header.id` inside the nested sub-object. Use the top-level fields for simple list or sidebar rendering; use the `metadata_header` sub-object when you need additional properties such as `description`, `tags`, `modified`, or `authorDisplayName`. ==== [[retrieve-viz-headers]] === Retrieve individual visualizations from a Liveboard To retrieve the list of visualizations on a specific Liveboard, use the `/api/rest/2.0/metadata/search` endpoint with the `include_visualization_headers` parameter set to `true`. Visualization headers are returned inline in the same response, no separate API call is required. [NOTE] ==== The V1 REST API endpoint `GET /tspublic/v1/metadata/listvizheaders` is deprecated. Use `/api/rest/2.0/metadata/search` with `"include_visualization_headers": true` for all new implementations. ==== [source,javascript]
async function fetchLiveboardVizHeaders(liveboardGuid) { const requestBody = { "metadata": [ { "type": "LIVEBOARD", "identifier": liveboardGuid } tr.append(tags_td);
const response = await fetch('/api/rest/2.0/metadata/search', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody),
credentials: 'include'
});
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`);
}
// V2.0 returns a top-level array; visualization_headers is per Liveboard item
const results = await response.json();
return results[0]?.visualization_headers ?? [];
}
const vizHeaders = await fetchLiveboardVizHeaders('{liveboard-GUID}');
vizHeaders.forEach(viz β {
// Use viz.id with LiveboardEmbed to load individual visualizations
console.log(Viz ID: ${viz.id}, Name: ${viz.name});
// new LiveboardEmbed('#embed', { liveboardId: '{liveboard-GUID}', vizId: viz.id })
});
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: [source,javascript]
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 xref:metadata-api.adoc#viz-header[get visualization headers REST API endpoint]. [source,javascript]
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)
});
}