• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

thoughtspot / visual-embed-sdk / #1480

21 Jan 2025 10:27AM UTC coverage: 38.568% (-2.7%) from 41.223%
#1480

push

Prashant.patil
SCAL-233454-exp auto-attach wv msg handler lint errors

227 of 1153 branches covered (19.69%)

Branch coverage included in aggregate %.

0 of 15 new or added lines in 1 file covered. (0.0%)

445 existing lines in 8 files now uncovered.

1244 of 2661 relevant lines covered (46.75%)

8.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

11.96
/src/embed/base.ts
1
/* eslint-disable camelcase */
2
/* eslint-disable import/no-mutable-exports */
3
/**
4
 * Copyright (c) 2022
5
 *
6
 * Base classes
7
 * @summary Base classes
8
 * @author Ayon Ghosh <ayon.ghosh@thoughtspot.com>
9
 */
10
import EventEmitter from 'eventemitter3';
16✔
11
import { registerReportingObserver } from '../utils/reporting';
16✔
12
import { resetCachedAuthToken } from '../authToken';
16✔
13
import { logger, setGlobalLogLevelOverride } from '../utils/logger';
16✔
14
import { tokenizedFetch } from '../tokenizedFetch';
16✔
15
import { EndPoints } from '../utils/authService/authService';
16✔
16
import { getThoughtSpotHost } from '../config';
16✔
17
import {
16✔
18
    AuthFunction,
19
    AuthType, EmbedConfig, LogLevel, Param, PrefetchFeatures,
20
} from '../types';
21
import {
16✔
22
    logout as _logout,
23
    AuthFailureType,
24
    AuthStatus,
25
    AuthEvent,
26
    notifyAuthFailure,
27
    notifyAuthSDKSuccess,
28
    notifyAuthSuccess,
29
    notifyLogout,
30
    setAuthEE,
31
    AuthEventEmitter,
32
    postLoginService,
33
} from '../auth';
34
import { getEmbedConfig, setEmbedConfig } from './embedConfig';
16✔
35
import { getQueryParamString } from '../utils';
16✔
36

37
const CONFIG_DEFAULTS: Partial<EmbedConfig> = {
16✔
38
    loginFailedMessage: 'Not logged in',
39
    authTriggerText: 'Authorize',
40
    authType: AuthType.None,
41
    logLevel: LogLevel.ERROR,
42
};
43

44
export interface executeTMLInput {
45
    metadata_tmls: string[];
46
    import_policy?: 'PARTIAL' | 'ALL_OR_NONE' | 'VALIDATE_ONLY';
47
    create_new?: boolean;
48
}
49

50
export interface exportTMLInput {
51
    metadata: {
52
        identifier: string;
53
        type?: 'LIVEBOARD' | 'ANSWER' | 'LOGICAL_TABLE' | 'CONNECTION';
54
    }[];
55
    export_associated?: boolean;
56
    export_fqn?: boolean;
57
    edoc_format?: 'YAML' | 'JSON';
58
}
59

60
export let authPromise: Promise<boolean>;
61

62
export const getAuthPromise = (): Promise<boolean> => authPromise;
16✔
63

64
export {
65
    notifyAuthFailure, notifyAuthSDKSuccess, notifyAuthSuccess, notifyLogout,
16✔
66
};
67

68
/**
69
 * Perform authentication on the ThoughtSpot app as applicable.
70
 */
