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

thoughtspot / visual-embed-sdk / #1809

02 May 2025 11:16AM UTC coverage: 93.618% (-0.3%) from 93.903%
#1809

Pull #200

yinstardev
util fn
Pull Request #200: Do not merge

1056 of 1211 branches covered (87.2%)

Branch coverage included in aggregate %.

11 of 14 new or added lines in 2 files covered. (78.57%)

5 existing lines in 1 file now uncovered.

2582 of 2675 relevant lines covered (96.52%)

67.07 hits per line

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

92.54
/src/utils.ts
1
/**
2
 * Copyright (c) 2023
3
 *
4
 * Common utility functions for ThoughtSpot Visual Embed SDK
5
 * @summary Utils
6
 * @author Ayon Ghosh <ayon.ghosh@thoughtspot.com>
7
 */
8

9
import merge from 'ts-deepmerge';
32✔
10
import {
11
    EmbedConfig,
12
    QueryParams,
13
    RuntimeFilter,
14
    CustomisationsInterface,
15
    DOMSelector,
16
    ViewConfig,
17
    RuntimeParameter,
18
} from './types';
19

20
/**
21
 * Construct a runtime filters query string from the given filters.
22
 * Refer to the following docs for more details on runtime filter syntax:
23
 * https://cloud-docs.thoughtspot.com/admin/ts-cloud/apply-runtime-filter.html
24
 * https://cloud-docs.thoughtspot.com/admin/ts-cloud/runtime-filter-operators.html
25
 * @param runtimeFilters
26
 */
27
export const getFilterQuery = (runtimeFilters: RuntimeFilter[]): string | null => {
32✔
28
    if (runtimeFilters && runtimeFilters.length) {
273✔
29
        const filters = runtimeFilters.map((filter, valueIndex) => {
18✔
30
            const index = valueIndex + 1;
20✔
31
            const filterExpr = [];
20✔
32
            filterExpr.push(`col${index}=${encodeURIComponent(filter.columnName)}`);
20✔
33
            filterExpr.push(`op${index}=${filter.operator}`);
20✔
34
            filterExpr.push(
20✔
35
                filter.values.map((value) => {
36
                    const encodedValue = typeof value === 'bigint' ? value.toString() : value;
21!
37
                    return `val${index}=${encodeURIComponent(String(encodedValue))}`;
21✔
38
                }).join('&'),
39
            );
40

41
            return filterExpr.join('&');
20✔
42
        });
43

44
        return `${filters.join('&')}`;
18✔
45
    }
46

47
    return null;
255✔
48
};
49

50
/**
51
 * Construct a runtime parameter override query string from the given option.
52
 * @param runtimeParameters
53
 */
54
export const getRuntimeParameters = (runtimeParameters: RuntimeParameter[]): string => {
32✔
55
    if (runtimeParameters && runtimeParameters.length) {
272✔
56
        const params = runtimeParameters.map((param, valueIndex) => {
7✔
57
            const index = valueIndex + 1;
9✔
58
            const filterExpr = [];
9✔
59
            filterExpr.push(`param${index}=${encodeURIComponent(param.name)}`);
9✔
60
            filterExpr.push(`paramVal${index}=${encodeURIComponent(param.value)}`);
9✔
61

62
            return filterExpr.join('&');
9✔
63
        });
64

65
        return `${params.join('&')}`;
7✔
66
    }
67

68
    return null;
265✔
69
};
70

71
/**
72
 * Convert a value to a string representation to be sent as a query
73
 * parameter to the ThoughtSpot app.
74
 * @param value Any parameter value
75
 */
76
const serializeParam = (value: any) => {
32✔
77
    // do not serialize primitive types
78
    if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
6,407✔
79
        return value;
6,122✔
80
    }
81

82
    return JSON.stringify(value);
285✔
83
};
84

85
/**
86
 * Convert a value to a string:
87
 * in case of an array, we convert it to CSV.
88
 * in case of any other type, we directly return the value.
89
 * @param value
90
 */
