@thoughtspot/visual-embed-sdk
Version:
ThoughtSpot Embed SDK
190 lines • 9.62 kB
JavaScript
import { HostEvent } from '../../types';
import { processTrigger as processTriggerService } from '../../utils/processTrigger';
import { getEmbedConfig } from '../embedConfig';
import { isValidUpdateFiltersPayload, isValidDrillDownPayload, throwUpdateFiltersValidationError, throwDrillDownValidationError, } from './utils';
import { UIPassthroughEvent, } from './contracts';
/**
* Maps HostEvent to its corresponding UIPassthroughEvent.
* Includes both custom-handler events (Pin, SaveAnswer, UpdateFilters, DrillDown)
* and getter events (GetAnswerSession, GetFilters, etc.) that use getDataWithPassthroughFallback.
*/
const PASSTHROUGH_MAP = {
// Custom handlers (setters with special logic)
[HostEvent.Pin]: UIPassthroughEvent.PinAnswerToLiveboard,
[HostEvent.SaveAnswer]: UIPassthroughEvent.SaveAnswer,
[HostEvent.UpdateFilters]: UIPassthroughEvent.UpdateFilters,
[HostEvent.DrillDown]: UIPassthroughEvent.Drilldown,
// Getters (use getDataWithPassthroughFallback)
[HostEvent.GetAnswerSession]: UIPassthroughEvent.GetAnswerSession,
[HostEvent.GetFilters]: UIPassthroughEvent.GetFilters,
[HostEvent.GetIframeUrl]: UIPassthroughEvent.GetIframeUrl,
[HostEvent.GetParameters]: UIPassthroughEvent.GetParameters,
[HostEvent.GetTML]: UIPassthroughEvent.GetTML,
[HostEvent.GetTabs]: UIPassthroughEvent.GetTabs,
[HostEvent.getExportRequestForCurrentPinboard]: UIPassthroughEvent.GetExportRequestForCurrentPinboard,
};
export class HostEventClient {
constructor(iFrame) {
/** Cached list of available UI passthrough keys from the embedded app */
this.availablePassthroughKeysCache = null;
this.iFrame = iFrame;
this.customHandlers = {
[HostEvent.Pin]: (p, c) => this.handlePinEvent(p, c),
[HostEvent.SaveAnswer]: (p, c) => this.handleSaveAnswerEvent(p, c),
[HostEvent.UpdateFilters]: (p, c) => this.handleUpdateFiltersEvent(p, c),
[HostEvent.DrillDown]: (p, c) => this.handleDrillDownEvent(p, c),
};
}
/**
* A wrapper over process trigger to
* @param {HostEvent} message Host event to send
* @param {any} data Data to send with the host event
* @returns {Promise<any>} - the response from the process trigger
*/
async processTrigger(message, data, context) {
if (!this.iFrame) {
throw new Error('Iframe element is not set');
}
const thoughtspotHost = getEmbedConfig().thoughtSpotHost;
return processTriggerService(this.iFrame, message, thoughtspotHost, data, context);
}
async handleHostEventWithParam(apiName, parameters, context) {
var _a, _b, _c, _d;
const response = (_b = (_a = (await this.triggerUIPassthroughApi(apiName, parameters, context))) === null || _a === void 0 ? void 0 : _a.find) === null || _b === void 0 ? void 0 : _b.call(_a, (r) => r.error || r.value);
if (!response) {
const error = `No answer found${parameters.vizId ? ` for vizId: ${parameters.vizId}` : ''}.`;
throw { error };
}
const errors = response.error
|| ((_c = response.value) === null || _c === void 0 ? void 0 : _c.errors)
|| ((_d = response.value) === null || _d === void 0 ? void 0 : _d.error);
if (errors) {
const message = typeof errors === 'string' ? errors : JSON.stringify(errors);
throw { error: message };
}
return { ...response.value };
}
async hostEventFallback(hostEvent, data, context) {
return this.processTrigger(hostEvent, data, context);
}
/**
* For getter events that return data. Tries UI passthrough first;
* if the app doesn't support it (no response data), falls back to
* the legacy host event channel. Real errors are thrown as-is.
*/
async getDataWithPassthroughFallback(passthroughEvent, hostEvent, payload, context) {
var _a, _b, _c;
const response = await this.triggerUIPassthroughApi(passthroughEvent, payload || {}, context);
const matched = (_a = response === null || response === void 0 ? void 0 : response.find) === null || _a === void 0 ? void 0 : _a.call(response, (r) => r.error || r.value);
if (!matched) {
return this.hostEventFallback(hostEvent, payload, context);
}
const errors = matched.error
|| ((_b = matched.value) === null || _b === void 0 ? void 0 : _b.errors)
|| ((_c = matched.value) === null || _c === void 0 ? void 0 : _c.error);
if (errors) {
const message = typeof errors === 'string' ? errors : JSON.stringify(errors);
throw new Error(message);
}
return { ...matched.value };
}
/**
* Setter for the iframe element used for host events
* @param {HTMLIFrameElement} iFrame - the iframe element to set
*/
setIframeElement(iFrame) {
this.iFrame = iFrame;
}
/**
* Fetches the list of available UI passthrough keys from the embedded app.
* Result is cached for the session. Returns empty array on failure.
*/
async getAvailableUIPassthroughKeys(context) {
var _a, _b;
if (this.availablePassthroughKeysCache !== null) {
return this.availablePassthroughKeysCache;
}
try {
const response = await this.triggerUIPassthroughApi(UIPassthroughEvent.GetAvailableUIPassthroughs, {}, context);
const matched = (_a = response === null || response === void 0 ? void 0 : response.find) === null || _a === void 0 ? void 0 : _a.call(response, (r) => r.value && !r.error);
const keys = (_b = matched === null || matched === void 0 ? void 0 : matched.value) === null || _b === void 0 ? void 0 : _b.keys;
this.availablePassthroughKeysCache = Array.isArray(keys) ? keys : [];
return this.availablePassthroughKeysCache;
}
catch {
return [];
}
}
async triggerUIPassthroughApi(apiName, parameters, context) {
const res = await this.processTrigger(HostEvent.UIPassthrough, {
type: apiName,
parameters,
}, context);
return res;
}
async handlePinEvent(payload, context) {
var _a, _b;
if (!payload || !('newVizName' in payload)) {
return this.hostEventFallback(HostEvent.Pin, payload, context);
}
const formattedPayload = {
...payload,
pinboardId: (_a = payload.liveboardId) !== null && _a !== void 0 ? _a : payload.pinboardId,
newPinboardName: (_b = payload.newLiveboardName) !== null && _b !== void 0 ? _b : payload.newPinboardName,
};
const data = await this.handleHostEventWithParam(UIPassthroughEvent.PinAnswerToLiveboard, formattedPayload, context);
return {
...data,
liveboardId: data.pinboardId,
};
}
async handleSaveAnswerEvent(payload, context) {
var _a, _b, _c, _d;
if (!payload || !('name' in payload) || !('description' in payload)) {
// Save is the fallback for SaveAnswer
return this.hostEventFallback(HostEvent.Save, payload, context);
}
const data = await this.handleHostEventWithParam(UIPassthroughEvent.SaveAnswer, payload, context);
return {
...data,
answerId: (_d = (_c = (_b = (_a = data === null || data === void 0 ? void 0 : data.saveResponse) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.Answer__save) === null || _c === void 0 ? void 0 : _c.answer) === null || _d === void 0 ? void 0 : _d.id,
};
}
handleUpdateFiltersEvent(payload, context) {
if (!isValidUpdateFiltersPayload(payload)) {
throwUpdateFiltersValidationError();
}
return this.handleHostEventWithParam(UIPassthroughEvent.UpdateFilters, payload, context);
}
handleDrillDownEvent(payload, context) {
if (!isValidDrillDownPayload(payload)) {
throwDrillDownValidationError();
}
return this.handleHostEventWithParam(UIPassthroughEvent.Drilldown, payload, context);
}
/**
* Dispatches a host event using the appropriate channel:
* 1. If the embedded app supports UI passthrough for this event, use it (custom handler or getter).
* 2. Otherwise fall back to the legacy host event channel.
*
* @param hostEvent - The host event to trigger
* @param payload - Optional payload for the event
* @param context - Optional context (e.g. vizId) for scoped operations
*/
async triggerHostEvent(hostEvent, payload, context) {
const customHandler = this.customHandlers[hostEvent];
const passthroughEvent = PASSTHROUGH_MAP[hostEvent];
// If embedded app supports passthrough but not this event, use legacy channel
const keys = passthroughEvent ? await this.getAvailableUIPassthroughKeys(context) : [];
if (passthroughEvent && keys.length > 0 && !keys.includes(passthroughEvent)) {
return this.hostEventFallback(hostEvent, payload, context);
}
// Custom handler (setters) > getter passthrough > legacy fallback
return (customHandler
? customHandler(payload, context)
: passthroughEvent
? this.getDataWithPassthroughFallback(passthroughEvent, hostEvent, payload, context)
: this.hostEventFallback(hostEvent, payload, context));
}
}
//# sourceMappingURL=host-event-client.js.map