Skip to content

Latest commit

 

History

History
350 lines (294 loc) · 14.2 KB

File metadata and controls

350 lines (294 loc) · 14.2 KB

Callback custom actions

Overview

Callback custom actions act fire off an EmbedEvent when the menu item or button linked to the action is clicked by a user.

Note

Callback custom actions only appear in the menu system when ThoughtSpot is embedded using the Visual Embed SDK. Please go the Develop tab and use the Visual Embed SDK Developer Playground to see callback custom actions within ThoughtSpot, or use your embedded application.

The embedded app will define a listener for all EmbedEvent.CustomAction events, with a number of if() statements evaluating the id property of the response to determine which callback action has been selected by the user.

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

The response payload contains metadata about the callback action, the content where the custom action was triggered from, as well as the data from the table or visualization.

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.

Define event listener using Visual Embed SDK

In your Visual Embed SDK code, add an .on() listener for the EmbedEvent.CustomAction event, with blocks parsing the id property of the response to define different behaviors for different callback customer actions.

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

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
}

Trigger a callback custom action 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.

Additional resources