71
export const handleAuth = (): Promise<boolean> => {
16✔
72
    let authFn: AuthFunction;
UNCOV
73
    if (process.env.SDK_ENVIRONMENT === 'mobile') {
×
UNCOV
74
        authFn = async (embedConfig) => {
×
75
            const { authenticateMobile } = await import('../auth');
×
UNCOV
76
            return authenticateMobile(embedConfig);
×
77
        };
78
    } else {
UNCOV
79
        authFn = async (embedConfig) => {
×
UNCOV
80
            const { authenticate } = await import('../auth');
×
UNCOV
81
            return authenticate(embedConfig);
×
82
        };
83
    }
84
    authPromise = authFn(getEmbedConfig());
×
85
    authPromise.then(
×
86
        (isLoggedIn) => {
UNCOV
87
            if (!isLoggedIn) {
×
88
                notifyAuthFailure(AuthFailureType.SDK);
×
89
            } else {
90
                // Post login service is called after successful login.
UNCOV
91
                postLoginService();
×
92
                notifyAuthSDKSuccess();
×
93
            }
94
        },
95
        () => {
UNCOV
96
            notifyAuthFailure(AuthFailureType.SDK);
×
97
        },
98
    );
UNCOV
99
    return authPromise;
×
100
};
101

102
const hostUrlToFeatureUrl = {
16✔
UNCOV
103
    [PrefetchFeatures.SearchEmbed]: (url: string, flags: string) => `${url}v2/?${flags}#/embed/answer`,
×
104
    [PrefetchFeatures.LiveboardEmbed]: (url: string, flags: string) => `${url}?${flags}`,
×
105
    [PrefetchFeatures.FullApp]: (url: string, flags: string) => `${url}?${flags}`,
×
106
    [PrefetchFeatures.VizEmbed]: (url: string, flags: string) => `${url}?${flags}`,
×
107
};
108

109
/**
110
 * Prefetches static resources from the specified URL. Web browsers can then cache the
111
 * prefetched resources and serve them from the user's local disk to provide faster access
112
 * to your app.
113
 * @param url The URL provided for prefetch
114
 * @param prefetchFeatures Specify features which needs to be prefetched.
115
 * @param additionalFlags This can be used to add any URL flag.
116
 * @version SDK: 1.4.0 | ThoughtSpot: ts7.sep.cl, 7.2.1
117
 * @group Global methods
118
 */
119
export const prefetch = (
16✔
120
    url?: string,
121
    prefetchFeatures?: PrefetchFeatures[],
122
    additionalFlags?: { [key: string]: string | number | boolean },
123
): void => {
UNCOV
124
    if (url === '') {
×
125
        // eslint-disable-next-line no-console
UNCOV
126
        logger.warn('The prefetch method does not have a valid URL');
×
127
    } else {
UNCOV
128
        const features = prefetchFeatures || [PrefetchFeatures.FullApp];
×
129
        let hostUrl = url || getEmbedConfig().thoughtSpotHost;
×
130
        const prefetchFlags = {
×
131
            [Param.EmbedApp]: true,
132
            ...getEmbedConfig()?.additionalFlags,
×
133
            ...additionalFlags,
134
        };
UNCOV
135
        hostUrl = hostUrl[hostUrl.length - 1] === '/' ? hostUrl : `${hostUrl}/`;
×
136
        Array.from(
×
137
            new Set(features
UNCOV
138
                .map((feature) => hostUrlToFeatureUrl[feature](
×
139
                    hostUrl,
140
                    getQueryParamString(prefetchFlags),
141
                ))),
142
        )
143
            .forEach(
144
                (prefetchUrl, index) => {
UNCOV
145
                    const iFrame = document.createElement('iframe');
×
146
                    iFrame.src = prefetchUrl;
×
147
                    iFrame.style.width = '0';
×
148
                    iFrame.style.height = '0';
×
149
                    iFrame.style.border = '0';
×
150
                    iFrame.classList.add('prefetchIframe');
×
151
                    iFrame.classList.add(`prefetchIframeNum-${index}`);
×
152
                    document.body.appendChild(iFrame);
×
153
                },
154
            );
155
    }
156
};
157

158
/**
159
 *
160
 * @param embedConfig
161
 */