91
const paramToString = (value: any) => (Array.isArray(value) ? value.join(',') : value);
32!
92

93
/**
94
 * Return a query param string composed from the given params object
95
 * @param queryParams
96
 * @param shouldSerializeParamValues
97
 */
98
export const getQueryParamString = (
32✔
99
    queryParams: QueryParams,
100
    shouldSerializeParamValues = false,
9✔
101
): string => {
102
    const qp: string[] = [];
288✔
103
    const params = Object.keys(queryParams);
288✔
104
    params.forEach((key) => {
288✔
105
        const val = queryParams[key];
6,699✔
106
        if (val !== undefined) {
6,699✔
107
            const serializedValue = shouldSerializeParamValues
6,424✔
108
                ? serializeParam(val)
109
                : paramToString(val);
110
            qp.push(`${key}=${serializedValue}`);
6,424✔
111
        }
112
    });
113

114
    if (qp.length) {
288✔
115
        return qp.join('&');
287✔
116
    }
117

118
    return null;
1✔
119
};
120

121
/**
122
 * Get a string representation of a dimension value in CSS
123
 * If numeric, it is considered in pixels.
124
 * @param value
125
 */
126
export const getCssDimension = (value: number | string): string => {
32✔
127
    if (typeof value === 'number') {
536✔
128
        return `${value}px`;
363✔
129
    }
130

131
    return value;
173✔
132
};
133

134
export const getSSOMarker = (markerId: string) => {
32✔
135
    const encStringToAppend = encodeURIComponent(markerId);
17✔
136
    return `tsSSOMarker=${encStringToAppend}`;
17✔
137
};
138

139
/**
140
 * Append a string to a URL's hash fragment
141
 * @param url A URL
142
 * @param stringToAppend The string to append to the URL hash
143
 */
144
export const appendToUrlHash = (url: string, stringToAppend: string) => {
32✔
145
    let outputUrl = url;
12✔
146
    const encStringToAppend = encodeURIComponent(stringToAppend);
12✔
147

148
    const marker = `tsSSOMarker=${encStringToAppend}`;
12✔
149

150
    let splitAdder = '';
12✔
151

152
    if (url.indexOf('#') >= 0) {
12✔
153
        // If second half of hash contains a '?' already add a '&' instead of
154
        // '?' which appends to query params.
155
        splitAdder = url.split('#')[1].indexOf('?') >= 0 ? '&' : '?';
8✔
156
    } else {
157
        splitAdder = '#?';
4✔
158
    }
159
    outputUrl = `${outputUrl}${splitAdder}${marker}`;
12✔
160

161
    return outputUrl;
12✔
162
};
163

164
/**
165
 *
166
 * @param url
167
 * @param stringToAppend
168
 * @param path
169
 */
170
export function getRedirectUrl(url: string, stringToAppend: string, path = '') {
32✔
171
    const targetUrl = path ? new URL(path, window.location.origin).href : url;
10✔
172
    return appendToUrlHash(targetUrl, stringToAppend);
10✔
173
}
174

175
export const getEncodedQueryParamsString = (queryString: string) => {
32✔
176
    if (!queryString) {
4✔
177
        return queryString;
1✔
178
    }
179
    return btoa(queryString).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
3✔
180
};
181

182
export const getOffsetTop = (element: any) => {
32✔
183
    const rect = element.getBoundingClientRect();
4✔
184
    return rect.top + window.scrollY;
4✔
185
};
186

187
export const embedEventStatus = {
32✔
188
    START: 'start',
189
    END: 'end',
190
};
191

192
export const setAttributes = (
32✔
193
    element: HTMLElement,
194
    attributes: { [key: string]: string | number | boolean },
195
): void => {
196
    Object.keys(attributes).forEach((key) => {
266✔
197
        element.setAttribute(key, attributes[key].toString());
11✔
198
    });
199
};
200

201
const isCloudRelease = (version: string) => version.endsWith('.cl');
35✔
202

