UNPKG

ashish-sdk

Version:
457 lines 17.7 kB
/** * 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