162
function sanity(embedConfig: EmbedConfig) {
UNCOV
163
    if (embedConfig.thoughtSpotHost === undefined) {
×
164
        throw new Error('ThoughtSpot host not provided');
×
165
    }
UNCOV
166
    if (embedConfig.authType === AuthType.TrustedAuthToken) {
×
167
        if (!embedConfig.authEndpoint && typeof embedConfig.getAuthToken !== 'function') {
×
168
            throw new Error('Trusted auth should provide either authEndpoint or getAuthToken');
×
169
        }
170
    }
171
}
172

173
/**
174
 *
175
 * @param embedConfig
176
 */
177
function backwardCompat(embedConfig: EmbedConfig): EmbedConfig {
UNCOV
178
    const newConfig = { ...embedConfig };
×
179
    if (embedConfig.noRedirect !== undefined && embedConfig.inPopup === undefined) {
×
180
        newConfig.inPopup = embedConfig.noRedirect;
×
181
    }
UNCOV
182
    return newConfig;
×
183
}
184

185
/**
186
 * Initializes the Visual Embed SDK globally and perform
187
 * authentication if applicable. This function needs to be called before any ThoughtSpot
188
 * component like Liveboard etc can be embedded. But need not wait for AuthEvent.SUCCESS
189
 * to actually embed. That is handled internally.
190
 * @param embedConfig The configuration object containing ThoughtSpot host,
191
 * authentication mechanism and so on.
192
 * @example
193
 * ```js
194
 *   const authStatus = init({
195
 *     thoughtSpotHost: 'https://my.thoughtspot.cloud',
196
 *     authType: AuthType.None,
197
 *   });
198
 *   authStatus.on(AuthStatus.FAILURE, (reason) => { // do something here });
199
 * ```
200
 * @returns {@link AuthEventEmitter} event emitter which emits events on authentication success,
201
 *      failure and logout. See {@link AuthStatus}
202
 * @version SDK: 1.0.0 | ThoughtSpot ts7.april.cl, 7.2.1
203
 * @group Authentication / Init
204
 */
205
export const init = (embedConfig: EmbedConfig):
16✔
206
    AuthEventEmitter => {
UNCOV
207
    sanity(embedConfig);
×
UNCOV
208
    resetCachedAuthToken();
×
UNCOV
209
    embedConfig = setEmbedConfig(
×
210
        backwardCompat({
211
            ...CONFIG_DEFAULTS,
212
            ...embedConfig,
213
            thoughtSpotHost: getThoughtSpotHost(embedConfig),
214
        }),
215
    );
216
    setGlobalLogLevelOverride(embedConfig.logLevel);
×
217

UNCOV
218
    const authEE = new EventEmitter<AuthStatus | AuthEvent>();
×
UNCOV
219
    setAuthEE(authEE);
×
UNCOV
220
    handleAuth();
×
221

222
    const { password, ...configToTrack } = getEmbedConfig();
×
UNCOV
223
    if (process.env.SDK_ENVIRONMENT === 'web') {
×
UNCOV
224
        registerReportingObserver();
×
UNCOV
225
        (async () => {
×
UNCOV
226
            const { uploadMixpanelEvent, MIXPANEL_EVENT } = await import('../mixpanel-service');
×
UNCOV
227
            uploadMixpanelEvent(MIXPANEL_EVENT.VISUAL_SDK_CALLED_INIT, {
×
228
                ...configToTrack,
229
                usedCustomizationSheet: embedConfig.customizations?.style?.customCSSUrl
×
230
            != null,
231
                usedCustomizationVariables:
232
            embedConfig.customizations?.style?.customCSS?.variables != null,
×
233
                usedCustomizationRules:
234
            embedConfig.customizations?.style?.customCSS?.rules_UNSTABLE != null,
×
235
                usedCustomizationStrings: !!embedConfig.customizations?.content?.strings,
×
236
                usedCustomizationIconSprite: !!embedConfig.customizations?.iconSpriteUrl,
×
237
            });
238
        })();
239
        if (getEmbedConfig().callPrefetch) {
×
UNCOV
240
            prefetch(getEmbedConfig().thoughtSpotHost);
×
241
        }
242
    } else {
UNCOV
243
        console.log('emptying out no mixpanel for mobile');
×
244
    }
UNCOV
245
    return authEE as AuthEventEmitter;
×
246
};
247

