# Custom menu and navigation elements

> For the complete documentation index, see [llms.txt](https://developers.thoughtspot.com/docs/llms.txt)

Source: https://developers.thoughtspot.com/docs/in-app-navigation

# 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]({{navprefix}}/{{customize-links}}) 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 SearchEmbed, SpotterEmbed, and LiveboardEmbed, is not recommended, because each embed method is designed to manage its own navigation, layout, and application state. Combining them can create complicated workflows and make it harder to maintain and customize your embedded application.

See the [code example on GitHub](https://github.com/thoughtspot/ts_everywhere_resources/blob/master/examples/menus_and_other_navigation_components/rest_api_ui_navigation_v2.html) 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`]({{navprefix}}/{{rest-api-v2-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`]({{navprefix}}/{{rest-api-v2-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]({{navprefix}}/{{rest-api-v2-metadata-search}}#_favorite_object_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:

1.  Request the user’s available content via the REST API
    
2.  (Optional) Cache the results within the embedding application
    
3.  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]({{navprefix}}/{{rest-api-v2-metadata-search}}#permissions) 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.

```javascript
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();
```

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`.

```javascript
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]({{navprefix}}/{{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 (matches `metadata_id` at the top level)
    
-   `name` — Object display name (matches `metadata_name` at the top level)
    
-   `description` — Text description added by the creator
    
-   `tags` — Array of tag objects; each has a `name` property
    
-   `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](#retrieve-viz-headers)).

## 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](https://github.com/thoughtspot/ts_everywhere_resources/blob/master/examples/menus_and_other_navigation_components/rest_api_ui_navigation_v2.html):

```javascript
// 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)
    });
}