UNPKG

@thoughtspot/visual-embed-sdk

Version:
293 lines (259 loc) 10.6 kB
import { ContextType, HostEvent } from '../../types'; import { processTrigger as processTriggerService } from '../../utils/processTrigger'; import { getEmbedConfig } from '../embedConfig'; import { isValidUpdateFiltersPayload, isValidDrillDownPayload, throwUpdateFiltersValidationError, throwDrillDownValidationError, } from './utils'; import { UIPassthroughArrayResponse, UIPassthroughEvent, HostEventRequest, HostEventResponse, UIPassthroughRequest, UIPassthroughResponse, TriggerPayload, TriggerResponse, } 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: Partial<Record<HostEvent, UIPassthroughEvent>> = { // 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 { iFrame: HTMLIFrameElement; /** Cached list of available UI passthrough keys from the embedded app */ private availablePassthroughKeysCache: string[] | null = null; /** Host events with custom handlers * (setters or special logic) - * bound to instance for protected method access */ private readonly customHandlers: Partial< Record<HostEvent, (payload: any, context?: ContextType) => Promise<any>> >; constructor(iFrame?: HTMLIFrameElement) { 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 */ protected async processTrigger(message: HostEvent, data: any, context?: ContextType): Promise<any> { if (!this.iFrame) { throw new Error('Iframe element is not set'); } const thoughtspotHost = getEmbedConfig().thoughtSpotHost; return processTriggerService( this.iFrame, message, thoughtspotHost, data, context, ); } public async handleHostEventWithParam<UIPassthroughEventT extends UIPassthroughEvent>( apiName: UIPassthroughEventT, parameters: UIPassthroughRequest<UIPassthroughEventT>, context?: ContextType, ): Promise<UIPassthroughResponse<UIPassthroughEventT>> { const response = (await this.triggerUIPassthroughApi(apiName, parameters, context)) ?.find?.((r) => r.error || r.value); if (!response) { const error = `No answer found${parameters.vizId ? ` for vizId: ${parameters.vizId}` : ''}.`; throw { error }; } const errors = response.error || (response.value as any)?.errors || (response.value as any)?.error; if (errors) { const message = typeof errors === 'string' ? errors : JSON.stringify(errors); throw { error: message }; } return { ...response.value }; } public async hostEventFallback( hostEvent: HostEvent, data: any, context?: ContextType, ): Promise<any> { 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. */ private async getDataWithPassthroughFallback<UIPassthroughEventT extends UIPassthroughEvent>( passthroughEvent: UIPassthroughEventT, hostEvent: HostEvent, payload: any, context?: ContextType, ): Promise<UIPassthroughResponse<UIPassthroughEventT>> { const response = await this.triggerUIPassthroughApi( passthroughEvent, payload || {}, context, ); const matched = response?.find?.((r) => r.error || r.value); if (!matched) { return this.hostEventFallback(hostEvent, payload, context); } const errors = matched.error || (matched.value as any)?.errors || (matched.value as any)?.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 */ public setIframeElement(iFrame: HTMLIFrameElement): void { 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. */ private async getAvailableUIPassthroughKeys(context?: ContextType): Promise<string[]> { if (this.availablePassthroughKeysCache !== null) { return this.availablePassthroughKeysCache; } try { const response = await this.triggerUIPassthroughApi( UIPassthroughEvent.GetAvailableUIPassthroughs, {}, context, ); const matched = response?.find?.((r) => r.value && !r.error); const keys = matched?.value?.keys; this.availablePassthroughKeysCache = Array.isArray(keys) ? keys : []; return this.availablePassthroughKeysCache; } catch { return []; } } public async triggerUIPassthroughApi<UIPassthroughEventT extends UIPassthroughEvent>( apiName: UIPassthroughEventT, parameters: UIPassthroughRequest<UIPassthroughEventT>, context?: ContextType, ): Promise<UIPassthroughArrayResponse<UIPassthroughEventT>> { const res = await this.processTrigger(HostEvent.UIPassthrough, { type: apiName, parameters, }, context); return res; } protected async handlePinEvent( payload: HostEventRequest<HostEvent.Pin>, context?: ContextType, ): Promise<HostEventResponse<HostEvent.Pin, ContextType>> { if (!payload || !('newVizName' in payload)) { return this.hostEventFallback(HostEvent.Pin, payload, context); } const formattedPayload = { ...payload, pinboardId: payload.liveboardId ?? (payload as any).pinboardId, newPinboardName: payload.newLiveboardName ?? (payload as any).newPinboardName, }; const data = await this.handleHostEventWithParam( UIPassthroughEvent.PinAnswerToLiveboard, formattedPayload, context as ContextType, ); return { ...data, liveboardId: (data as any).pinboardId, }; } protected async handleSaveAnswerEvent( payload: HostEventRequest<HostEvent.SaveAnswer>, context?: ContextType, ): Promise<any> { 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 as ContextType, ); return { ...data, answerId: data?.saveResponse?.data?.Answer__save?.answer?.id, }; } protected handleUpdateFiltersEvent( payload: HostEventRequest<HostEvent.UpdateFilters>, context?: ContextType, ): Promise<any> { if (!isValidUpdateFiltersPayload(payload)) { throwUpdateFiltersValidationError(); } return this.handleHostEventWithParam(UIPassthroughEvent.UpdateFilters, payload, context as ContextType); } protected handleDrillDownEvent( payload: HostEventRequest<HostEvent.DrillDown>, context?: ContextType, ): Promise<any> { if (!isValidDrillDownPayload(payload)) { throwDrillDownValidationError(); } return this.handleHostEventWithParam(UIPassthroughEvent.Drilldown, payload, context as ContextType); } /** * 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 */ public async triggerHostEvent< HostEventT extends HostEvent, PayloadT, ContextT extends ContextType, >( hostEvent: HostEventT, payload?: TriggerPayload<PayloadT, HostEventT>, context?: ContextT, ): Promise<TriggerResponse<PayloadT, HostEventT, ContextType>> { 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 as ContextType) : []; if (passthroughEvent && keys.length > 0 && !keys.includes(passthroughEvent)) { return this.hostEventFallback(hostEvent, payload, context) as any; } // Custom handler (setters) > getter passthrough > legacy fallback return (customHandler ? customHandler(payload, context as ContextType) : passthroughEvent ? this.getDataWithPassthroughFallback(passthroughEvent, hostEvent, payload, context as ContextType) : this.hostEventFallback(hostEvent, payload, context) ) as any; } }