Callback custom actions

Callback custom actions

Callback custom actions allow you to get data payloads from an embedded ThoughtSpot object and initiate a callback to the host application. For example, if you have embedded a ThoughtSpot Liveboard in your application, you can add a callback action to the Liveboard menu to get a JSON response payload from the Liveboard visualization and initiate a callback to your app.

Feature highlights

  • Callback custom actions are supported on embedded ThoughtSpot instances only and require a ThoughtSpot Embedded Edition license.

  • Users with developer or admin privileges can create a callback custom action in the Developer portal.

  • Developers can set a callback action as a global or local action.

  • To access callback actions, users must have the New Answer Experience enabled on their application instance.

  • Users with edit permissions to a Worksheet or visualization can add a local callback action to a visualization or Answer of their choice.

  • Developers must register the callback in the Visual Embed SDK and define data classes and functions to parse and process the JSON data payload retrieved from the callback event.

Create a callback custom action🔗

Before you begin, make sure you have the developer privileges to add a custom action in the Developer portal.

  1. Go to Develop > Customizations > Custom actions.

  2. Click Create action.

  3. Add a label for the custom action. For example, Send Data.

  4. Select the Callback option.

  5. Note the callback action ID.

    The callback ID is used as a unique reference in the Visual Embed SDK to handle the callback event. You can also use this ID to disable, show, or hide the callback action in the embedded app.

    By default, custom actions are added to all Liveboard visualizations and saved Answers and set as Global. If you want to add this action only to specific visualizations, clear the On by default on all visualizations checkbox.

  6. To restrict action availability to specific user groups, click Show advanced availability settings, and select the groups.

  7. Click Create action.

    The custom action is added to the Actions dashboard in the Developer portal.

  8. To view and verify the custom action you just created, navigate to a visualization page.

Important

By default, the global actions are added as a menu action in the More menu the more options menu on all visualizations and saved answers. The local custom actions are not added as a clickable action on any Answer page or visualization. You can assign a local action to a specific visualization or saved Answer, or all new answers built from a specific worksheet and add it to a menu component.

Register the callback using Visual Embed SDK🔗

After a callback custom action is added in ThoughtSpot, add an event handler to listen to the callback event and trigger a data payload as a response when a user clicks the callback action in the UI.

In this example, the data payload from the custom action response is logged in the console.

Example code for Answer payloads

searchEmbed.on(EmbedEvent.CustomAction, payload => {
    const data = payload.data;
    if (data.id === 'show-data') {
        console.log('Custom Action event:', data.embedAnswerData);
    }
})

Example code to get underlying data using AnswerService class

Use the AnswerService class to run GraphQL queries in the context of the Answer on which the custom action is triggered.

 searchEmbed.on(EmbedEvent.CustomAction, async e => {
    const underlying = await e.answerService.getUnderlyingDataForPoint([
      'col name 1'
    ]);
    const data = await underlying.fetchData(0, 100);
 })

Example code for Liveboard payload (Classic experience mode)

liveboardEmbed.on(EmbedEvent.CustomAction, payload => {
    if (payload.id === 'show-data') {
        console.log('Custom Action event:', payload.data);
    }
})

Example code for Liveboard data payload (New experience mode)

liveboardEmbed.on(EmbedEvent.CustomAction, payload => {
    const customActionId = 'show-data';
    if (payload.data.id === customActionId) {
        console.log('Custom Action event:', payload.data);
    }
})

Example code to fetch large datasets in batches

const data = payload.data;
if (data.id === 'show-data') {
    const fetchAnswerData = await payload.answerService.fetchData(1, 5);
    //where the first integer is the offset value and the second integer is batchsize
    console.log('fetchAnswerData:::', fetchAnswerData);
}

Process large datasets in batches🔗

If your application instance requires callback custom actions to retrieve large data sets in answer payload, define a method to process data in batches and paginate the response.

.on(EmbedEvent.CustomAction, async(payload: any) => {
	const data = payload.data;
	if(data.id === 'show-data') {
		const fetchAnswerData = await payload.answerService.fetchData(offset, batchSize);
		console.log('fetchAnswerData:::', fetchAnswerData);
	}
})
offset

Integer. Sets the starting point for the records retrieved from the search answer. For example, if you want to retrieve the initial set of records in 3 batches, set the value to 0.

batchSize

