/** * Copyright (c) 2022 * * Embed a ThoughtSpot Liveboard or visualization * https://developers.thoughtspot.com/docs/embed-liveboard * https://developers.thoughtspot.com/docs/embed-a-viz * @summary Liveboard & visualization embed * @author Ayon Ghosh */ import { getPreview } from '../utils/graphql/preview-service'; import { ERROR_MESSAGE } from '../errors'; import { EmbedEvent, Param, RuntimeFilter, DOMSelector, HostEvent, SearchLiveboardCommonViewConfig as LiveboardOtherViewConfig, BaseViewConfig, LiveboardAppEmbedViewConfig, FullHeightViewConfig, ErrorDetailsTypes, EmbedErrorCodes, EmbedErrorSeverity, ContextType, DefaultAppInitData, } from '../types'; import { FullHeightController } from '../full-height'; import { getQueryParamString, isUndefined, setParamIfDefined } from '../utils'; import { getAuthPromise } from './base'; import { TsEmbed, V1Embed } from './ts-embed'; import { addPreviewStylesIfNotPresent } from '../utils/global-styles'; import { HostEventRequest, TriggerData, TriggerResponse } from '../contracts/host-event-contracts'; import { logger } from '../utils/logger'; import { SpotterChatViewConfig, StarterPromptsConfig } from './conversation'; import { buildStarterPromptsAppInitData } from './spotter-utils'; import { SpotterVizConfig, buildSpotterVizAppInitData } from './spotter-viz-utils'; // Home unmounts the liveboard container, which is how its state gets cleared. The // settle window gives the container time to do it before we ask for the liveboard back. const HOME_ROUTE = 'home'; const HOME_SETTLE_MS = 200; /** * APP_INIT data shape for LiveboardEmbed. * @internal */ export interface LiveboardEmbedAppInitData extends DefaultAppInitData { embedParams?: { spotterVizConfig?: SpotterVizConfig; starterPrompts?: StarterPromptsConfig; }; } /** * The configuration for the embedded Liveboard or visualization page view. * @group Embed components */ export interface LiveboardViewConfig extends BaseViewConfig, FullHeightViewConfig, LiveboardOtherViewConfig, LiveboardAppEmbedViewConfig { /** * If set to true, the context menu in visualizations will be enabled. * @version SDK: 1.1.0 | ThoughtSpot: 8.1.0.sw * @deprecated this option is deprecated. * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * enableVizTransformations:true, * }) * ``` */ enableVizTransformations?: boolean; /** * The Liveboard to display in the embedded view. * Use either liveboardId or pinboardId to reference the Liveboard to embed. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.3.0 | ThoughtSpot ts7.aug.cl, 7.2.1 * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * liveboardId:'id of liveboard', * }) * ``` */ liveboardId?: string; /** * To support backward compatibility * @hidden */ pinboardId?: string; /** * The visualization within the Liveboard to display. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.9.1 | ThoughtSpot: 8.1.0.cl, 8.4.1-sw * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * vizId:'430496d6-6903-4601-937e-2c691821af3c', * }) * ``` */ vizId?: string; /** * If set to true, all filter chips from a * Liveboard page will be read-only (no X buttons) * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.3.0 | ThoughtSpot ts7.aug.cl, 7.2.1.sw * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * preventLiveboardFilterRemoval:true, * }) * ``` */ preventLiveboardFilterRemoval?: boolean; /** * Array of visualization IDs which should be visible when the Liveboard * renders. This can be changed by triggering the `SetVisibleVizs` * event. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.9.1 | ThoughtSpot: 8.1.0.cl, 8.4.1-sw * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * visibleVizs: [ * '430496d6-6903-4601-937e-2c691821af3c', * 'f547ec54-2a37-4516-a222-2b06719af726' * ] * }) * ``` */ visibleVizs?: string[]; /** * To support backward compatibility * @hidden */ preventPinboardFilterRemoval?: boolean; /** * Render embedded Liveboards and visualizations in the * new Liveboard experience mode. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.14.0 | ThoughtSpot: 8.6.0.cl, 8.8.1-sw * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * liveboardV2:true, * }) * ``` */ liveboardV2?: boolean; /** * Set a Liveboard tab as an active tab. * Specify the tab ID. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.15.0 | ThoughtSpot: 8.7.0.cl, 8.8.1-sw * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * activeTabId:'id-1234', * }) * ``` */ activeTabId?: string; /** * The GUID of a saved personalized view to load. * A personalized view is a saved configuration of a Liveboard * that includes specific filter selections. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.46.0 | ThoughtSpot: 26.4.0.cl * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * liveboardId: 'liveboard-guid', * personalizedViewId: 'view-guid', * activeTabId: 'tab-guid', * }) * ``` */ personalizedViewId?: string; /** * Show or hide the tab panel of the embedded Liveboard. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.25.0 | ThoughtSpot: 9.6.0.cl, 9.8.0.sw * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * hideTabPanel:true, * }) * ``` */ hideTabPanel?: boolean; /** * Show a preview image of the visualization before the visualization loads. * Only works for visualizations embeds with a viz id. * * Also, viz snapshot should be enabled in the ThoughtSpot instance. * Contact ThoughtSpot support to enable this feature. * * Since this will show preview images, be careful that it may show * undesired data to the user when using row level security. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.32.0 | ThoughtSpot: 10.0.0.cl * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * liveboardId: 'liveboard-id', * vizId: 'viz-id', * showPreviewLoader: true, * }); * embed.render(); * ``` */ showPreviewLoader?: boolean; /** * The Liveboard to run on regular intervals to fetch the cdw token. * * Supported embed types: `LiveboardEmbed` * @hidden * @version SDK: 1.35.0 | ThoughtSpot: 10.6.0.cl * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * oAuthPollingInterval: 30000, * }) * ``` */ oAuthPollingInterval?: number; /** * The Liveboard is set to force a token fetch during the initial load. * * Supported embed types: `LiveboardEmbed` * @hidden * @version SDK: 1.35.0 | ThoughtSpot: 10.6.0.cl * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * isForceRedirect: false, * }) * ``` */ isForceRedirect?: boolean; /** * The source connection ID for authentication. * * Supported embed types: `LiveboardEmbed` * @hidden * @version SDK: 1.35.0 | ThoughtSpot: 10.6.0.cl * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * dataSourceId: '', * }) * ``` */ dataSourceId?: string; /** * The list of tab IDs to hide from the embedded Liveboard. * These tabs will be hidden from their respective Liveboards. * Use this to hide a tab ID. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.26.0 | ThoughtSpot: 9.7.0.cl, 10.1.0.sw * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... // other embed view config * hiddenTabs: [ * '430496d6-6903-4601-937e-2c691821af3c', * 'f547ec54-2a37-4516-a222-2b06719af726' * ] * }); * ``` */ hiddenTabs?: string[]; /** * The list of tab IDs to show in the embedded Liveboard. * Only the tabs specified in the array will be shown in the Liveboard. * * Use either `visibleTabs` or `hiddenTabs`. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.26.0 | ThoughtSpot: 9.7.0.cl, 10.1.0.sw * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... // other embed view config * visibleTabs: [ * '430496d6-6903-4601-937e-2c691821af3c', * 'f547ec54-2a37-4516-a222-2b06719af726' * ] * }) * ``` */ visibleTabs?: string[]; /** * This flag is used to enable/disable the styling and grouping in a Liveboard. Use {@link isLiveboardMasterpiecesEnabled} instead. * @deprecated This flag is deprecated. * * Supported embed types: `LiveboardEmbed`, `AppEmbed` * @type {boolean} * @version SDK: 1.40.0 | ThoughtSpot: 10.11.0.cl * @example * ```js * // Replace with embed component name. For example, AppEmbed or LiveboardEmbed * const embed = new ('#tsEmbed', { * ... // other embed view config * isLiveboardStylingAndGroupingEnabled: true, * }) * ``` */ isLiveboardStylingAndGroupingEnabled?: boolean; /** * This flag is used to enable/disable the png embedding of liveboard in scheduled * mails * * Supported embed types: `AppEmbed`, `LiveboardEmbed` * @type {boolean} * @version SDK: 1.42.0 | ThoughtSpot: 10.14.0.cl * @example * ```js * // Replace with embed component name. For example, AppEmbed or LiveboardEmbed * const embed = new ('#tsEmbed', { * ... // other embed view config * isPNGInScheduledEmailsEnabled: true, * }) * ``` */ isPNGInScheduledEmailsEnabled?: boolean; /** * Enables the 'what you see is what you get' PDF export for Liveboards. Each tab is rendered on a single page * following the exact UI layout, instead of splitting visualizations across multiple A4 pages. * This feature is GA from SDK version 1.51.0 and ThoughtSpot version 26.8.0.cl. * * Supported embed types: `AppEmbed`, `LiveboardEmbed` * @type {boolean} * @version SDK: 1.48.0 | ThoughtSpot: 26.5.0.cl * @default true * @example * ```js * // Replace with embed component name. For example, AppEmbed or LiveboardEmbed * const embed = new ('#tsEmbed', { * ... // other embed view config * isContinuousLiveboardPDFEnabled: true, * }) * ``` */ isContinuousLiveboardPDFEnabled?: boolean; /** * This flag is used to enable/disable the XLSX/CSV download option for Liveboards * This feature is GA from SDK version 1.51.0 and ThoughtSpot version 26.6.0.cl. * * Supported embed types: `AppEmbed`, `LiveboardEmbed` * @type {boolean} * @version SDK: 1.46.0 | ThoughtSpot: 26.3.0.cl * @default true * @example * ```js * // Replace with embed component name. For example, AppEmbed or LiveboardEmbed * const embed = new ('#tsEmbed', { * ... // other embed view config * isLiveboardXLSXCSVDownloadEnabled: true, * }) * ``` */ isLiveboardXLSXCSVDownloadEnabled?: boolean; /** * This flag is used to enable/disable the granular XLSX/CSV schedules feature * This feature is GA from SDK version 1.51.0 and ThoughtSpot version 26.6.0.cl. * * Supported embed types: `AppEmbed`, `LiveboardEmbed` * @type {boolean} * @version SDK: 1.46.0 | ThoughtSpot: 26.3.0.cl * @default true * @example * ```js * // Replace with embed component name. For example, AppEmbed or LiveboardEmbed * const embed = new ('#tsEmbed', { * ... // other embed view config * isGranularXLSXCSVSchedulesEnabled: true, * }) * ``` */ isGranularXLSXCSVSchedulesEnabled?: boolean; /** * showSpotterLimitations : show limitation text * of the spotter underneath the chat input. * default is false. * * @type {boolean} * @version SDK: 1.41.1 | ThoughtSpot: 10.5.0.cl * @example * ```js * const embed = new LiveboardEmbed('#embed-container', { * // ...other options * showSpotterLimitations: true, * }) * ``` */ showSpotterLimitations?: boolean; /** * updatedSpotterChatPrompt : Controls the updated spotter chat prompt. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.45.0 | ThoughtSpot: 26.2.0.cl * @default false * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * updatedSpotterChatPrompt : true, * }) * ``` */ updatedSpotterChatPrompt?: boolean; /** * showSpotterRadiance : Controls the radiance on the Spotter page. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.52.0 | ThoughtSpot Cloud: 26.9.0.cl * @default false * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * showSpotterRadiance : true, * }) * ``` */ showSpotterRadiance?: boolean; /** * Enables the stop answer generation button in the Spotter embed UI, * allowing users to interrupt an ongoing answer generation. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.48.0 | ThoughtSpot: 26.5.0.cl * @default false */ enableStopAnswerGenerationEmbed?: boolean; /** * Configuration for customizing Spotter chat UI * branding in tool response cards. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.46.0 | ThoughtSpot: 26.4.0.cl * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * spotterChatConfig: { * hideToolResponseCardBranding: true, * toolResponseCardBrandingLabel: 'MyBrand', * }, * }) * ``` */ spotterChatConfig?: SpotterChatViewConfig; /** * Configuration for the SpotterViz interface shown on the Liveboard. * Customize the brand name, description, chat input placeholder, * starter prompts, and visibility of starter prompts in the SpotterViz panel. * * Supported embed types: `AppEmbed`, `LiveboardEmbed` * @version SDK: 1.50.0 | ThoughtSpot Cloud: 26.7.0.cl * @example * ```js * const embed = new LiveboardEmbed('#embed-container', { * ... // other options * spotterViz: { * brandName: 'MyBrand', * brandHeadline: 'Hi, there! I\'m', * description: 'Ask questions about your data', * inputChatPlaceholder: 'Ask a question...', * hideStarterPrompts: false, * customStarterPrompts: [{ id: '1', displayText: 'Top products', fullPrompt: 'What are the top products by revenue?' }], * // The options below require SDK: 1.51.0 * loaderHeadline: 'Crunching the numbers...', * loaderTips: [ * { label: 'Tip', text: 'try asking about revenue by region' }, * { label: 'Tip', text: 'use natural language' }, * ], * liveboardBrandName: 'Reports', * spotterBrandName: 'Analyst', * insightTileBrandName: 'Insight card', * insightTileViewPlanLabel: 'View plan', * insightTileLoaderText: 'Generating insight', * }, * }) * ``` */ spotterViz?: SpotterVizConfig; /** * If set to true, enables visualization data caching on the Liveboard. * @type {boolean} * @version SDK: 1.49.0 | ThoughtSpot: 26.6.0.cl * @example * ```js * const embed = new LiveboardEmbed('#embed-container', { * ... // other options * enableLiveboardDataCache: true, * }) * ``` */ enableLiveboardDataCache?: boolean; /** * updatedSpotterExperience : Controls the updated Spotter experience. * * Supported embed types: `LiveboardEmbed` * @version SDK: 1.52.0 | ThoughtSpot Cloud: 26.9.0.cl * @default false * @example * ```js * const embed = new LiveboardEmbed('#tsEmbed', { * ... //other embed view config * updatedSpotterExperience : true, * }) * ``` */ updatedSpotterExperience?: boolean; } /** * Embed a ThoughtSpot Liveboard or visualization. When rendered it already * waits for the authentication to complete, so you need not wait for * `AuthStatus.SUCCESS`. * @group Embed components * @example * ```js * import { .. } from '@thoughtspot/visual-embed-sdk'; * init({ ... }); * const embed = new LiveboardEmbed("#container", { * liveboardId: , * // .. other params here. * }) * ``` */ export class LiveboardEmbed extends V1Embed { protected viewConfig: LiveboardViewConfig; private readonly fullHeightController?: FullHeightController; constructor(domSelector: DOMSelector, viewConfig: LiveboardViewConfig) { viewConfig.embedComponentType = 'LiveboardEmbed'; super(domSelector, viewConfig); if (this.viewConfig.fullHeight === true) { if (this.viewConfig.vizId) { logger.warn('Full height is currently only supported for Liveboard embeds.' + 'Using full height with vizId might lead to unexpected behavior.'); } this.fullHeightController = new FullHeightController(this.viewConfig, { getIframe: () => this.iFrame, setFrameHeight: (height) => this.setIFrameHeight(height), on: (eventType, callback) => { this.on(eventType, callback); }, trigger: (hostEvent, data) => { this.trigger(hostEvent, data); }, }); } } protected async getAppInitData(): Promise { const defaultAppInitData = await super.getAppInitData(); const vizInitData = buildSpotterVizAppInitData(defaultAppInitData, this.viewConfig); return buildStarterPromptsAppInitData(vizInitData, this.viewConfig); } /** * Construct a map of params to be passed on to the * embedded Liveboard or visualization. */ protected getEmbedParams() { const params = this.getEmbedParamsObject(); const queryParams = getQueryParamString(params, true); return queryParams; } protected getEmbedParamsObject() { let params: any = {}; params = this.getBaseQueryParams(params); const { enableVizTransformations, visibleVizs, liveboardV2, vizId, hideTabPanel, activeTabId, hideLiveboardHeader, showLiveboardDescription, showLiveboardTitle, isLiveboardHeaderSticky = true, isLiveboardCompactHeaderEnabled, showLiveboardVerifiedBadge = true, showLiveboardReverifyBanner = true, hideIrrelevantChipsInLiveboardTabs, showMaskedFilterChip = false, isLiveboardMasterpiecesEnabled, newChartsLibrary, isEnhancedFilterInteractivityEnabled, enableAskSage, enable2ColumnLayout, isLiveboardAlwaysOn12ColLayout, dataPanelV2 = true, updatedSpotterExperience, enableCustomColumnGroups = false, oAuthPollingInterval, isForceRedirect, dataSourceId, coverAndFilterOptionInPDF, isLiveboardStylingAndGroupingEnabled, liveboardGutter, isPNGInScheduledEmailsEnabled = false, isLiveboardXLSXCSVDownloadEnabled, isGranularXLSXCSVSchedulesEnabled, showSpotterLimitations, isCentralizedLiveboardFilterUXEnabled = false, isScopedLiveboardFilteringEnabled, isLinkParametersEnabled, updatedSpotterChatPrompt, showSpotterRadiance, enableStopAnswerGenerationEmbed, spotterChatConfig, isThisPeriodInDateFiltersEnabled, isContinuousLiveboardPDFEnabled, enableLiveboardDataCache, } = this.viewConfig; const preventLiveboardFilterRemoval = this.viewConfig.preventLiveboardFilterRemoval || this.viewConfig.preventPinboardFilterRemoval; this.fullHeightController?.addQueryParams(params); if (enableVizTransformations !== undefined) { params[Param.EnableVizTransformations] = enableVizTransformations.toString(); } if (preventLiveboardFilterRemoval) { params[Param.preventLiveboardFilterRemoval] = true; } if (!isUndefined(updatedSpotterChatPrompt)) { params[Param.UpdatedSpotterChatPrompt] = !!updatedSpotterChatPrompt; } if (!isUndefined(showSpotterRadiance)) { params[Param.ShowSpotterRadiance] = !!showSpotterRadiance; } if (!isUndefined(enableStopAnswerGenerationEmbed)) { params[Param.EnableStopAnswerGenerationEmbed] = !!enableStopAnswerGenerationEmbed; } if (visibleVizs) { params[Param.visibleVizs] = visibleVizs; } params[Param.livedBoardEmbed] = true; if (vizId) { params[Param.vizEmbed] = true; } if (liveboardV2 !== undefined) { params[Param.LiveboardV2Enabled] = liveboardV2; } if (enable2ColumnLayout !== undefined) { params[Param.Enable2ColumnLayout] = enable2ColumnLayout; } if (isLiveboardAlwaysOn12ColLayout !== undefined) { params[Param.IsLiveboardAlwaysOn12ColLayout] = isLiveboardAlwaysOn12ColLayout; } if (hideTabPanel) { params[Param.HideTabPanel] = hideTabPanel; } if (hideLiveboardHeader) { params[Param.HideLiveboardHeader] = hideLiveboardHeader; } if (showLiveboardDescription) { params[Param.ShowLiveboardDescription] = showLiveboardDescription; } if (showLiveboardTitle) { params[Param.ShowLiveboardTitle] = showLiveboardTitle; } if (enableAskSage) { params[Param.enableAskSage] = enableAskSage; } if (oAuthPollingInterval !== undefined) { params[Param.OauthPollingInterval] = oAuthPollingInterval; } if (isForceRedirect) { params[Param.IsForceRedirect] = isForceRedirect; } if (dataSourceId !== undefined) { params[Param.DataSourceId] = dataSourceId; } if (isLiveboardStylingAndGroupingEnabled !== undefined) { params[Param.IsLiveboardStylingAndGroupingEnabled] = isLiveboardStylingAndGroupingEnabled; } if (liveboardGutter !== undefined) { params[Param.LiveboardGutter] = liveboardGutter; } if (isPNGInScheduledEmailsEnabled !== undefined) { params[Param.isPNGInScheduledEmailsEnabled] = isPNGInScheduledEmailsEnabled; } if (isLiveboardXLSXCSVDownloadEnabled !== undefined) { params[Param.isLiveboardXLSXCSVDownloadEnabled] = isLiveboardXLSXCSVDownloadEnabled; } if (isGranularXLSXCSVSchedulesEnabled !== undefined) { params[Param.isGranularXLSXCSVSchedulesEnabled] = isGranularXLSXCSVSchedulesEnabled; } if (showSpotterLimitations !== undefined) { params[Param.ShowSpotterLimitations] = showSpotterLimitations; } if (!isUndefined(updatedSpotterExperience)) { params[Param.UpdatedSpotterExperience] = !!updatedSpotterExperience; } // Handle spotterChatConfig params if (spotterChatConfig) { const { hideToolResponseCardBranding, toolResponseCardBrandingLabel, spotterFileUploadEnabled, spotterFileUploadFileTypes, enableStarterPrompts, openSpotterOnLiveboardByDefault, } = spotterChatConfig; setParamIfDefined(params, Param.HideToolResponseCardBranding, hideToolResponseCardBranding, true); setParamIfDefined(params, Param.ToolResponseCardBrandingLabel, toolResponseCardBrandingLabel); if (spotterFileUploadEnabled !== undefined) { params[Param.SpotterFileUploadEnabled] = spotterFileUploadEnabled; } if (enableStarterPrompts !== undefined) { params[Param.IsStarterPromptsEnabled] = enableStarterPrompts; } if (openSpotterOnLiveboardByDefault !== undefined) { params[Param.OpenSpotterOnLiveboardByDefault] = openSpotterOnLiveboardByDefault; } if (spotterFileUploadFileTypes !== undefined) { params[Param.SpotterFileUploadFileTypes] = JSON.stringify(spotterFileUploadFileTypes); } } if (isLinkParametersEnabled !== undefined) { params[Param.isLinkParametersEnabled] = isLinkParametersEnabled; } if (isCentralizedLiveboardFilterUXEnabled !== undefined) { params[ Param.isCentralizedLiveboardFilterUXEnabled ] = isCentralizedLiveboardFilterUXEnabled; } if (isScopedLiveboardFilteringEnabled !== undefined) { params[ Param.isScopedLiveboardFilteringEnabled ] = isScopedLiveboardFilteringEnabled; } if (isThisPeriodInDateFiltersEnabled !== undefined) { params[Param.IsThisPeriodInDateFiltersEnabled] = isThisPeriodInDateFiltersEnabled; } if (isContinuousLiveboardPDFEnabled !== undefined) { params[Param.IsWYSIWYGLiveboardPDFEnabled] = isContinuousLiveboardPDFEnabled; } if (enableLiveboardDataCache !== undefined) { params[Param.EnableLiveboardDataCache] = enableLiveboardDataCache; } if (newChartsLibrary !== undefined) { params[Param.EnableNewChartLibrary] = newChartsLibrary; } params[Param.LiveboardHeaderSticky] = isLiveboardHeaderSticky; if (!isUndefined(isLiveboardCompactHeaderEnabled)) { params[Param.LiveboardHeaderV2] = isLiveboardCompactHeaderEnabled; } params[Param.ShowLiveboardVerifiedBadge] = showLiveboardVerifiedBadge; params[Param.ShowLiveboardReverifyBanner] = showLiveboardReverifyBanner; if (!isUndefined(hideIrrelevantChipsInLiveboardTabs)) { params[Param.HideIrrelevantFiltersInTab] = hideIrrelevantChipsInLiveboardTabs; } params[Param.ShowMaskedFilterChip] = showMaskedFilterChip; if (!isUndefined(isLiveboardMasterpiecesEnabled)) { params[Param.IsLiveboardMasterpiecesEnabled] = isLiveboardMasterpiecesEnabled; } if (!isUndefined(isEnhancedFilterInteractivityEnabled)) { params[Param.IsEnhancedFilterInteractivityEnabled] = isEnhancedFilterInteractivityEnabled; } params[Param.DataPanelV2Enabled] = dataPanelV2; params[Param.EnableCustomColumnGroups] = enableCustomColumnGroups; if (!isUndefined(coverAndFilterOptionInPDF)) { params[Param.CoverAndFilterOptionInPDF] = coverAndFilterOptionInPDF; } const queryParams = getQueryParamString(params, true); return params; } private getIframeSuffixSrc( liveboardId: string, vizId: string, activeTabId: string, personalizedViewId?: string, ) { // Extract view from liveboardId if passed along with it (legacy // approach) // View must be appended as query param at the end, not // embedded in path let liveboardGuid = liveboardId; let legacyViewId: string | undefined; if (liveboardId?.includes('?')) { const [id, query] = liveboardId.split('?'); liveboardGuid = id; const params = new URLSearchParams(query); legacyViewId = params.get('view') || undefined; } // personalizedViewId takes precedence over legacyViewId (when passed // as part of liveboardId) const effectiveViewId = personalizedViewId || legacyViewId; let suffix = `/embed/viz/${liveboardGuid}`; if (activeTabId) { suffix = `${suffix}/tab/${activeTabId}`; } if (vizId) { suffix = `${suffix}/${vizId}`; } const additionalParams: { [key: string]: string } = {}; if (effectiveViewId) { additionalParams.view = effectiveViewId; } const tsPostHashParams = this.getThoughtSpotPostUrlParams(additionalParams); suffix = `${suffix}${tsPostHashParams}`; return suffix; } /** * Construct the URL of the embedded ThoughtSpot Liveboard or visualization * to be loaded within the iFrame. */ private getIFrameSrc(): string { const { vizId, activeTabId, personalizedViewId } = this.viewConfig; const liveboardId = this.viewConfig.liveboardId ?? this.viewConfig.pinboardId; if (!liveboardId) { this.handleError({ errorType: ErrorDetailsTypes.VALIDATION_ERROR, message: ERROR_MESSAGE.LIVEBOARD_VIZ_ID_VALIDATION, code: EmbedErrorCodes.LIVEBOARD_ID_MISSING, severity: EmbedErrorSeverity.SEV1, error: ERROR_MESSAGE.LIVEBOARD_VIZ_ID_VALIDATION, }); } return `${this.getRootIframeSrc()}${this.getIframeSuffixSrc( liveboardId, vizId, activeTabId, personalizedViewId, )}`; } private setActiveTab(data: { tabId: string }) { if (!this.viewConfig.vizId) { const prefixPath = this.iFrame.src.split('#/')[1].split('/tab')[0]; const path = `${prefixPath}/tab/${data.tabId}`; super.trigger(HostEvent.Navigate, path); } } private async showPreviewLoader() { if (!this.viewConfig.showPreviewLoader || !this.viewConfig.vizId) { return; } try { const preview = await getPreview( this.thoughtSpotHost, this.viewConfig.vizId, this.viewConfig.liveboardId, ); if (!preview.vizContent) { return; } addPreviewStylesIfNotPresent(); const div = document.createElement('div'); div.innerHTML = `
${preview.vizContent}
`; const previewDiv = div.firstElementChild as HTMLElement; this.hostElement.appendChild(previewDiv); this.hostElement.style.position = 'relative'; this.on(EmbedEvent.Data, () => { previewDiv.remove(); }); } catch (error) { console.error('Error fetching preview', error); } } /** * @hidden * Internal state to track the current liveboard id. * This is used to navigate to the correct liveboard when the prerender is visible. */ public currentLiveboardState = { liveboardId: this.viewConfig.liveboardId, vizId: this.viewConfig.vizId, activeTabId: this.viewConfig.activeTabId, personalizedViewId: this.viewConfig.personalizedViewId, }; protected beforePrerenderVisible(): void { super.beforePrerenderVisible(); // Captured before showPreRender() hands the wrapper over to this instance. // Itself means a hide/show, not a hand-over, so there is nothing stale to clear. const previous = this.getPreRenderObj(); const showing = previous === this ? undefined : previous?.currentLiveboardState; this.executeAfterEmbedContainerLoaded(async () => { // Without this the params callback suspends on its await and Navigate // goes out first, so the container loads with the previous config's filters. await this.preRenderParamsApplied; if (this.isShowingLiveboardRoute(showing)) { // Navigate to the route we are already on is a no-op, so the liveboard // keeps its state. Home unmounts the container, which clears it. this.trigger(HostEvent.Navigate, HOME_ROUTE) .catch((error) => logger.warn('Could not route via home', error)); // Not awaited above: the container never acks Navigate, so that promise // only settles on the 30s trigger timeout. await new Promise((resolve) => { setTimeout(resolve, HOME_SETTLE_MS); }); } this.navigateToLiveboard( this.viewConfig.liveboardId, this.viewConfig.vizId, this.viewConfig.activeTabId, this.viewConfig.personalizedViewId, ); this.currentLiveboardState = { liveboardId: this.viewConfig.liveboardId, vizId: this.viewConfig.vizId, activeTabId: this.viewConfig.activeTabId, personalizedViewId: this.viewConfig.personalizedViewId, }; }); } // The whole route, not just the liveboard id: the path is built from the viz, tab // and view too, so a same-liveboard show onto another tab is a real navigation. private isShowingLiveboardRoute(showing?: LiveboardEmbed['currentLiveboardState']): boolean { if (!showing?.liveboardId) return false; const { liveboardId, vizId, activeTabId, personalizedViewId, } = this.viewConfig; return showing.liveboardId === liveboardId && showing.vizId === vizId && showing.activeTabId === activeTabId && showing.personalizedViewId === personalizedViewId; } protected async handleRenderForPrerender(): Promise { if (isUndefined(this.viewConfig.liveboardId)) { return this.prerenderGeneric(); } return super.handleRenderForPrerender(); } /** * Triggers an event to the embedded app * Payload typing follows {@link TsEmbed.trigger}: unknown fields fail to compile * from SDK 1.52.0; strict contract checks land in SDK 1.54.0. * @param {HostEvent} messageType The event type * @param {TriggerData} data The payload, typed against the event's contract * @returns A promise that resolves with the response from the embedded app */ public trigger< HostEventT extends HostEvent, // Must mirror TsEmbed.trigger: without the default, no-payload calls // through LiveboardEmbed lose the typed response and collapse to `any`. PayloadT = HostEventRequest, ContextT extends ContextType = ContextType, >( messageType: HostEventT, // Mirror TsEmbed.trigger: contract shape is the contextual type and // unknown fields on object literals are flagged. Strict in SDK 1.54.0. data: TriggerData = ({} as any), context?: ContextT, ): Promise> { const dataWithVizId: any = data; if (messageType === HostEvent.SetActiveTab) { this.setActiveTab(data as { tabId: string }); return Promise.resolve(null); } if (typeof dataWithVizId === 'object' && this.viewConfig.vizId) { dataWithVizId.vizId = this.viewConfig.vizId; } return super.trigger(messageType, dataWithVizId, context); } /** * Destroys the ThoughtSpot embed, and remove any nodes from the DOM. * @version SDK: 1.39.0 | ThoughtSpot: 10.10.0.cl */ public destroy() { super.destroy(); this.fullHeightController?.destroy(); } private postRender() { this.fullHeightController?.onRender(); } /** * Render an embedded ThoughtSpot Liveboard or visualization * @param renderOptions An object specifying the Liveboard ID, * visualization ID and the runtime filters. */ public async render(): Promise { await super.render(); const src = this.getIFrameSrc(); await this.renderV1Embed(src); this.showPreviewLoader(); this.postRender(); return this; } public navigateToLiveboard( liveboardId: string, vizId?: string, activeTabId?: string, personalizedViewId?: string, ) { const path = this.getIframeSuffixSrc(liveboardId, vizId, activeTabId, personalizedViewId); this.viewConfig.liveboardId = liveboardId; this.viewConfig.activeTabId = activeTabId; this.viewConfig.vizId = vizId; this.viewConfig.personalizedViewId = personalizedViewId; if (this.isRendered) { this.trigger(HostEvent.Navigate, path.substring(1)); } else if (this.getPreRenderConfig().id) { this.preRender(true); } else { this.render(); } } /** * Returns the full url of the Liveboard/visualization which can be used to open * this Liveboard inside the full ThoughtSpot application in a new tab. * @returns url string */ public getLiveboardUrl(): string { let url = `${this.thoughtSpotHost}/#/pinboard/${this.viewConfig.liveboardId}`; if (this.viewConfig.activeTabId) { url = `${url}/tab/${this.viewConfig.activeTabId}`; } if (this.viewConfig.vizId) { url = `${url}/${this.viewConfig.vizId}`; } if (this.viewConfig.personalizedViewId) { url = `${url}?view=${this.viewConfig.personalizedViewId}`; } return url; } } /** * @hidden */ export class PinboardEmbed extends LiveboardEmbed { }