ashish-sdk
Version:
ThoughtSpot Embed SDK
457 lines • 17.7 kB
JavaScript
/**
* Copyright (c) 2022
*
* Base classes
*
* @summary Base classes
* @author Ayon Ghosh <ayon.ghosh@thoughtspot.com>
*/
import { getEncodedQueryParamsString, getCssDimension, getOffsetTop, } from '../utils';
import { getThoughtSpotHost, URL_MAX_LENGTH, DEFAULT_EMBED_WIDTH, DEFAULT_EMBED_HEIGHT, getV2BasePath, } from '../config';
import { EmbedEvent, Param, } from '../types';
import { uploadMixpanelEvent, MIXPANEL_EVENT } from '../mixpanel-service';
import { getProcessData } from '../utils/processData';
import { processTrigger } from '../utils/processTrigger';
// eslint-disable-next-line import/no-cycle
import pkgInfo from '../../package.json';
import { getAuthPromise, getEmbedConfig, renderInQueue } from './base';
const { version } = pkgInfo;
/**
* The event id map from v2 event names to v1 event id
* v1 events are the classic embed events implemented in Blink v1
* We cannot rename v1 event types to maintain backward compatibility
* @internal
*/
const V1EventMap = {
[EmbedEvent.Data]: [EmbedEvent.V1Data],
};
/**
* Base class for embedding v2 experience
* Note: the v2 version of ThoughtSpot Blink is built on the new stack:
* React+GraphQL
*/
export class TsEmbed {
constructor(domSelector, viewConfig) {
/**
* Should we encode URL Query Params using base64 encoding which thoughtspot
* will generate for embedding. This provides additional security to
* thoughtspot clusters against Cross site scripting attacks.
* @default false
*/
this.shouldEncodeUrlQueryParams = false;
this.el = this.getDOMNode(domSelector);
// TODO: handle error
this.embedConfig = getEmbedConfig();
this.thoughtSpotHost = getThoughtSpotHost(this.embedConfig);
this.thoughtSpotV2Base = getV2BasePath(this.embedConfig);
this.eventHandlerMap = new Map();
this.isError = false;
this.viewConfig = viewConfig;
this.shouldEncodeUrlQueryParams = this.embedConfig.shouldEncodeUrlQueryParams;
this.registerPlugins(viewConfig === null || viewConfig === void 0 ? void 0 : viewConfig.plugins);
if (!this.embedConfig.suppressNoCookieAccessAlert) {
this.on(EmbedEvent.NoCookieAccess, () => {
// eslint-disable-next-line no-alert
alert('Third party cookie access is blocked on this browser, please allow third party cookies for ThoughtSpot to work properly');
});
}
}
registerPlugins(plugins) {
if (!plugins) {
return;
}
plugins.forEach((plugin) => {
Object.keys(plugin.handlers).forEach((eventName) => {
const listener = plugin.handlers[eventName];
this.on(eventName, listener);
});
});
}
/**
* Gets a reference to the root DOM node where
* the embedded content will appear.
* @param domSelector
*/
getDOMNode(domSelector) {
return typeof domSelector === 'string'
? document.querySelector(domSelector)
: domSelector;
}
/**
* Throws error encountered during initialization.
*/
throwInitError() {
this.handleError('You need to init the ThoughtSpot SDK module first');
}
/**
* Handles errors within the SDK
* @param error The error message or object
*/
handleError(error) {
this.isError = true;
this.executeCallbacks(EmbedEvent.Error, {
error,
});
// Log error
console.log(error);
}
/**
* Extracts the type field from the event payload
* @param event The window message event
*/
getEventType(event) {
var _a, _b;
// eslint-disable-next-line no-underscore-dangle
return ((_a = event.data) === null || _a === void 0 ? void 0 : _a.type) || ((_b = event.data) === null || _b === void 0 ? void 0 : _b.__type);
}
/**
* Extracts the port field from the event payload
* @param event The window message event
* @returns
*/
getEventPort(event) {
if (event.ports.length && event.ports[0]) {
return event.ports[0];
}
return null;
}
/**
* fix for ts7.sep.cl
* will be removed for ts7.oct.cl
* @hidden
*/
formatEventData(event) {
const eventData = {
...event.data,
};
if (!eventData.data) {
eventData.data = event.data.payload;
}
return eventData;
}
/**
* Adds a global event listener to window for "message" events.
* ThoughtSpot detects if a particular event is targeted to this
* embed instance through an identifier contained in the payload,
* and executes the registered callbacks accordingly.
*/
subscribeToEvents() {
window.addEventListener('message', (event) => {
const eventType = this.getEventType(event);
const eventPort = this.getEventPort(event);
const eventData = this.formatEventData(event);
if (event.source === this.iFrame.contentWindow) {
this.executeCallbacks(eventType, getProcessData(eventType, eventData, this.thoughtSpotHost), eventPort);
}
});
}
/**
* Constructs the base URL string to load the ThoughtSpot app.
*/
getEmbedBasePath(query) {
let queryString = query;
if (this.shouldEncodeUrlQueryParams) {
queryString = `?base64UrlEncodedFlags=${getEncodedQueryParamsString(queryString.substr(1))}`;
}
const basePath = [
this.thoughtSpotHost,
this.thoughtSpotV2Base,
queryString,
]
.filter((x) => x.length > 0)
.join('/');
return `${basePath}#/embed`;
}
/**
* Common query params set for all the embed modes.
* @returns queryParams
*/
getBaseQueryParams() {
var _a;
const queryParams = {};
let hostAppUrl = ((_a = window === null || window === void 0 ? void 0 : window.location) === null || _a === void 0 ? void 0 : _a.host) || '';
// The below check is needed because TS Cloud firewall, blocks localhost/127.0.0.1
// in any url param.
if (hostAppUrl.includes('localhost') ||
hostAppUrl.includes('127.0.0.1')) {
hostAppUrl = 'local-host';
}
queryParams[Param.HostAppUrl] = encodeURIComponent(hostAppUrl);
queryParams[Param.ViewPortHeight] = window.innerHeight;
queryParams[Param.ViewPortWidth] = window.innerWidth;
queryParams[Param.Version] = version;
if (this.embedConfig.customCssUrl) {
queryParams[Param.CustomCSSUrl] = this.embedConfig.customCssUrl;
}
const { disabledActions, disabledActionReason, hiddenActions, visibleActions, additionalFlags, } = this.viewConfig;
if (Array.isArray(visibleActions) && Array.isArray(hiddenActions)) {
this.handleError('You cannot have both hidden actions and visible actions');
return queryParams;
}
if (disabledActions === null || disabledActions === void 0 ? void 0 : disabledActions.length) {
queryParams[Param.DisableActions] = disabledActions;
}
if (disabledActionReason) {
queryParams[Param.DisableActionReason] = disabledActionReason;
}
if (hiddenActions === null || hiddenActions === void 0 ? void 0 : hiddenActions.length) {
queryParams[Param.HideActions] = hiddenActions;
}
if (Array.isArray(visibleActions)) {
queryParams[Param.VisibleActions] = visibleActions;
}
if (additionalFlags && additionalFlags.constructor.name === 'Object') {
Object.assign(queryParams, additionalFlags);
}
return queryParams;
}
/**
* Constructs the base URL string to load v1 of the ThoughtSpot app.
* This is used for embedding Liveboards, visualizations, and full application.
* @param queryString The query string to append to the URL.
* @param isAppEmbed A Boolean parameter to specify if you are embedding
* the full application.
*/
getV1EmbedBasePath(queryString, showPrimaryNavbar = false, disableProfileAndHelp = false, isAppEmbed = false) {
const queryStringFrag = queryString ? `&${queryString}` : '';
const primaryNavParam = `&primaryNavHidden=${!showPrimaryNavbar}`;
const disableProfileAndHelpParam = `&profileAndHelpInNavBarHidden=${disableProfileAndHelp}`;
let queryParams = `?embedApp=true${isAppEmbed ? primaryNavParam : ''}${isAppEmbed ? disableProfileAndHelpParam : ''}${queryStringFrag}`;
if (this.shouldEncodeUrlQueryParams) {
queryParams = `?base64UrlEncodedFlags=${getEncodedQueryParamsString(queryParams.substr(1))}`;
}
let path = `${this.thoughtSpotHost}/${queryParams}#`;
if (!isAppEmbed) {
path = `${path}/embed`;
}
return path;
}
/**
* Renders the embedded ThoughtSpot app in an iframe and sets up
* event listeners.
* @param url
* @param frameOptions
*/
renderIFrame(url, frameOptions) {
if (this.isError) {
return;
}
if (!this.thoughtSpotHost) {
this.throwInitError();
}
if (url.length > URL_MAX_LENGTH) {
// warn: The URL is too long
}
renderInQueue((nextInQueue) => {
var _a;
const initTimestamp = Date.now();
this.executeCallbacks(EmbedEvent.Init, {
data: {
timestamp: initTimestamp,
},
});
uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_START);
(_a = getAuthPromise()) === null || _a === void 0 ? void 0 : _a.then(() => {
uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_COMPLETE);
this.iFrame =
this.iFrame || document.createElement('iframe');
this.iFrame.src = url;
// according to screenfull.js documentation
// allowFullscreen, webkitallowfullscreen and mozallowfullscreen must be true
this.iFrame.allowFullscreen = true;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.iFrame.webkitallowfullscreen = true;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.iFrame.mozallowfullscreen = true;
const width = getCssDimension((frameOptions === null || frameOptions === void 0 ? void 0 : frameOptions.width) || DEFAULT_EMBED_WIDTH);
const height = getCssDimension((frameOptions === null || frameOptions === void 0 ? void 0 : frameOptions.height) || DEFAULT_EMBED_HEIGHT);
this.iFrame.style.width = `${width}`;
this.iFrame.style.height = `${height}`;
this.iFrame.style.border = '0';
this.iFrame.name = 'ThoughtSpot Embedded Analytics';
this.iFrame.addEventListener('load', () => {
nextInQueue();
const loadTimestamp = Date.now();
this.executeCallbacks(EmbedEvent.Load, {
data: {
timestamp: loadTimestamp,
},
});
uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_IFRAME_LOAD_PERFORMANCE, {
timeTookToLoad: loadTimestamp - initTimestamp,
});
});
this.iFrame.addEventListener('error', () => {
nextInQueue();
});
this.el.innerHTML = '';
this.el.appendChild(this.iFrame);
const prefetchIframe = document.querySelectorAll('.prefetchIframe');
if (prefetchIframe.length) {
prefetchIframe.forEach((el) => {
el.remove();
});
}
this.subscribeToEvents();
}).catch((error) => {
nextInQueue();
uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_RENDER_FAILED);
this.handleError(error);
});
});
}
/**
* Sets the height of the iframe
* @param height The height in pixels
*/
setIFrameHeight(height) {
this.iFrame.style.height = `${height}px`;
}
/**
* Executes all registered event handlers for a particular event type
* @param eventType The event type
* @param data The payload invoked with the event handler
* @param eventPort The event Port for a specific MessageChannel
*/
executeCallbacks(eventType, data, eventPort) {
const callbacks = this.eventHandlerMap.get(eventType) || [];
callbacks.forEach((callback) => callback(data, (payload) => {
this.triggerEventOnPort(eventPort, payload);
}));
}
/**
* Returns the ThoughtSpot hostname or IP address.
*/
getThoughtSpotHost() {
return this.thoughtSpotHost;
}
/**
* Gets the v1 event type (if applicable) for the EmbedEvent type
* @param eventType The v2 event type
* @returns The corresponding v1 event type if one exists
* or else the v2 event type itself
*/
getCompatibleEventType(eventType) {
return V1EventMap[eventType] || eventType;
}
/**
* Calculates the iframe center for the current visible viewPort
* of iframe using Scroll position of Host App, offsetTop for iframe
* in Host app. ViewPort height of the tab.
* @returns iframe Center in visible viewport,
* Iframe height,
* View port height.
*/
getIframeCenter() {
const offsetTopClient = getOffsetTop(this.iFrame);
const scrollTopClient = window.scrollY;
const viewPortHeight = window.innerHeight;
const iframeHeight = this.iFrame.offsetHeight;
const iframeScrolled = scrollTopClient - offsetTopClient;
let iframeVisibleViewPort;
let iframeOffset;
if (iframeScrolled < 0) {
iframeVisibleViewPort =
viewPortHeight - (offsetTopClient - scrollTopClient);
iframeVisibleViewPort = Math.min(iframeHeight, iframeVisibleViewPort);
iframeOffset = 0;
}
else {
iframeVisibleViewPort = Math.min(iframeHeight - iframeScrolled, viewPortHeight);
iframeOffset = iframeScrolled;
}
const iframeCenter = iframeOffset + iframeVisibleViewPort / 2;
return {
iframeCenter,
iframeScrolled,
iframeHeight,
viewPortHeight,
iframeVisibleViewPort,
};
}
/**
* Registers an event listener to trigger an alert when the ThoughtSpot app
* sends an event of a particular message type to the host application.
*
* @param messageType The message type
* @param callback A callback function
*/
on(messageType, callback) {
if (this.isRendered) {
this.handleError('Please register event handlers before calling render');
}
const callbacks = this.eventHandlerMap.get(messageType) || [];
callbacks.push(callback);
this.eventHandlerMap.set(messageType, callbacks);
return this;
}
/**
* Triggers an event on specific Port registered against
* for the EmbedEvent
* @param eventType The message type
* @param data The payload to send
*/
triggerEventOnPort(eventPort, payload) {
if (eventPort) {
try {
eventPort.postMessage({
type: payload.type,
data: payload.data,
});
}
catch (e) {
eventPort.postMessage({ error: e });
console.log(e);
}
}
else {
console.log('Event Port is not defined');
}
}
/**
* Triggers an event to the embedded app
* @param messageType The event type
* @param data The payload to send with the message
*/
trigger(messageType, data) {
processTrigger(this.iFrame, messageType, this.thoughtSpotHost, data);
uploadMixpanelEvent(`${MIXPANEL_EVENT.VISUAL_SDK_TRIGGER}-${messageType}`);
return this;
}
/**
* Marks the ThoughtSpot object to have been rendered
* Needs to be overridden by subclasses to do the actual
* rendering of the iframe.
* @param args
*/
render() {
this.isRendered = true;
return this;
}
}
/**
* Base class for embedding v1 experience
* Note: The v1 version of ThoughtSpot Blink works on the AngularJS stack
* which is currently under migration to v2
*/
export class V1Embed extends TsEmbed {
constructor(domSelector, viewConfig) {
super(domSelector, viewConfig);
this.viewConfig = viewConfig;
}
/**
* Render the app in an iframe and set up event handlers
* @param iframeSrc
*/
renderV1Embed(iframeSrc) {
this.renderIFrame(iframeSrc, this.viewConfig.frameParams);
}
// @override
on(messageType, callback) {
const eventType = this.getCompatibleEventType(messageType);
return super.on(eventType, callback);
}
}
//# sourceMappingURL=ts-embed.js.map