Integer. Divides the data set into batches. For example, if you specify the batchSize as 3, the three batches are processed independently for faster response.

If the dataset is divided into 10 batches, and the offset value is 0 and the batchSize is 3, the first three batches of the dataset are retrieved in the answer payload in one UI query. Similarly, if the offset value is 1 and the batchSize is 3, the next set of batches (4,5, and 6) are retrieved.

Parse JSON response payload🔗

The callback actions can return JSON payloads that are complex and need to be parsed before using it for application needs. The format of the JSON response payload can vary based on the type of the embedded object and the placement of the custom action in the menu. For example, the format of the data payload triggered by an action on a Liveboard visualization is different from the data retrieved for an Answer. When defining functions in your code to parse and handle data, make sure you use the correct classes.

Define functions and classes to handle Liveboard data🔗

The following example shows how to get data from a callback action triggered on a Liveboard visualization:

const onCustomAction = () => {
        const embed = new LiveboardEmbed("#embed", {
            liveboardId: "e40c0727-01e6-49db-bb2f-5aa19661477b",
            vizId: "8d2e93ad-cae8-4c8e-a364-e7966a69a41e",
        });
        embed.on(EmbedEvent.CustomAction, payload => {
                if (payload.id === "show-data" || "payload.data.id === show-data") {
                    showData(payload)
                }
            })
            .render();
        const showData = (payload) => {
            const liveboardActionData = LiveboardActionData.createFromJSON(payload);
            const dataElement = document.getElementById('show-data');
            dataElement.style.display = 'block';
        }

The format of the data payload for Liveboard visualization varies if the callback action is triggered from a Liveboard in the new experience mode. To view the payload structure, refer to the examples on Custom action response payload page.

The following code sample shows sample classes and functions for parsing JSON data from a Liveboard. This example assumes that the callback action is placed in the More options menu of the Liveboard visualization.

/**
 * This class handles data from Liveboard visualizations if the callback action is set as
 * a More menu or primary action.
 * It does not work for Search or saved Answer data payloads or callback actions in the context menus.
 */
class LiveboardActionData {
  /**
   * Creates a new LiveboardContextActionData  from the payload.
   * @param jsonData  A string from payload.data
   * @returns {LiveboardContextActionData}
   */
    static createFromJSON(jsonData) {
        let isV1 = true;
        // Handle data structure differences between Liveboards operating in the classic and new experience modes.
        if (typeof jsonData.data === 'string' || jsonData.data instanceof String) {
            jsonData = JSON.parse(jsonData.data);
            isV1 = true;
        } else {
            jsonData = jsonData.data;
            isV1 = false;
        }
        const liveboardActionData = new LiveboardActionData(jsonData);
        try {
            const columnNames = [];
            const data = [];

            if (isV1) {
                const reportBookData = getValues(jsonData.reportBookData)[0]; // assume there's only one.
                const vizData = getValues(reportBookData.vizData)[0]; // assume there's only one.

                // Get the column meta information.
                const columns = vizData.dataSets.PINBOARD_VIZ.columns;
                const nbrCols = columns.length;
                for (let colCnt = 0; colCnt < nbrCols; colCnt += 1) {
                    columnNames.push(columns[colCnt].column.name);
                }
                // can come in two flavors, so need to get the right data
                const dataSet = (Array.isArray(vizData.dataSets.PINBOARD_VIZ.data)) ?
                    vizData.dataSets.PINBOARD_VIZ.data[0].columnDataLite :
                    vizData.dataSets.PINBOARD_VIZ.data.columnDataLite;

                for (let cnt = 0; cnt < columnNames.length; cnt++) {
                    data.push(dataSet[cnt].dataValue); // should be an array of columns values.
                }
            } else { // is v2
                const columns = jsonData.embedAnswerData.columns;
                const nbrCols = columns.length;
                for (let colCnt = 0; colCnt < nbrCols; colCnt++) {
                    columnNames.push(columns[colCnt].column.name);
                }
                for (let colCnt = 0; colCnt < nbrCols; colCnt++) {
                    // The data is always under 0 for what we want.
                    data.push(jsonData.embedAnswerData.data[0].columnDataLite[colCnt].dataValue);
                }
            }
            liveboardActionData.columnNames = columnNames;
        } catch (error) {
            console.error(`Error creating Liveboard action data: ${error}`);
            console.error(jsonData);
        }
        return liveboardActionData;
    }
}

The above example uses additional classes and functions to parse and get data in the tabular format, the number of columns, rows, and column names. These classes are defined in the code sample in dataclasses.js on ThoughtSpot Embedded Resources GitHub repository. You can also find sample classes and functions to parse JSON payload from context menu actions in dataclasses.js.

Define functions and classes to handle Answer data🔗

The following example shows the code sample to get Answer data from the show-data callback action and sample classes and functions to parse the JSON response payload.

const showData = (payload) => {
    const data = payload.data;
    if (data.id === 'show-data') {
        // Display the data as a table.
        const actionData = ActionData.createFromJSON(payload);
        const html = actionDataToHTML(actionData);
        const dataContentElement = document.getElementById('modal-data-content');
        dataContentElement.innerHTML = html;
        const dataElement = document.getElementById('show-data');
        dataElement.style.display = 'block';
    } else {
        console.log(`Got unknown custom actions ${data.id}`);
    }
}

The following example shows the sample classes and functions for handling custom action data:

//This function converts column-based representations of the data to a table for display
const zip = (arrays) => {
    return arrays[0].map(function(_, i) {
        return arrays.map(function(array) {
            return array[i]
        })
    });
}
/**
 * This class handles data from Search and Answers if the callback action is set as a More menu or primary action.
 * It does not work for Liveboard visualizations or callback actions in the context menu.
 */
class ActionData {
    // Wrapper for the data sent when a custom action is triggered.
    constructor() {
        this._columnNames = []; // list of the columns in order.
        this._data = {}; // data is stored and indexed by column with the index being column name.
    }
    get nbrRows() {
        // Returns the number of rows.  Assumes all columns are of the same length.
        if (this._columnNames && Object.keys(this._data)) { // make sure there is some data.
            return this._data[this._columnNames[0]]?.length;
        }
        return 0;
    }
    get nbrColumns() {
        // Returns the number of columns.
        return this._columnNames.length;
    }
    static createFromJSON(jsonData) {
        // Creates a new ActionData object from JSON.
        const actionData = new ActionData();
        // Gets the column names.
        const nbrCols = jsonData.data.embedAnswerData.columns.length;
        for (let colCnt = 0; colCnt < nbrCols; colCnt += 1) {
            actionData._columnNames.push(jsonData.data.embedAnswerData.columns[colCnt].column.name);
        }
        let dataSet;
        dataSet = (Array.isArray(jsonData.data.embedAnswerData.data)) ?
            jsonData.data.embedAnswerData.data[0].columnDataLite :
            jsonData.data.embedAnswerData.data.columnDataLite;
        for (let colCnt = 0; colCnt < actionData.nbrColumns; colCnt++) {
            actionData._data[actionData._columnNames[colCnt]] = Array.from(dataSet[colCnt].dataValue); // shallow copy the data
        }
        return actionData
    }
    getDataAsTable() {
        // returns the data as a table.  The columns will be in the same order as the column headers.
        const arrays = []
        for (const cname of this._columnNames) {
            arrays.push(this._data[cname])
        }
        return zip(arrays); // returns a two dimensional data array
    }
}
const actionDataToHTML = (actionData) => {
    // Converts an ActionData data to an HTML table.
    let table = '<table class="tabular-data">';
    // Add a header
    table += '<tr>';
    for (const columnName of actionData._columnNames) {
        table += `<th class="tabular-data-th">${columnName}</th>`;
    }
    table += '</tr>';
    const data = actionData.getDataAsTable();
    for (let rnbr = 0; rnbr < actionData.nbrRows; rnbr++) {
        table += '<tr>';
        for (let cnbr = 0; cnbr < actionData.nbrColumns; cnbr++) {
            table += `<td class="tabular-data">${data[rnbr][cnbr]}</td>`;
        }
        table += '</tr>';
    }
    table += '</table>';
    return table;
}
export {
    ActionData,
    actionDataToHTML
}

Initiate a callback and test your implementation🔗

To initiate a callback action:

  1. Navigate to a Liveboard visualization or saved Answer page.

    Custom actions appear as disabled on unsaved charts and tables. If you have generated a chart or table from a new search query, you must save the Answer before associating a custom action.

  2. Click the callback action.

  3. Verify if the callback action triggers a payload and initiates a callback to the host app.