203
/* For Search Embed: ReleaseVersionInBeta */
204
export const checkReleaseVersionInBeta = (
32✔
205
    releaseVersion: string,
206
    suppressBetaWarning: boolean,
207
): boolean => {
208
    if (releaseVersion !== '' && !isCloudRelease(releaseVersion)) {
80✔
209
        const splittedReleaseVersion = releaseVersion.split('.');
34✔
210
        const majorVersion = Number(splittedReleaseVersion[0]);
34✔
211
        const isBetaVersion = majorVersion < 8;
34✔
212
        return !suppressBetaWarning && isBetaVersion;
34✔
213
    }
214
    return false;
46✔
215
};
216

217
export const getCustomisations = (
32✔
218
    embedConfig: EmbedConfig,
219
    viewConfig: ViewConfig,
220
): CustomisationsInterface => {
221
    const customizationsFromViewConfig = viewConfig.customizations;
17✔
222
    const customizationsFromEmbedConfig = embedConfig.customizations
17✔
223
        || ((embedConfig as any).customisations as CustomisationsInterface);
224

225
    const customizations: CustomisationsInterface = {
17✔
226
        style: {
227
            ...customizationsFromEmbedConfig?.style,
51✔
228
            ...customizationsFromViewConfig?.style,
51✔
229
            customCSS: {
230
                ...customizationsFromEmbedConfig?.style?.customCSS,
102✔
231
                ...customizationsFromViewConfig?.style?.customCSS,
102✔
232
            },
233
            customCSSUrl:
234
                customizationsFromViewConfig?.style?.customCSSUrl
136✔
235
                || customizationsFromEmbedConfig?.style?.customCSSUrl,
102✔
236
        },
237
        content: {
238
            ...customizationsFromEmbedConfig?.content,
51✔
239
            ...customizationsFromViewConfig?.content,
51✔
240
        },
241
    };
242
    return customizations;
17✔
243
};
244

245
export const getRuntimeFilters = (runtimefilters: any) => getFilterQuery(runtimefilters || []);
32!
246

247
/**
248
 * Gets a reference to the DOM node given
249
 * a selector.
250
 * @param domSelector
251
 */
252
export function getDOMNode(domSelector: DOMSelector): HTMLElement {
32✔
253
    return typeof domSelector === 'string' ? document.querySelector(domSelector) : domSelector;
289✔
254
}
255

256
export const deepMerge = (target: any, source: any) => merge(target, source);
32✔
257

258
export const getOperationNameFromQuery = (query: string) => {
32✔
259
    const regex = /(?:query|mutation)\s+(\w+)/;
22✔
260
    const matches = query.match(regex);
22✔
261
    return matches?.[1];
22!
262
};
263

264
/**
265
 *
266
 * @param obj
267
 */
268
export function removeTypename(obj: any) {
32✔
269
    if (!obj || typeof obj !== 'object') return obj;
43✔
270

271
    // eslint-disable-next-line no-restricted-syntax
272
    for (const key in obj) {
36✔
273
        if (key === '__typename') {
90✔
274
            delete obj[key];
2✔
275
        } else if (typeof obj[key] === 'object') {
88✔
276
            removeTypename(obj[key]);
18✔
277
        }
278
    }
279
    return obj;
36✔
280
}
281

282
/**
283
 * Sets the specified style properties on an HTML element.
284
 * @param {HTMLElement} element - The HTML element to which the styles should be applied.
285
 * @param {Partial<CSSStyleDeclaration>} styleProperties - An object containing style
286
 * property names and their values.
287
 * @example
288
 * // Apply styles to an element
289
 * const element = document.getElementById('myElement');
290
 * const styles = {
291
 *   backgroundColor: 'red',
292
 *   fontSize: '16px',
293
 * };
294
 * setStyleProperties(element, styles);
295
 */