248
/**
249
 *
250
 */
251
export function disableAutoLogin(): void {
16✔
UNCOV
252
    getEmbedConfig().autoLogin = false;
×
253
}
254

255
/**
256
 * Logs out from ThoughtSpot. This also sets the autoLogin flag to false, to
257
 * prevent the SDK from automatically logging in again.
258
 *
259
 * You can call the `init` method again to re login, if autoLogin is set to
260
 * true in this second call it will be honored.
261
 * @param doNotDisableAutoLogin This flag when passed will not disable autoLogin
262
 * @returns Promise which resolves when logout completes.
263
 * @version SDK: 1.10.1 | ThoughtSpot: 8.2.0.cl, 8.4.1-sw
264
 * @group Global methods
265
 */
266
export const logout = (doNotDisableAutoLogin = false): Promise<boolean> => {
16!
UNCOV
267
    if (!doNotDisableAutoLogin) {
×
UNCOV
268
        disableAutoLogin();
×
269
    }
UNCOV
270
    return _logout(getEmbedConfig()).then((isLoggedIn) => {
×
UNCOV
271
        notifyLogout();
×
UNCOV
272
        return isLoggedIn;
×
273
    });
274
};
275

276
let renderQueue: Promise<any> = Promise.resolve();
16✔
277

278
/**
279
 * Renders functions in a queue, resolves to next function only after the callback next
280
 * is called
281
 * @param fn The function being registered
282
 */
