@thoughtspot/visual-embed-sdk 1.47.3 → 1.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/package.json +1 -1
- package/cjs/src/auth.d.ts.map +1 -1
- package/cjs/src/auth.js +11 -1
- package/cjs/src/auth.js.map +1 -1
- package/cjs/src/auth.spec.js +38 -0
- package/cjs/src/auth.spec.js.map +1 -1
- package/cjs/src/authToken.d.ts +2 -0
- package/cjs/src/authToken.d.ts.map +1 -1
- package/cjs/src/authToken.js +7 -5
- package/cjs/src/authToken.js.map +1 -1
- package/cjs/src/embed/app.d.ts +1 -1
- package/cjs/src/embed/app.js +1 -1
- package/cjs/src/embed/app.js.map +1 -1
- package/cjs/src/embed/app.spec.js +9 -0
- package/cjs/src/embed/app.spec.js.map +1 -1
- package/cjs/src/embed/liveboard.d.ts +1 -1
- package/cjs/src/embed/liveboard.js +1 -1
- package/cjs/src/embed/liveboard.js.map +1 -1
- package/cjs/src/embed/liveboard.spec.js +10 -0
- package/cjs/src/embed/liveboard.spec.js.map +1 -1
- package/cjs/src/embed/ts-embed.js +2 -2
- package/cjs/src/embed/ts-embed.js.map +1 -1
- package/cjs/src/embed/ts-embed.spec.js +29 -0
- package/cjs/src/embed/ts-embed.spec.js.map +1 -1
- package/cjs/src/tokenizedFetch.d.ts.map +1 -1
- package/cjs/src/tokenizedFetch.js +12 -9
- package/cjs/src/tokenizedFetch.js.map +1 -1
- package/cjs/src/tokenizedFetch.spec.d.ts +2 -0
- package/cjs/src/tokenizedFetch.spec.d.ts.map +1 -0
- package/cjs/src/tokenizedFetch.spec.js +68 -0
- package/cjs/src/tokenizedFetch.spec.js.map +1 -0
- package/cjs/src/types.d.ts +42 -13
- package/cjs/src/types.d.ts.map +1 -1
- package/cjs/src/types.js +28 -4
- package/cjs/src/types.js.map +1 -1
- package/dist/{index-DZq20cR6.js → index-Ck-r09gt.js} +1 -1
- package/dist/src/auth.d.ts.map +1 -1
- package/dist/src/authToken.d.ts +2 -0
- package/dist/src/authToken.d.ts.map +1 -1
- package/dist/src/embed/app.d.ts +1 -1
- package/dist/src/embed/liveboard.d.ts +1 -1
- package/dist/src/tokenizedFetch.d.ts.map +1 -1
- package/dist/src/tokenizedFetch.spec.d.ts +2 -0
- package/dist/src/tokenizedFetch.spec.d.ts.map +1 -0
- package/dist/src/types.d.ts +42 -13
- package/dist/src/types.d.ts.map +1 -1
- package/dist/tsembed-react.es.js +58 -21
- package/dist/tsembed-react.js +57 -20
- package/dist/tsembed.es.js +58 -21
- package/dist/tsembed.js +57 -20
- package/dist/visual-embed-sdk-react-full.d.ts +44 -15
- package/dist/visual-embed-sdk-react.d.ts +44 -15
- package/dist/visual-embed-sdk.d.ts +44 -15
- package/lib/package.json +1 -1
- package/lib/src/auth.d.ts.map +1 -1
- package/lib/src/auth.js +12 -2
- package/lib/src/auth.js.map +1 -1
- package/lib/src/auth.spec.js +38 -0
- package/lib/src/auth.spec.js.map +1 -1
- package/lib/src/authToken.d.ts +2 -0
- package/lib/src/authToken.d.ts.map +1 -1
- package/lib/src/authToken.js +2 -2
- package/lib/src/authToken.js.map +1 -1
- package/lib/src/embed/app.d.ts +1 -1
- package/lib/src/embed/app.js +1 -1
- package/lib/src/embed/app.js.map +1 -1
- package/lib/src/embed/app.spec.js +9 -0
- package/lib/src/embed/app.spec.js.map +1 -1
- package/lib/src/embed/liveboard.d.ts +1 -1
- package/lib/src/embed/liveboard.js +1 -1
- package/lib/src/embed/liveboard.js.map +1 -1
- package/lib/src/embed/liveboard.spec.js +10 -0
- package/lib/src/embed/liveboard.spec.js.map +1 -1
- package/lib/src/embed/ts-embed.js +2 -2
- package/lib/src/embed/ts-embed.js.map +1 -1
- package/lib/src/embed/ts-embed.spec.js +29 -0
- package/lib/src/embed/ts-embed.spec.js.map +1 -1
- package/lib/src/tokenizedFetch.d.ts.map +1 -1
- package/lib/src/tokenizedFetch.js +13 -10
- package/lib/src/tokenizedFetch.js.map +1 -1
- package/lib/src/tokenizedFetch.spec.d.ts +2 -0
- package/lib/src/tokenizedFetch.spec.d.ts.map +1 -0
- package/lib/src/tokenizedFetch.spec.js +65 -0
- package/lib/src/tokenizedFetch.spec.js.map +1 -0
- package/lib/src/types.d.ts +42 -13
- package/lib/src/types.d.ts.map +1 -1
- package/lib/src/types.js +28 -4
- package/lib/src/types.js.map +1 -1
- package/lib/src/visual-embed-sdk.d.ts +44 -15
- package/package.json +1 -1
- package/src/auth.spec.ts +55 -1
- package/src/auth.ts +11 -2
- package/src/authToken.ts +2 -2
- package/src/embed/app.spec.ts +13 -0
- package/src/embed/app.ts +2 -2
- package/src/embed/liveboard.spec.ts +14 -0
- package/src/embed/liveboard.ts +4 -4
- package/src/embed/ts-embed.spec.ts +35 -0
- package/src/embed/ts-embed.ts +1 -1
- package/src/tokenizedFetch.spec.ts +81 -0
- package/src/tokenizedFetch.ts +14 -11
- package/src/types.ts +42 -13
|
@@ -1902,9 +1902,10 @@ export interface BaseViewConfig extends ApiInterceptFlags {
|
|
|
1902
1902
|
styleSheet__unstable?: string;
|
|
1903
1903
|
/**
|
|
1904
1904
|
* The list of actions to disable from the primary menu, more menu
|
|
1905
|
-
* (...), and the contextual menu.
|
|
1906
|
-
*
|
|
1907
|
-
* Use this to disable
|
|
1905
|
+
* (...), and the contextual menu. Disabled actions are grayed out
|
|
1906
|
+
* and still visible to the user, but cannot be clicked.
|
|
1907
|
+
* Use this when you want to disable an action (keep it visible but non-interactive).
|
|
1908
|
+
* To completely remove an action from the UI, use {@link hiddenActions} instead.
|
|
1908
1909
|
*
|
|
1909
1910
|
* Supported embed types: `AppEmbed`, `LiveboardEmbed`, `SearchEmbed`, `SpotterAgentEmbed`, `SpotterEmbed`, `SearchBarEmbed`
|
|
1910
1911
|
* @version SDK: 1.6.0 | ThoughtSpot: ts8.nov.cl, 8.4.1.sw
|
|
@@ -1935,9 +1936,10 @@ export interface BaseViewConfig extends ApiInterceptFlags {
|
|
|
1935
1936
|
*/
|
|
1936
1937
|
disabledActionReason?: string;
|
|
1937
1938
|
/**
|
|
1938
|
-
* The list of actions to
|
|
1939
|
-
*
|
|
1940
|
-
* Use this to
|
|
1939
|
+
* The list of actions to completely remove from the embedded view.
|
|
1940
|
+
* Hidden actions are not visible to the user at all (fully removed from the UI).
|
|
1941
|
+
* Use this when you want to remove an action entirely.
|
|
1942
|
+
* To keep an action visible but non-interactive (grayed out), use {@link disabledActions} instead.
|
|
1941
1943
|
*
|
|
1942
1944
|
* Supported embed types: `AppEmbed`, `LiveboardEmbed`, `SearchEmbed`, `SpotterAgentEmbed`, `SpotterEmbed`, `SearchBarEmbed`
|
|
1943
1945
|
* @version SDK: 1.6.0 | ThoughtSpot: ts8.nov.cl, 8.4.1.sw
|
|
@@ -1956,9 +1958,8 @@ export interface BaseViewConfig extends ApiInterceptFlags {
|
|
|
1956
1958
|
* The list of actions to display from the primary menu, more menu
|
|
1957
1959
|
* (...), and the contextual menu. These will be only actions that
|
|
1958
1960
|
* are visible to the user.
|
|
1959
|
-
* Use this
|
|
1960
|
-
*
|
|
1961
|
-
* Use either this or hiddenActions.
|
|
1961
|
+
* Use this as an allowlist — only the actions listed here will be shown.
|
|
1962
|
+
* All other actions will be hidden. Use either this or {@link hiddenActions}, not both.
|
|
1962
1963
|
*
|
|
1963
1964
|
* Supported embed types: `AppEmbed`, `LiveboardEmbed`, `SearchEmbed`, `SpotterAgentEmbed`, `SpotterEmbed`, `SearchBarEmbed`
|
|
1964
1965
|
* @version SDK: 1.6.0 | ThoughtSpot: ts8.nov.cl, 8.4.1.sw
|
|
@@ -2412,6 +2413,7 @@ export interface BaseViewConfig extends ApiInterceptFlags {
|
|
|
2412
2413
|
/**
|
|
2413
2414
|
* Refresh the auth token when the token is near expiry.
|
|
2414
2415
|
* @version SDK: 1.45.2 | ThoughtSpot: 26.3.0.cl
|
|
2416
|
+
* @default true
|
|
2415
2417
|
* @example
|
|
2416
2418
|
* ```js
|
|
2417
2419
|
* const embed = new AppEmbed('#tsEmbed', {
|
|
@@ -6719,10 +6721,14 @@ export enum DataSourceVisualMode {
|
|
|
6719
6721
|
/**
|
|
6720
6722
|
* ThoughtSpot application pages include actions and menu commands
|
|
6721
6723
|
* for various user-initiated operations. These actions are represented
|
|
6722
|
-
* as enumeration members in the SDK. To
|
|
6723
|
-
*
|
|
6724
|
-
*
|
|
6725
|
-
*
|
|
6724
|
+
* as enumeration members in the SDK. To control actions in the embedded view:
|
|
6725
|
+
* - disabledActions — the action is grayed out and still visible, but non-interactive (user can see but not click).
|
|
6726
|
+
* - hiddenActions — the action is completely removed from the UI (user cannot see it at all).
|
|
6727
|
+
* - visibleActions — allowlist, only these actions are shown; all others are hidden.
|
|
6728
|
+
*
|
|
6729
|
+
* Use disabledActions to disable (gray out) an action.
|
|
6730
|
+
* Use hiddenActions to hide (fully remove) an action.
|
|
6731
|
+
* Use visibleActions to show only specific actions.
|
|
6726
6732
|
* @example
|
|
6727
6733
|
* ```js
|
|
6728
6734
|
* const embed = new LiveboardEmbed('#tsEmbed', {
|
|
@@ -7569,6 +7575,26 @@ export enum Action {
|
|
|
7569
7575
|
* @version SDK: 1.21.0 | ThoughtSpot: 9.2.0.cl, 9.5.1.sw
|
|
7570
7576
|
*/
|
|
7571
7577
|
AxisMenuRemove = "axisMenuRemove",
|
|
7578
|
+
/**
|
|
7579
|
+
* The **Compare with** action in the chart axis customization menu.
|
|
7580
|
+
* Allows comparing data across dimensions or measures.
|
|
7581
|
+
* @example
|
|
7582
|
+
* ```js
|
|
7583
|
+
* disabledActions: [Action.AxisMenuCompare]
|
|
7584
|
+
* ```
|
|
7585
|
+
* @version SDK: 1.50.0 | ThoughtSpot: 26.7.0.cl
|
|
7586
|
+
*/
|
|
7587
|
+
AxisMenuCompare = "axisMenuCompare",
|
|
7588
|
+
/**
|
|
7589
|
+
* The **Merge with** action in the chart axis customization menu.
|
|
7590
|
+
* Allows merging data across dimensions or measures.
|
|
7591
|
+
* @example
|
|
7592
|
+
* ```js
|
|
7593
|
+
* disabledActions: [Action.AxisMenuMerge]
|
|
7594
|
+
* ```
|
|
7595
|
+
* @version SDK: 1.50.0 | ThoughtSpot: 26.7.0.cl
|
|
7596
|
+
*/
|
|
7597
|
+
AxisMenuMerge = "axisMenuMerge",
|
|
7572
7598
|
/**
|
|
7573
7599
|
* @hidden
|
|
7574
7600
|
*/
|
|
@@ -8791,6 +8817,9 @@ export interface DefaultAppInitData {
|
|
|
8791
8817
|
customActions: CustomAction[];
|
|
8792
8818
|
interceptTimeout: number | undefined;
|
|
8793
8819
|
interceptUrls: (string | InterceptedApiType)[];
|
|
8820
|
+
embedExpiryInAuthToken: boolean;
|
|
8821
|
+
shouldBypassPayloadValidation?: boolean;
|
|
8822
|
+
useHostEventsV2?: boolean;
|
|
8794
8823
|
}
|
|
8795
8824
|
/**
|
|
8796
8825
|
* Enum for the type of API intercepted
|
|
@@ -11043,7 +11072,7 @@ export interface AppViewConfig extends AllEmbedViewConfig {
|
|
|
11043
11072
|
/**
|
|
11044
11073
|
* Enables the 'what you see is what you get' PDF export for Liveboards. Each tab is rendered on a single page
|
|
11045
11074
|
* following the exact UI layout, instead of splitting visualizations across multiple A4 pages.
|
|
11046
|
-
* This feature is GA from version 26.5.0.cl
|
|
11075
|
+
* This feature is GA from version 26.5.0.cl. It is disabled by default in embed deployments.
|
|
11047
11076
|
*
|
|
11048
11077
|
* Supported embed types: `AppEmbed`, `LiveboardEmbed`
|
|
11049
11078
|
* @type {boolean}
|
|
@@ -11986,7 +12015,7 @@ export interface LiveboardViewConfig extends BaseViewConfig, SearchLiveboardComm
|
|
|
11986
12015
|
/**
|
|
11987
12016
|
* Enables the 'what you see is what you get' PDF export for Liveboards. Each tab is rendered on a single page
|
|
11988
12017
|
* following the exact UI layout, instead of splitting visualizations across multiple A4 pages.
|
|
11989
|
-
* This feature is GA from version 26.5.0.cl
|
|
12018
|
+
* This feature is GA from version 26.5.0.cl. It is disabled by default in embed deployments.
|
|
11990
12019
|
*
|
|
11991
12020
|
* Supported embed types: `AppEmbed`, `LiveboardEmbed`
|
|
11992
12021
|
* @type {boolean}
|
package/package.json
CHANGED
package/src/auth.spec.ts
CHANGED
|
@@ -450,7 +450,7 @@ describe('Unit test for auth', () => {
|
|
|
450
450
|
|
|
451
451
|
it('should support emitting SAML_POPUP_CLOSED_NO_AUTH event', () => {
|
|
452
452
|
const emitSpy = jest.fn();
|
|
453
|
-
const mockEventEmitter = {
|
|
453
|
+
const mockEventEmitter = {
|
|
454
454
|
emit: emitSpy,
|
|
455
455
|
once: jest.fn()
|
|
456
456
|
};
|
|
@@ -460,6 +460,60 @@ describe('Unit test for auth', () => {
|
|
|
460
460
|
authInstance.setAuthEE(null);
|
|
461
461
|
});
|
|
462
462
|
|
|
463
|
+
it('should set loggedInStatus from cachedAuthToken without calling isLoggedIn again after popup flow', async () => {
|
|
464
|
+
Object.defineProperty(window, 'location', { value: { href: '', hash: '' } });
|
|
465
|
+
checkReleaseVersionInBetaInstance.storeValueInWindow('cachedAuthToken', 'test-cached-token');
|
|
466
|
+
(authInstance as any).samlCompletionPromise = Promise.resolve();
|
|
467
|
+
global.window.open = jest.fn();
|
|
468
|
+
|
|
469
|
+
jest.spyOn(tokenAuthService, 'isActiveService')
|
|
470
|
+
.mockReturnValueOnce(Promise.resolve(false));
|
|
471
|
+
|
|
472
|
+
await authInstance.doSamlAuth({ ...embedConfig.doSamlAuthNoRedirect });
|
|
473
|
+
|
|
474
|
+
expect(authInstance.loggedInStatus).toBe(true);
|
|
475
|
+
expect(tokenAuthService.isActiveService).toHaveBeenCalledTimes(1);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('should store decoded accessToken in window when SAMLComplete event includes accessToken', async () => {
|
|
479
|
+
Object.defineProperty(window, 'location', { value: { href: '', hash: '' } });
|
|
480
|
+
global.window.open = jest.fn().mockReturnValue({ closed: false, focus: jest.fn(), close: jest.fn() });
|
|
481
|
+
|
|
482
|
+
(authInstance as any).samlCompletionPromise = null;
|
|
483
|
+
|
|
484
|
+
let capturedMessageHandler: ((e: any) => void) | null = null;
|
|
485
|
+
jest.spyOn(window, 'addEventListener').mockImplementation((type: any, handler: any) => {
|
|
486
|
+
if (type === 'message') {
|
|
487
|
+
capturedMessageHandler = handler;
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
jest.spyOn(tokenAuthService, 'isActiveService')
|
|
492
|
+
.mockReturnValueOnce(Promise.resolve(false));
|
|
493
|
+
|
|
494
|
+
const authPromise = authInstance.doSamlAuth({ ...embedConfig.doSamlAuthNoRedirect });
|
|
495
|
+
|
|
496
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
497
|
+
|
|
498
|
+
expect(capturedMessageHandler).not.toBeNull();
|
|
499
|
+
|
|
500
|
+
const accessToken = 'my-access-token';
|
|
501
|
+
capturedMessageHandler!({
|
|
502
|
+
data: {
|
|
503
|
+
type: EmbedEvent.SAMLComplete,
|
|
504
|
+
accessToken: encodeURIComponent(accessToken),
|
|
505
|
+
},
|
|
506
|
+
source: { close: jest.fn() },
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
await authPromise;
|
|
510
|
+
|
|
511
|
+
expect(
|
|
512
|
+
checkReleaseVersionInBetaInstance.getValueFromWindow('cachedAuthToken'),
|
|
513
|
+
).toBe(accessToken);
|
|
514
|
+
expect(authInstance.loggedInStatus).toBe(true);
|
|
515
|
+
});
|
|
516
|
+
|
|
463
517
|
});
|
|
464
518
|
|
|
465
519
|
describe('doOIDCAuth', () => {
|
package/src/auth.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import EventEmitter from 'eventemitter3';
|
|
2
|
-
import { getAuthenticationToken } from './authToken';
|
|
2
|
+
import { getAuthenticationToken, storeAuthTokenInCache, getCacheAuthToken } from './authToken';
|
|
3
3
|
import { getEmbedConfig } from './embed/embedConfig';
|
|
4
4
|
import { initMixpanel } from './mixpanel-service';
|
|
5
5
|
import {
|
|
@@ -462,6 +462,10 @@ async function samlPopupFlow(ssoURL: string, triggerContainer: DOMSelector, trig
|
|
|
462
462
|
samlCompletionPromise = samlCompletionPromise || new Promise<void>((resolve, reject) => {
|
|
463
463
|
window.addEventListener('message', (e) => {
|
|
464
464
|
if (e.data.type === EmbedEvent.SAMLComplete) {
|
|
465
|
+
if (e.data.accessToken) {
|
|
466
|
+
const decodedToken = decodeURIComponent(e.data.accessToken);
|
|
467
|
+
storeAuthTokenInCache(decodedToken);
|
|
468
|
+
}
|
|
465
469
|
samlCompletionResolved = true;
|
|
466
470
|
if (popupClosedCheck) {
|
|
467
471
|
clearInterval(popupClosedCheck);
|
|
@@ -503,7 +507,12 @@ const doSSOAuth = async (embedConfig: EmbedConfig, ssoEndPoint: string): Promise
|
|
|
503
507
|
const ssoURL = `${thoughtSpotHost}${ssoEndPoint}`;
|
|
504
508
|
if (embedConfig.inPopup) {
|
|
505
509
|
await samlPopupFlow(ssoURL, embedConfig.authTriggerContainer, embedConfig.authTriggerText);
|
|
506
|
-
|
|
510
|
+
const cachedToken = getCacheAuthToken();
|
|
511
|
+
if (cachedToken) {
|
|
512
|
+
loggedInStatus = true;
|
|
513
|
+
} else {
|
|
514
|
+
loggedInStatus = await isLoggedIn(thoughtSpotHost);
|
|
515
|
+
}
|
|
507
516
|
return;
|
|
508
517
|
}
|
|
509
518
|
|
package/src/authToken.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { logger } from './utils/logger';
|
|
|
6
6
|
|
|
7
7
|
const cacheAuthTokenKey = 'cachedAuthToken';
|
|
8
8
|
|
|
9
|
-
const getCacheAuthToken = (): string | null => getValueFromWindow(cacheAuthTokenKey);
|
|
10
|
-
const storeAuthTokenInCache = (token: string): void => {
|
|
9
|
+
export const getCacheAuthToken = (): string | null => getValueFromWindow(cacheAuthTokenKey);
|
|
10
|
+
export const storeAuthTokenInCache = (token: string): void => {
|
|
11
11
|
storeValueInWindow(cacheAuthTokenKey, token);
|
|
12
12
|
};
|
|
13
13
|
|
package/src/embed/app.spec.ts
CHANGED
|
@@ -480,6 +480,19 @@ describe('App embed tests', () => {
|
|
|
480
480
|
});
|
|
481
481
|
});
|
|
482
482
|
|
|
483
|
+
test('should disable isWYSIWYGLiveboardPDFEnabled by default in url', async () => {
|
|
484
|
+
const appEmbed = new AppEmbed(getRootEl(), {
|
|
485
|
+
...defaultViewConfig,
|
|
486
|
+
} as AppViewConfig);
|
|
487
|
+
appEmbed.render();
|
|
488
|
+
await executeAfterWait(() => {
|
|
489
|
+
expectUrlMatchesWithParams(
|
|
490
|
+
getIFrameSrc(),
|
|
491
|
+
`http://${thoughtSpotHost}/?embedApp=true&profileAndHelpInNavBarHidden=false&isWYSIWYGLiveboardPDFEnabled=false${defaultParamsPost}#/home`,
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
483
496
|
test('should set isLinkParametersEnabled to true in url', async () => {
|
|
484
497
|
const appEmbed = new AppEmbed(getRootEl(), {
|
|
485
498
|
...defaultViewConfig,
|
package/src/embed/app.ts
CHANGED
|
@@ -590,7 +590,7 @@ export interface AppViewConfig extends AllEmbedViewConfig {
|
|
|
590
590
|
/**
|
|
591
591
|
* Enables the 'what you see is what you get' PDF export for Liveboards. Each tab is rendered on a single page
|
|
592
592
|
* following the exact UI layout, instead of splitting visualizations across multiple A4 pages.
|
|
593
|
-
* This feature is GA from version 26.5.0.cl
|
|
593
|
+
* This feature is GA from version 26.5.0.cl. It is disabled by default in embed deployments.
|
|
594
594
|
*
|
|
595
595
|
* Supported embed types: `AppEmbed`, `LiveboardEmbed`
|
|
596
596
|
* @type {boolean}
|
|
@@ -914,7 +914,7 @@ export class AppEmbed extends V1Embed {
|
|
|
914
914
|
minimumHeight,
|
|
915
915
|
isThisPeriodInDateFiltersEnabled,
|
|
916
916
|
enableHomepageAnnouncement = false,
|
|
917
|
-
isContinuousLiveboardPDFEnabled,
|
|
917
|
+
isContinuousLiveboardPDFEnabled = false,
|
|
918
918
|
enableLiveboardDataCache,
|
|
919
919
|
} = this.viewConfig;
|
|
920
920
|
|
|
@@ -275,6 +275,20 @@ describe('Liveboard/viz embed tests', () => {
|
|
|
275
275
|
});
|
|
276
276
|
});
|
|
277
277
|
|
|
278
|
+
test('should disable isWYSIWYGLiveboardPDFEnabled by default in url', async () => {
|
|
279
|
+
const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
|
|
280
|
+
...defaultViewConfig,
|
|
281
|
+
liveboardId,
|
|
282
|
+
} as LiveboardViewConfig);
|
|
283
|
+
liveboardEmbed.render();
|
|
284
|
+
await executeAfterWait(() => {
|
|
285
|
+
expectUrlMatchesWithParams(
|
|
286
|
+
getIFrameSrc(),
|
|
287
|
+
`http://${thoughtSpotHost}/?embedApp=true${defaultParams}&isWYSIWYGLiveboardPDFEnabled=false${prefixParams}#/embed/viz/${liveboardId}`,
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
278
292
|
test('should set isLiveboardXLSXCSVDownloadEnabled to true in url', async () => {
|
|
279
293
|
const liveboardEmbed = new LiveboardEmbed(getRootEl(), {
|
|
280
294
|
isLiveboardXLSXCSVDownloadEnabled: true,
|
package/src/embed/liveboard.ts
CHANGED
|
@@ -388,9 +388,9 @@ export interface LiveboardViewConfig extends BaseViewConfig, LiveboardOtherViewC
|
|
|
388
388
|
*/
|
|
389
389
|
isPNGInScheduledEmailsEnabled?: boolean;
|
|
390
390
|
/**
|
|
391
|
-
* Enables the 'what you see is what you get' PDF export for Liveboards. Each tab is rendered on a single page
|
|
392
|
-
* following the exact UI layout, instead of splitting visualizations across multiple A4 pages.
|
|
393
|
-
* This feature is GA from version 26.5.0.cl
|
|
391
|
+
* Enables the 'what you see is what you get' PDF export for Liveboards. Each tab is rendered on a single page
|
|
392
|
+
* following the exact UI layout, instead of splitting visualizations across multiple A4 pages.
|
|
393
|
+
* This feature is GA from version 26.5.0.cl. It is disabled by default in embed deployments.
|
|
394
394
|
*
|
|
395
395
|
* Supported embed types: `AppEmbed`, `LiveboardEmbed`
|
|
396
396
|
* @type {boolean}
|
|
@@ -640,7 +640,7 @@ export class LiveboardEmbed extends V1Embed {
|
|
|
640
640
|
enableStopAnswerGenerationEmbed,
|
|
641
641
|
spotterChatConfig,
|
|
642
642
|
isThisPeriodInDateFiltersEnabled,
|
|
643
|
-
isContinuousLiveboardPDFEnabled,
|
|
643
|
+
isContinuousLiveboardPDFEnabled = false,
|
|
644
644
|
enableLiveboardDataCache,
|
|
645
645
|
} = this.viewConfig;
|
|
646
646
|
|
|
@@ -97,6 +97,7 @@ beforeAll(() => {
|
|
|
97
97
|
const customisations = {
|
|
98
98
|
style: {
|
|
99
99
|
customCSS: {},
|
|
100
|
+
customCSSUrl: undefined as string | undefined,
|
|
100
101
|
},
|
|
101
102
|
content: {},
|
|
102
103
|
};
|
|
@@ -132,6 +133,9 @@ const getMockAppInitPayload = (data: any) => {
|
|
|
132
133
|
customVariablesForThirdPartyTools,
|
|
133
134
|
interceptTimeout: undefined,
|
|
134
135
|
interceptUrls: [],
|
|
136
|
+
shouldBypassPayloadValidation:undefined,
|
|
137
|
+
useHostEventsV2:undefined,
|
|
138
|
+
embedExpiryInAuthToken:true
|
|
135
139
|
};
|
|
136
140
|
return {
|
|
137
141
|
type: EmbedEvent.APP_INIT,
|
|
@@ -641,6 +645,37 @@ describe('Unit test case for ts embed', () => {
|
|
|
641
645
|
});
|
|
642
646
|
});
|
|
643
647
|
|
|
648
|
+
test.each([
|
|
649
|
+
['not set', undefined, true],
|
|
650
|
+
['false', false, false],
|
|
651
|
+
['true', true, true],
|
|
652
|
+
] as [string, boolean | undefined, boolean][])(
|
|
653
|
+
'embedExpiryInAuthToken is %s when refreshAuthTokenOnNearExpiry is %s',
|
|
654
|
+
async (_label, refreshAuthTokenOnNearExpiry, expectedEmbedExpiry) => {
|
|
655
|
+
const mockEmbedEventPayload = {
|
|
656
|
+
type: EmbedEvent.APP_INIT,
|
|
657
|
+
data: {},
|
|
658
|
+
};
|
|
659
|
+
const searchEmbed = new AppEmbed(getRootEl(), {
|
|
660
|
+
...defaultViewConfig,
|
|
661
|
+
refreshAuthTokenOnNearExpiry,
|
|
662
|
+
});
|
|
663
|
+
searchEmbed.render();
|
|
664
|
+
const mockPort: any = {
|
|
665
|
+
postMessage: jest.fn(),
|
|
666
|
+
};
|
|
667
|
+
await executeAfterWait(() => {
|
|
668
|
+
const iframe = getIFrameEl();
|
|
669
|
+
postMessageToParent(iframe.contentWindow, mockEmbedEventPayload, mockPort);
|
|
670
|
+
});
|
|
671
|
+
await executeAfterWait(() => {
|
|
672
|
+
expect(mockPort.postMessage).toHaveBeenCalledWith(
|
|
673
|
+
getMockAppInitPayload({ embedExpiryInAuthToken: expectedEmbedExpiry }),
|
|
674
|
+
);
|
|
675
|
+
});
|
|
676
|
+
},
|
|
677
|
+
);
|
|
678
|
+
|
|
644
679
|
test('when Embed event status have start status', (done) => {
|
|
645
680
|
const mockEmbedEventPayload = {
|
|
646
681
|
type: EmbedEvent.Save,
|
package/src/embed/ts-embed.ts
CHANGED
|
@@ -481,7 +481,7 @@ export class TsEmbed {
|
|
|
481
481
|
this.embedConfig.customVariablesForThirdPartyTools || {},
|
|
482
482
|
hiddenListColumns: this.viewConfig.hiddenListColumns || [],
|
|
483
483
|
customActions: customActionsResult.actions,
|
|
484
|
-
embedExpiryInAuthToken: this.viewConfig.refreshAuthTokenOnNearExpiry,
|
|
484
|
+
embedExpiryInAuthToken: this.viewConfig.refreshAuthTokenOnNearExpiry ?? true,
|
|
485
485
|
...getInterceptInitData(this.viewConfig),
|
|
486
486
|
...getHostEventsConfig(this.viewConfig),
|
|
487
487
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import 'jest-fetch-mock';
|
|
2
|
+
import * as embedConfigModule from './embed/embedConfig';
|
|
3
|
+
import * as authTokenModule from './authToken';
|
|
4
|
+
import { AuthType } from './types';
|
|
5
|
+
import { tokenizedFetch } from './tokenizedFetch';
|
|
6
|
+
|
|
7
|
+
jest.mock('./embed/embedConfig');
|
|
8
|
+
jest.mock('./authToken');
|
|
9
|
+
|
|
10
|
+
const mockGetEmbedConfig = embedConfigModule.getEmbedConfig as jest.Mock;
|
|
11
|
+
const mockGetAuthenticationToken = authTokenModule.getAuthenticationToken as jest.Mock;
|
|
12
|
+
const mockGetCacheAuthToken = authTokenModule.getCacheAuthToken as jest.Mock;
|
|
13
|
+
|
|
14
|
+
describe('tokenizedFetch', () => {
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
jest.clearAllMocks();
|
|
17
|
+
fetchMock.resetMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('non-cookieless auth', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockGetEmbedConfig.mockReturnValue({ authType: AuthType.TrustedAuthToken });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should add Authorization Bearer header when cachedAuthToken exists', async () => {
|
|
26
|
+
mockGetCacheAuthToken.mockReturnValue('my-cached-token');
|
|
27
|
+
fetchMock.mockResponseOnce(JSON.stringify({}));
|
|
28
|
+
|
|
29
|
+
await tokenizedFetch('https://example.com/api', { method: 'GET' });
|
|
30
|
+
|
|
31
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
32
|
+
const request = fetchMock.mock.calls[0][0] as Request;
|
|
33
|
+
expect(request).toBeInstanceOf(Request);
|
|
34
|
+
expect(request.headers.get('Authorization')).toBe('Bearer my-cached-token');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should fetch with credentials include when no cachedAuthToken', async () => {
|
|
38
|
+
mockGetCacheAuthToken.mockReturnValue(null);
|
|
39
|
+
fetchMock.mockResponseOnce(JSON.stringify({}));
|
|
40
|
+
|
|
41
|
+
await tokenizedFetch('https://example.com/api', { method: 'GET' });
|
|
42
|
+
|
|
43
|
+
expect(fetchMock).toHaveBeenCalledWith('https://example.com/api', {
|
|
44
|
+
credentials: 'include',
|
|
45
|
+
method: 'GET',
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('cookieless auth (TrustedAuthTokenCookieless)', () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
mockGetEmbedConfig.mockReturnValue({
|
|
53
|
+
authType: AuthType.TrustedAuthTokenCookieless,
|
|
54
|
+
thoughtSpotHost: 'https://example.com',
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should add Authorization Bearer header from getAuthenticationToken', async () => {
|
|
59
|
+
mockGetAuthenticationToken.mockResolvedValue('cookieless-token');
|
|
60
|
+
fetchMock.mockResponseOnce(JSON.stringify({}));
|
|
61
|
+
|
|
62
|
+
await tokenizedFetch('https://example.com/api', { method: 'POST' });
|
|
63
|
+
|
|
64
|
+
expect(mockGetAuthenticationToken).toHaveBeenCalled();
|
|
65
|
+
const request = fetchMock.mock.calls[0][0] as Request;
|
|
66
|
+
expect(request).toBeInstanceOf(Request);
|
|
67
|
+
expect(request.headers.get('Authorization')).toBe('Bearer cookieless-token');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should not add Authorization header when getAuthenticationToken returns null', async () => {
|
|
71
|
+
mockGetAuthenticationToken.mockResolvedValue(null);
|
|
72
|
+
fetchMock.mockResponseOnce(JSON.stringify({}));
|
|
73
|
+
|
|
74
|
+
await tokenizedFetch('https://example.com/api', { method: 'POST' });
|
|
75
|
+
|
|
76
|
+
const request = fetchMock.mock.calls[0][0] as Request;
|
|
77
|
+
expect(request).toBeInstanceOf(Request);
|
|
78
|
+
expect(request.headers.get('Authorization')).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
package/src/tokenizedFetch.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAuthenticationToken } from './authToken';
|
|
1
|
+
import { getAuthenticationToken, getCacheAuthToken } from './authToken';
|
|
2
2
|
import { getEmbedConfig } from './embed/embedConfig';
|
|
3
3
|
|
|
4
4
|
import { AuthType } from './types';
|
|
@@ -20,18 +20,21 @@ import { AuthType } from './types';
|
|
|
20
20
|
*/
|
|
21
21
|
export const tokenizedFetch: typeof fetch = async (input, init): Promise<Response> => {
|
|
22
22
|
const embedConfig = getEmbedConfig();
|
|
23
|
+
const options: RequestInit = { ...init };
|
|
24
|
+
let token: string | undefined;
|
|
23
25
|
if (embedConfig.authType !== AuthType.TrustedAuthTokenCookieless) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
credentials: 'include'
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
token = getCacheAuthToken();
|
|
27
|
+
if (!token) {
|
|
28
|
+
return fetch(input, { ...options, credentials: 'include' });
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
token = await getAuthenticationToken(embedConfig);
|
|
29
32
|
}
|
|
33
|
+
const req = new Request(input, options);
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (authToken) {
|
|
34
|
-
req.headers.append('Authorization', `Bearer ${authToken}`);
|
|
35
|
+
if (token) {
|
|
36
|
+
req.headers.append('Authorization', `Bearer ${token}`);
|
|
35
37
|
}
|
|
38
|
+
|
|
36
39
|
return fetch(req);
|
|
37
|
-
};
|
|
40
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -926,9 +926,10 @@ export interface BaseViewConfig extends ApiInterceptFlags {
|
|
|
926
926
|
styleSheet__unstable?: string;
|
|
927
927
|
/**
|
|
928
928
|
* The list of actions to disable from the primary menu, more menu
|
|
929
|
-
* (...), and the contextual menu.
|
|
930
|
-
*
|
|
931
|
-
* Use this to disable
|
|
929
|
+
* (...), and the contextual menu. Disabled actions are grayed out
|
|
930
|
+
* and still visible to the user, but cannot be clicked.
|
|
931
|
+
* Use this when you want to disable an action (keep it visible but non-interactive).
|
|
932
|
+
* To completely remove an action from the UI, use {@link hiddenActions} instead.
|
|
932
933
|
*
|
|
933
934
|
* Supported embed types: `AppEmbed`, `LiveboardEmbed`, `SearchEmbed`, `SpotterAgentEmbed`, `SpotterEmbed`, `SearchBarEmbed`
|
|
934
935
|
* @version SDK: 1.6.0 | ThoughtSpot: ts8.nov.cl, 8.4.1.sw
|
|
@@ -959,9 +960,10 @@ export interface BaseViewConfig extends ApiInterceptFlags {
|
|
|
959
960
|
*/
|
|
960
961
|
disabledActionReason?: string;
|
|
961
962
|
/**
|
|
962
|
-
* The list of actions to
|
|
963
|
-
*
|
|
964
|
-
* Use this to
|
|
963
|
+
* The list of actions to completely remove from the embedded view.
|
|
964
|
+
* Hidden actions are not visible to the user at all (fully removed from the UI).
|
|
965
|
+
* Use this when you want to remove an action entirely.
|
|
966
|
+
* To keep an action visible but non-interactive (grayed out), use {@link disabledActions} instead.
|
|
965
967
|
*
|
|
966
968
|
* Supported embed types: `AppEmbed`, `LiveboardEmbed`, `SearchEmbed`, `SpotterAgentEmbed`, `SpotterEmbed`, `SearchBarEmbed`
|
|
967
969
|
* @version SDK: 1.6.0 | ThoughtSpot: ts8.nov.cl, 8.4.1.sw
|
|
@@ -980,9 +982,8 @@ export interface BaseViewConfig extends ApiInterceptFlags {
|
|
|
980
982
|
* The list of actions to display from the primary menu, more menu
|
|
981
983
|
* (...), and the contextual menu. These will be only actions that
|
|
982
984
|
* are visible to the user.
|
|
983
|
-
* Use this
|
|
984
|
-
*
|
|
985
|
-
* Use either this or hiddenActions.
|
|
985
|
+
* Use this as an allowlist — only the actions listed here will be shown.
|
|
986
|
+
* All other actions will be hidden. Use either this or {@link hiddenActions}, not both.
|
|
986
987
|
*
|
|
987
988
|
* Supported embed types: `AppEmbed`, `LiveboardEmbed`, `SearchEmbed`, `SpotterAgentEmbed`, `SpotterEmbed`, `SearchBarEmbed`
|
|
988
989
|
* @version SDK: 1.6.0 | ThoughtSpot: ts8.nov.cl, 8.4.1.sw
|
|
@@ -1437,6 +1438,7 @@ export interface BaseViewConfig extends ApiInterceptFlags {
|
|
|
1437
1438
|
/**
|
|
1438
1439
|
* Refresh the auth token when the token is near expiry.
|
|
1439
1440
|
* @version SDK: 1.45.2 | ThoughtSpot: 26.3.0.cl
|
|
1441
|
+
* @default true
|
|
1440
1442
|
* @example
|
|
1441
1443
|
* ```js
|
|
1442
1444
|
* const embed = new AppEmbed('#tsEmbed', {
|
|
@@ -5960,10 +5962,14 @@ export enum Param {
|
|
|
5960
5962
|
/**
|
|
5961
5963
|
* ThoughtSpot application pages include actions and menu commands
|
|
5962
5964
|
* for various user-initiated operations. These actions are represented
|
|
5963
|
-
* as enumeration members in the SDK. To
|
|
5964
|
-
*
|
|
5965
|
-
*
|
|
5966
|
-
*
|
|
5965
|
+
* as enumeration members in the SDK. To control actions in the embedded view:
|
|
5966
|
+
* - disabledActions — the action is grayed out and still visible, but non-interactive (user can see but not click).
|
|
5967
|
+
* - hiddenActions — the action is completely removed from the UI (user cannot see it at all).
|
|
5968
|
+
* - visibleActions — allowlist, only these actions are shown; all others are hidden.
|
|
5969
|
+
*
|
|
5970
|
+
* Use disabledActions to disable (gray out) an action.
|
|
5971
|
+
* Use hiddenActions to hide (fully remove) an action.
|
|
5972
|
+
* Use visibleActions to show only specific actions.
|
|
5967
5973
|
* @example
|
|
5968
5974
|
* ```js
|
|
5969
5975
|
* const embed = new LiveboardEmbed('#tsEmbed', {
|
|
@@ -6814,6 +6820,26 @@ export enum Action {
|
|
|
6814
6820
|
* @version SDK: 1.21.0 | ThoughtSpot: 9.2.0.cl, 9.5.1.sw
|
|
6815
6821
|
*/
|
|
6816
6822
|
AxisMenuRemove = 'axisMenuRemove',
|
|
6823
|
+
/**
|
|
6824
|
+
* The **Compare with** action in the chart axis customization menu.
|
|
6825
|
+
* Allows comparing data across dimensions or measures.
|
|
6826
|
+
* @example
|
|
6827
|
+
* ```js
|
|
6828
|
+
* disabledActions: [Action.AxisMenuCompare]
|
|
6829
|
+
* ```
|
|
6830
|
+
* @version SDK: 1.50.0 | ThoughtSpot: 26.7.0.cl
|
|
6831
|
+
*/
|
|
6832
|
+
AxisMenuCompare = 'axisMenuCompare',
|
|
6833
|
+
/**
|
|
6834
|
+
* The **Merge with** action in the chart axis customization menu.
|
|
6835
|
+
* Allows merging data across dimensions or measures.
|
|
6836
|
+
* @example
|
|
6837
|
+
* ```js
|
|
6838
|
+
* disabledActions: [Action.AxisMenuMerge]
|
|
6839
|
+
* ```
|
|
6840
|
+
* @version SDK: 1.50.0 | ThoughtSpot: 26.7.0.cl
|
|
6841
|
+
*/
|
|
6842
|
+
AxisMenuMerge = 'axisMenuMerge',
|
|
6817
6843
|
/**
|
|
6818
6844
|
* @hidden
|
|
6819
6845
|
*/
|
|
@@ -8093,6 +8119,9 @@ export interface DefaultAppInitData {
|
|
|
8093
8119
|
customActions: CustomAction[];
|
|
8094
8120
|
interceptTimeout: number | undefined;
|
|
8095
8121
|
interceptUrls: (string | InterceptedApiType)[];
|
|
8122
|
+
embedExpiryInAuthToken:boolean;
|
|
8123
|
+
shouldBypassPayloadValidation?:boolean
|
|
8124
|
+
useHostEventsV2?:boolean
|
|
8096
8125
|
}
|
|
8097
8126
|
|
|
8098
8127
|
/**
|