296
export const setStyleProperties = (
32✔
297
    element: HTMLElement,
298
    styleProperties: Partial<CSSStyleDeclaration>,
299
): void => {
300
    if (!element?.style) return;
30✔
301
    Object.keys(styleProperties).forEach((styleProperty) => {
29✔
302
        element.style[styleProperty] = styleProperties[styleProperty].toString();
100✔
303
    });
304
};
305
/**
306
 * Removes specified style properties from an HTML element.
307
 * @param {HTMLElement} element - The HTML element from which the styles should be removed.
308
 * @param {string[]} styleProperties - An array of style property names to be removed.
309
 * @example
310
 * // Remove styles from an element
311
 * const element = document.getElementById('myElement');
312
 * element.style.backgroundColor = 'red';
313
 * const propertiesToRemove = ['backgroundColor'];
314
 * removeStyleProperties(element, propertiesToRemove);
315
 */
316
export const removeStyleProperties = (element: HTMLElement, styleProperties: string[]): void => {
32✔
317
    if (!element?.style) return;
8✔
318
    styleProperties.forEach((styleProperty) => {
7✔
319
        element.style.removeProperty(styleProperty);
19✔
320
    });
321
};
322

323
export const isUndefined = (value: any): boolean => value === undefined;
123✔
324

325
// Return if the value is a string, double or boolean.
326
export const getTypeFromValue = (value: any): [string, string] => {
32✔
327
    if (typeof value === 'string') {
1!
328
        return ['char', 'string'];
×
329
    }
330
    if (typeof value === 'number') {
1✔
331
        return ['double', 'double'];
1✔
332
    }
333
    if (typeof value === 'boolean') {
×
334
        return ['boolean', 'boolean'];
×
335
    }
336
    return ['', ''];
×
337
};
338

339
const sdkWindowKey = '_tsEmbedSDK' as any;
32✔
340

341
/**
342
 * Stores a value in the global `window` object under the `_tsEmbedSDK` namespace.
343
 * @param key - The key under which the value will be stored.
344
 * @param value - The value to store.
345
 * @param options - Additional options.
346
 * @param options.ignoreIfAlreadyExists - Does not set if value for key is set.
347
 *
348
 * @returns The stored value.
349
 *
350
 * @version SDK: 1.36.2 | ThoughtSpot: *
351
 */
352
export function storeValueInWindow<T>(
32✔
353
    key: string,
354
    value: T,
355
    options: { ignoreIfAlreadyExists?: boolean } = {},
295✔
356
): T {
357
    if (!isBrowser()) {
314!
NEW
358
        return value;
×
359
    }
360

361
    if (!window[sdkWindowKey]) {
314✔
362
        (window as any)[sdkWindowKey] = {};
22✔
363
    }
364

365
    if (options.ignoreIfAlreadyExists && key in (window as any)[sdkWindowKey]) {
314!
366
        return (window as any)[sdkWindowKey][key];
×
367
    }
368

369
    (window as any)[sdkWindowKey][key] = value;
314✔
370
    return value;
314✔
371
}
372

373
/**
374
 * Retrieves a stored value from the global `window` object under the `_tsEmbedSDK` namespace.
375
 * @param key - The key whose value needs to be retrieved.
376
 * @returns The stored value or `undefined` if the key is not found.
377
 */
378
export const getValueFromWindow = <T = any>(key: string): T | undefined => {
32✔
379
    if (!isBrowser()) {
1,820!
NEW
380
        return undefined;
×
381
    }
382
    return (window as any)?.[sdkWindowKey]?.[key];
1,820!
383
};
384

385
/**
386
 * Resets the key if it exists in the `window` object under the `_tsEmbedSDK` key.
387
 * Returns true if the key was reset, false otherwise.
388
 * @param key - Key to reset
389
 * @returns - boolean indicating if the key was reset
390
 */
391
export function resetValueFromWindow(key: string): boolean {
32✔
392
    if (!isBrowser()) {
3!
NEW
393
        return false;
×
394
    }
395
    if (key in window[sdkWindowKey]) {
3✔
396
        delete (window as any)[sdkWindowKey][key];
3✔
397
        return true;
3✔
398
    }
399
    return false;
×
400
}
401

402
export const isBrowser = () => typeof window !== 'undefined';
2,218✔
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