283
export const renderInQueue = (fn: (next?: (val?: any) => void) => Promise<any>): Promise<any> => {
16✔
284
    const { queueMultiRenders = false } = getEmbedConfig();
×
UNCOV
285
    if (queueMultiRenders) {
×
UNCOV
286
        renderQueue = renderQueue.then(() => new Promise((res) => fn(res)));
×
UNCOV
287
        return renderQueue;
×
288
    }
289
    // Sending an empty function to keep it consistent with the above usage.
UNCOV
290
    return fn(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
×
291
};
292

293
/**
294
 * Imports TML representation of the metadata objects into ThoughtSpot.
295
 * @param data
296
 * @returns imports TML data into ThoughtSpot
297
 * @example
298
 * ```js
299
 *  executeTML({
300
 * //Array of metadata Tmls in string format
301
 *      metadata_tmls: [
302
 *          "'\''{\"guid\":\"9bd202f5-d431-44bf-9a07-b4f7be372125\",
303
 *          \"liveboard\":{\"name\":\"Parameters Liveboard\"}}'\''"
304
 *      ],
305
 *      import_policy: 'PARTIAL', // Specifies the import policy for the TML import.
306
 *      create_new: false, // If selected, creates TML objects with new GUIDs.
307
 *  }).then(result => {
308
 *      console.log(result);
309
 *  }).catch(error => {
310
 *      console.error(error);
311
 *  });
312
 *```
313
 * @version SDK: 1.23.0 | ThoughtSpot: 9.4.0.cl
314
 * @group Global methods
315
 */
316
export const executeTML = async (data: executeTMLInput): Promise<any> => {
16✔
317
    try {
×
318
        sanity(getEmbedConfig());
×
319
    } catch (err) {
UNCOV
320
        return Promise.reject(err);
×
321
    }
322

323
    const { thoughtSpotHost, authType } = getEmbedConfig();
×
UNCOV
324
    const headers: Record<string, string | undefined> = {
×
325
        'Content-Type': 'application/json',
326
        'x-requested-by': 'ThoughtSpot',
327
    };
328

UNCOV
329
    const payload = {
×
330
        metadata_tmls: data.metadata_tmls,
331
        import_policy: data.import_policy || 'PARTIAL',
×
332
        create_new: data.create_new || false,
×
333
    };
UNCOV
334
    return tokenizedFetch(`${thoughtSpotHost}${EndPoints.EXECUTE_TML}`, {
×
335
        method: 'POST',
336
        headers,
337
        body: JSON.stringify(payload),
338
        credentials: 'include',
339
    })
340
        .then((response) => {
UNCOV
341
            if (!response.ok) {
×
UNCOV
342
                throw new Error(
×
343
                    `Failed to import TML data: ${response.status} - ${response.statusText}`,
344
                );
345
            }
UNCOV
346
            return response.json();
×
347
        })
348
        .catch((error) => {
UNCOV
349
            throw error;
×
350
        });
351
};
352

353
/**
354
 * Exports TML representation of the metadata objects from ThoughtSpot in JSON or YAML
355
 * format.
356
 * @param data
357
 * @returns exports TML data
358
 * @example
359
 * ```js
360
 * exportTML({
361
 *   metadata: [
362
 *     {
363
 *       type: "LIVEBOARD", //Metadata Type
364
 *       identifier: "9bd202f5-d431-44bf-9a07-b4f7be372125" //Metadata Id
365
 *     }
366
 *   ],
367
 *   export_associated: false,//indicates whether to export associated metadata objects
368
 *   export_fqn: false, //Adds FQNs of the referenced objects.For example, if you are
369
 *                      //exporting a Liveboard and its associated objects, the API
370
 *                      //returns the Liveboard TML data with the FQNs of the referenced
371
 *                      //worksheet. If the exported TML data includes FQNs, you don't need
372
 *                      //to manually add FQNs of the referenced objects during TML import.
373
 *   edoc_format: "JSON" //It takes JSON or YAML value
374
 * }).then(result => {
375
 *   console.log(result);
376
 * }).catch(error => {
377
 *   console.error(error);
378
 * });
379
 * ```
380
 * @version SDK: 1.23.0 | ThoughtSpot: 9.4.0.cl
381
 * @group Global methods
382
 */
383
export const exportTML = async (data: exportTMLInput): Promise<any> => {
16✔
384
    const { thoughtSpotHost, authType } = getEmbedConfig();
×
UNCOV
385
    try {
×
UNCOV
386
        sanity(getEmbedConfig());
×
387
    } catch (err) {
UNCOV
388
        return Promise.reject(err);
×
389
    }
UNCOV
390
    const payload = {
×
391
        metadata: data.metadata,
392
        export_associated: data.export_associated || false,
×
393
        export_fqn: data.export_fqn || false,
×
394
        edoc_format: data.edoc_format || 'YAML',
×
395
    };
396

UNCOV
397
    const headers: Record<string, string | undefined> = {
×
398
        'Content-Type': 'application/json',
399
        'x-requested-by': 'ThoughtSpot',
400
    };
401

UNCOV
402
    return tokenizedFetch(`${thoughtSpotHost}${EndPoints.EXPORT_TML}`, {
×
403
        method: 'POST',
404
        headers,
405
        body: JSON.stringify(payload),
406
        credentials: 'include',
407
    })
408
        .then((response) => {
UNCOV
409
            if (!response.ok) {
×
UNCOV
410
                throw new Error(
×
411
                    `Failed to export TML: ${response.status} - ${response.statusText}`,
412
                );
413
            }
UNCOV
414
            return response.json();
×
415
        })
416
        .catch((error) => {
UNCOV
417
            throw error;
×
418
        });
419
};
420

421
// For testing purposes only
422
/**
423
 *
424
 */
425
export function reset(): void {
16✔
UNCOV
426
    setEmbedConfig({} as any);
×
UNCOV
427
    setAuthEE(null);
×
UNCOV
428
    authPromise = null;
×
429
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc