@thoughtspot/visual-embed-sdk 1.46.2 → 1.46.3
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.js +0 -3
- package/cjs/src/auth.js.map +1 -1
- package/cjs/src/auth.spec.js +1 -3
- package/cjs/src/auth.spec.js.map +1 -1
- package/cjs/src/authToken.js +0 -1
- package/cjs/src/authToken.js.map +1 -1
- package/cjs/src/authToken.spec.js +2 -1
- package/cjs/src/authToken.spec.js.map +1 -1
- package/cjs/src/embed/app.d.ts +2 -1
- package/cjs/src/embed/app.d.ts.map +1 -1
- package/cjs/src/embed/app.js.map +1 -1
- package/cjs/src/embed/auto-frame-renderer.d.ts +41 -0
- package/cjs/src/embed/auto-frame-renderer.d.ts.map +1 -0
- package/cjs/src/embed/auto-frame-renderer.js +145 -0
- package/cjs/src/embed/auto-frame-renderer.js.map +1 -0
- package/cjs/src/embed/auto-frame-renderer.spec.d.ts +2 -0
- package/cjs/src/embed/auto-frame-renderer.spec.d.ts.map +1 -0
- package/cjs/src/embed/auto-frame-renderer.spec.js +195 -0
- package/cjs/src/embed/auto-frame-renderer.spec.js.map +1 -0
- package/cjs/src/embed/base.d.ts.map +1 -1
- package/cjs/src/embed/base.js +4 -7
- package/cjs/src/embed/base.js.map +1 -1
- package/cjs/src/embed/hostEventClient/host-event-client.js +0 -2
- package/cjs/src/embed/hostEventClient/host-event-client.js.map +1 -1
- package/cjs/src/embed/liveboard.d.ts +2 -1
- package/cjs/src/embed/liveboard.d.ts.map +1 -1
- package/cjs/src/embed/liveboard.js +5 -3
- package/cjs/src/embed/liveboard.js.map +1 -1
- package/cjs/src/embed/liveboard.spec.js +4 -2
- package/cjs/src/embed/liveboard.spec.js.map +1 -1
- package/cjs/src/embed/sage.js +0 -1
- package/cjs/src/embed/sage.js.map +1 -1
- package/cjs/src/embed/search.spec.js +0 -2
- package/cjs/src/embed/search.spec.js.map +1 -1
- package/cjs/src/embed/ts-embed.d.ts.map +1 -1
- package/cjs/src/embed/ts-embed.js +3 -3
- package/cjs/src/embed/ts-embed.js.map +1 -1
- package/cjs/src/embed/ts-embed.spec.js +2 -1
- package/cjs/src/embed/ts-embed.spec.js.map +1 -1
- package/cjs/src/index.d.ts +3 -2
- package/cjs/src/index.d.ts.map +1 -1
- package/cjs/src/index.js +3 -1
- package/cjs/src/index.js.map +1 -1
- package/cjs/src/react/index.js +0 -1
- package/cjs/src/react/index.js.map +1 -1
- package/cjs/src/react/index.spec.js +6 -3
- package/cjs/src/react/index.spec.js.map +1 -1
- package/cjs/src/types.d.ts +56 -32
- package/cjs/src/types.d.ts.map +1 -1
- package/cjs/src/types.js +34 -18
- package/cjs/src/types.js.map +1 -1
- package/cjs/src/utils/graphql/answerService/answerService.d.ts +5 -4
- package/cjs/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
- package/cjs/src/utils/graphql/answerService/answerService.js +7 -11
- package/cjs/src/utils/graphql/answerService/answerService.js.map +1 -1
- package/cjs/src/utils/graphql/preview-service.js +0 -1
- package/cjs/src/utils/graphql/preview-service.js.map +1 -1
- package/cjs/src/utils/processData.js +0 -5
- package/cjs/src/utils/processData.js.map +1 -1
- package/dist/{index-DkizS4xM.js → index-DyX-x6uN.js} +1 -1
- package/dist/src/embed/app.d.ts +2 -1
- package/dist/src/embed/app.d.ts.map +1 -1
- package/dist/src/embed/auto-frame-renderer.d.ts +41 -0
- package/dist/src/embed/auto-frame-renderer.d.ts.map +1 -0
- package/dist/src/embed/auto-frame-renderer.spec.d.ts +2 -0
- package/dist/src/embed/auto-frame-renderer.spec.d.ts.map +1 -0
- package/dist/src/embed/base.d.ts.map +1 -1
- package/dist/src/embed/liveboard.d.ts +2 -1
- package/dist/src/embed/liveboard.d.ts.map +1 -1
- package/dist/src/embed/ts-embed.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/types.d.ts +56 -32
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/graphql/answerService/answerService.d.ts +5 -4
- package/dist/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
- package/dist/tsembed-react.es.js +55 -57
- package/dist/tsembed-react.js +54 -56
- package/dist/tsembed.es.js +194 -57
- package/dist/tsembed.js +193 -55
- package/dist/visual-embed-sdk-react-full.d.ts +54 -33
- package/dist/visual-embed-sdk-react.d.ts +54 -33
- package/dist/visual-embed-sdk.d.ts +103 -38
- package/lib/package.json +1 -1
- package/lib/src/auth.js +0 -3
- package/lib/src/auth.js.map +1 -1
- package/lib/src/auth.spec.js +1 -3
- package/lib/src/auth.spec.js.map +1 -1
- package/lib/src/authToken.js +0 -1
- package/lib/src/authToken.js.map +1 -1
- package/lib/src/authToken.spec.js +2 -1
- package/lib/src/authToken.spec.js.map +1 -1
- package/lib/src/embed/app.d.ts +2 -1
- package/lib/src/embed/app.d.ts.map +1 -1
- package/lib/src/embed/app.js.map +1 -1
- package/lib/src/embed/auto-frame-renderer.d.ts +41 -0
- package/lib/src/embed/auto-frame-renderer.d.ts.map +1 -0
- package/lib/src/embed/auto-frame-renderer.js +141 -0
- package/lib/src/embed/auto-frame-renderer.js.map +1 -0
- package/lib/src/embed/auto-frame-renderer.spec.d.ts +2 -0
- package/lib/src/embed/auto-frame-renderer.spec.d.ts.map +1 -0
- package/lib/src/embed/auto-frame-renderer.spec.js +192 -0
- package/lib/src/embed/auto-frame-renderer.spec.js.map +1 -0
- package/lib/src/embed/base.d.ts.map +1 -1
- package/lib/src/embed/base.js +4 -7
- package/lib/src/embed/base.js.map +1 -1
- package/lib/src/embed/hostEventClient/host-event-client.js +0 -2
- package/lib/src/embed/hostEventClient/host-event-client.js.map +1 -1
- package/lib/src/embed/liveboard.d.ts +2 -1
- package/lib/src/embed/liveboard.d.ts.map +1 -1
- package/lib/src/embed/liveboard.js +5 -3
- package/lib/src/embed/liveboard.js.map +1 -1
- package/lib/src/embed/liveboard.spec.js +4 -2
- package/lib/src/embed/liveboard.spec.js.map +1 -1
- package/lib/src/embed/sage.js +0 -1
- package/lib/src/embed/sage.js.map +1 -1
- package/lib/src/embed/search.spec.js +0 -2
- package/lib/src/embed/search.spec.js.map +1 -1
- package/lib/src/embed/ts-embed.d.ts.map +1 -1
- package/lib/src/embed/ts-embed.js +3 -3
- package/lib/src/embed/ts-embed.js.map +1 -1
- package/lib/src/embed/ts-embed.spec.js +2 -1
- package/lib/src/embed/ts-embed.spec.js.map +1 -1
- package/lib/src/index.d.ts +3 -2
- package/lib/src/index.d.ts.map +1 -1
- package/lib/src/index.js +1 -0
- package/lib/src/index.js.map +1 -1
- package/lib/src/react/index.js +0 -1
- package/lib/src/react/index.js.map +1 -1
- package/lib/src/react/index.spec.js +6 -3
- package/lib/src/react/index.spec.js.map +1 -1
- package/lib/src/types.d.ts +56 -32
- package/lib/src/types.d.ts.map +1 -1
- package/lib/src/types.js +34 -18
- package/lib/src/types.js.map +1 -1
- package/lib/src/utils/graphql/answerService/answerService.d.ts +5 -4
- package/lib/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
- package/lib/src/utils/graphql/answerService/answerService.js +7 -11
- package/lib/src/utils/graphql/answerService/answerService.js.map +1 -1
- package/lib/src/utils/graphql/preview-service.js +0 -1
- package/lib/src/utils/graphql/preview-service.js.map +1 -1
- package/lib/src/utils/processData.js +0 -5
- package/lib/src/utils/processData.js.map +1 -1
- package/lib/src/visual-embed-sdk.d.ts +11466 -0
- package/package.json +1 -1
- package/src/auth.spec.ts +1 -1
- package/src/auth.ts +3 -3
- package/src/authToken.spec.ts +2 -1
- package/src/authToken.ts +1 -1
- package/src/embed/app.ts +2 -1
- package/src/embed/auto-frame-renderer.spec.ts +266 -0
- package/src/embed/auto-frame-renderer.ts +152 -0
- package/src/embed/base.spec.ts +1 -1
- package/src/embed/base.ts +7 -8
- package/src/embed/hostEventClient/host-event-client.ts +2 -2
- package/src/embed/liveboard.spec.ts +4 -2
- package/src/embed/liveboard.ts +7 -4
- package/src/embed/sage.ts +1 -1
- package/src/embed/search.spec.ts +2 -2
- package/src/embed/ts-embed.spec.ts +2 -1
- package/src/embed/ts-embed.ts +3 -3
- package/src/index.ts +3 -0
- package/src/react/index.spec.tsx +6 -3
- package/src/react/index.tsx +1 -1
- package/src/types.ts +57 -32
- package/src/utils/graphql/answerService/answerService.ts +8 -7
- package/src/utils/graphql/preview-service.ts +1 -1
- package/src/utils/processData.ts +5 -5
package/package.json
CHANGED
package/src/auth.spec.ts
CHANGED
|
@@ -283,7 +283,7 @@ describe('Unit test for auth', () => {
|
|
|
283
283
|
text: () => Promise.resolve('abc'),
|
|
284
284
|
} as any));
|
|
285
285
|
jest.spyOn(authService, 'fetchAuthPostService').mockImplementation(() =>
|
|
286
|
-
|
|
286
|
+
|
|
287
287
|
Promise.reject({
|
|
288
288
|
status: 500,
|
|
289
289
|
}));
|
package/src/auth.ts
CHANGED
|
@@ -19,11 +19,11 @@ import { getSessionInfo, getPreauthInfo } from './utils/sessionInfoService';
|
|
|
19
19
|
import { ERROR_MESSAGE } from './errors';
|
|
20
20
|
import { resetAllCachedServices } from './utils/resetServices';
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
export let loggedInStatus = false;
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
export let samlAuthWindow: Window = null;
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
export let samlCompletionPromise: Promise<void> = null;
|
|
28
28
|
|
|
29
29
|
let releaseVersion = '';
|
package/src/authToken.spec.ts
CHANGED
|
@@ -129,7 +129,8 @@ describe('AuthToken Unit tests', () => {
|
|
|
129
129
|
expect(token).toBe(newToken);
|
|
130
130
|
// Should not validate cached token (condition at line 23 is false)
|
|
131
131
|
expect(validateAuthTokenSpy).not.toHaveBeenCalledWith(config, cachedToken, true);
|
|
132
|
-
// But should validate new token (though it returns early when
|
|
132
|
+
// But should validate new token (though it returns early when
|
|
133
|
+
// disableTokenVerification is true)
|
|
133
134
|
expect(validateAuthTokenSpy).toHaveBeenCalledWith(config, newToken);
|
|
134
135
|
expect(getAuthTokenMock).toHaveBeenCalled();
|
|
135
136
|
|
package/src/authToken.ts
CHANGED
|
@@ -83,7 +83,7 @@ export const validateAuthToken = async (
|
|
|
83
83
|
|
|
84
84
|
if (cachedAuthToken && cachedAuthToken === authToken) {
|
|
85
85
|
if (!embedConfig.suppressErrorAlerts && !suppressAlert) {
|
|
86
|
-
|
|
86
|
+
|
|
87
87
|
alert(ERROR_MESSAGE.DUPLICATE_TOKEN_ERR);
|
|
88
88
|
}
|
|
89
89
|
throw new Error(ERROR_MESSAGE.DUPLICATE_TOKEN_ERR);
|
package/src/embed/app.ts
CHANGED
|
@@ -570,7 +570,8 @@ export interface AppViewConfig extends AllEmbedViewConfig {
|
|
|
570
570
|
isLiveboardStylingAndGroupingEnabled?: boolean;
|
|
571
571
|
|
|
572
572
|
/**
|
|
573
|
-
* This flag is used to enable/disable the png embedding of liveboard in scheduled
|
|
573
|
+
* This flag is used to enable/disable the png embedding of liveboard in scheduled
|
|
574
|
+
* mails
|
|
574
575
|
*
|
|
575
576
|
* Supported embed types: `AppEmbed`, `LiveboardEmbed`
|
|
576
577
|
* @type {boolean}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { startAutoMCPFrameRenderer } from './auto-frame-renderer';
|
|
2
|
+
import { Param, AuthType } from '../types';
|
|
3
|
+
import { init } from '../index';
|
|
4
|
+
import * as authInstance from '../auth';
|
|
5
|
+
import { TsEmbed } from './ts-embed';
|
|
6
|
+
import {
|
|
7
|
+
getDocumentBody,
|
|
8
|
+
} from '../test/test-utils';
|
|
9
|
+
|
|
10
|
+
const thoughtSpotHost = 'tshost';
|
|
11
|
+
|
|
12
|
+
describe('startAutoMCPFrameRenderer', () => {
|
|
13
|
+
let renderIFrameSpy: jest.SpyInstance;
|
|
14
|
+
let getEmbedBasePathSpy: jest.SpyInstance;
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
init({
|
|
18
|
+
thoughtSpotHost,
|
|
19
|
+
authType: AuthType.None,
|
|
20
|
+
});
|
|
21
|
+
jest.spyOn(authInstance, 'postLoginService').mockImplementation(() => Promise.resolve(undefined));
|
|
22
|
+
jest.spyOn(window, 'alert').mockImplementation(() => undefined);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
document.body.innerHTML = getDocumentBody();
|
|
27
|
+
getEmbedBasePathSpy = jest.spyOn(
|
|
28
|
+
TsEmbed.prototype as any,
|
|
29
|
+
'getEmbedBasePath',
|
|
30
|
+
).mockImplementation(function (this: any, query: string) {
|
|
31
|
+
return `http://${thoughtSpotHost}/?${query}#`;
|
|
32
|
+
});
|
|
33
|
+
renderIFrameSpy = jest.spyOn(
|
|
34
|
+
TsEmbed.prototype as any,
|
|
35
|
+
'renderIFrame',
|
|
36
|
+
).mockResolvedValue(undefined);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
renderIFrameSpy.mockRestore();
|
|
41
|
+
getEmbedBasePathSpy.mockRestore();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('MutationObserver setup', () => {
|
|
45
|
+
test('should return a MutationObserver', () => {
|
|
46
|
+
const observer = startAutoMCPFrameRenderer();
|
|
47
|
+
expect(observer).toBeInstanceOf(MutationObserver);
|
|
48
|
+
observer.disconnect();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should observe document.body with childList and subtree', () => {
|
|
52
|
+
const observeSpy = jest.spyOn(MutationObserver.prototype, 'observe');
|
|
53
|
+
const observer = startAutoMCPFrameRenderer();
|
|
54
|
+
expect(observeSpy).toHaveBeenCalledWith(document.body, {
|
|
55
|
+
childList: true,
|
|
56
|
+
subtree: true,
|
|
57
|
+
});
|
|
58
|
+
observer.disconnect();
|
|
59
|
+
observeSpy.mockRestore();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('iframe detection via tsmcp param', () => {
|
|
64
|
+
test('should process directly-added iframes with tsmcp=true', async () => {
|
|
65
|
+
const observer = startAutoMCPFrameRenderer();
|
|
66
|
+
|
|
67
|
+
const iframe = document.createElement('iframe');
|
|
68
|
+
iframe.src = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true#/embed/viz/lb1/tab1`;
|
|
69
|
+
document.body.appendChild(iframe);
|
|
70
|
+
|
|
71
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
72
|
+
|
|
73
|
+
expect(renderIFrameSpy).toHaveBeenCalled();
|
|
74
|
+
observer.disconnect();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('should not process iframes without tsmcp param', async () => {
|
|
78
|
+
const observer = startAutoMCPFrameRenderer();
|
|
79
|
+
|
|
80
|
+
const iframe = document.createElement('iframe');
|
|
81
|
+
iframe.src = `https://${thoughtSpotHost}/v2/?embedApp=true#/embed/viz/lb1`;
|
|
82
|
+
document.body.appendChild(iframe);
|
|
83
|
+
|
|
84
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
85
|
+
|
|
86
|
+
expect(renderIFrameSpy).not.toHaveBeenCalled();
|
|
87
|
+
observer.disconnect();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('should process tsmcp iframes nested inside added elements', async () => {
|
|
91
|
+
const observer = startAutoMCPFrameRenderer();
|
|
92
|
+
|
|
93
|
+
const wrapper = document.createElement('div');
|
|
94
|
+
const iframe = document.createElement('iframe');
|
|
95
|
+
iframe.src = `https://${thoughtSpotHost}/?${Param.Tsmcp}=true`;
|
|
96
|
+
wrapper.appendChild(iframe);
|
|
97
|
+
document.body.appendChild(wrapper);
|
|
98
|
+
|
|
99
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
100
|
+
|
|
101
|
+
expect(renderIFrameSpy).toHaveBeenCalled();
|
|
102
|
+
observer.disconnect();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('should not process nested iframes without tsmcp param', async () => {
|
|
106
|
+
const observer = startAutoMCPFrameRenderer();
|
|
107
|
+
|
|
108
|
+
const wrapper = document.createElement('div');
|
|
109
|
+
const iframe = document.createElement('iframe');
|
|
110
|
+
iframe.src = `https://${thoughtSpotHost}/?embedApp=true`;
|
|
111
|
+
wrapper.appendChild(iframe);
|
|
112
|
+
document.body.appendChild(wrapper);
|
|
113
|
+
|
|
114
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
115
|
+
|
|
116
|
+
expect(renderIFrameSpy).not.toHaveBeenCalled();
|
|
117
|
+
observer.disconnect();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('should ignore non-iframe element nodes', async () => {
|
|
121
|
+
const observer = startAutoMCPFrameRenderer();
|
|
122
|
+
|
|
123
|
+
const div = document.createElement('div');
|
|
124
|
+
div.textContent = `${Param.Tsmcp}=true`;
|
|
125
|
+
document.body.appendChild(div);
|
|
126
|
+
|
|
127
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
128
|
+
|
|
129
|
+
expect(renderIFrameSpy).not.toHaveBeenCalled();
|
|
130
|
+
observer.disconnect();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('should ignore text nodes', async () => {
|
|
134
|
+
const observer = startAutoMCPFrameRenderer();
|
|
135
|
+
|
|
136
|
+
const text = document.createTextNode('tsmcp=true');
|
|
137
|
+
document.body.appendChild(text);
|
|
138
|
+
|
|
139
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
140
|
+
|
|
141
|
+
expect(renderIFrameSpy).not.toHaveBeenCalled();
|
|
142
|
+
observer.disconnect();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('should process multiple tsmcp iframes in one mutation', async () => {
|
|
146
|
+
const observer = startAutoMCPFrameRenderer();
|
|
147
|
+
|
|
148
|
+
const wrapper = document.createElement('div');
|
|
149
|
+
const iframe1 = document.createElement('iframe');
|
|
150
|
+
iframe1.src = `https://${thoughtSpotHost}/?${Param.Tsmcp}=true&id=1`;
|
|
151
|
+
const iframe2 = document.createElement('iframe');
|
|
152
|
+
iframe2.src = `https://${thoughtSpotHost}/?${Param.Tsmcp}=true&id=2`;
|
|
153
|
+
wrapper.appendChild(iframe1);
|
|
154
|
+
wrapper.appendChild(iframe2);
|
|
155
|
+
document.body.appendChild(wrapper);
|
|
156
|
+
|
|
157
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
158
|
+
|
|
159
|
+
expect(renderIFrameSpy).toHaveBeenCalledTimes(2);
|
|
160
|
+
observer.disconnect();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should ignore iframes with invalid src URLs', async () => {
|
|
164
|
+
const observer = startAutoMCPFrameRenderer();
|
|
165
|
+
|
|
166
|
+
const iframe = document.createElement('iframe');
|
|
167
|
+
iframe.src = 'about:blank';
|
|
168
|
+
document.body.appendChild(iframe);
|
|
169
|
+
|
|
170
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
171
|
+
|
|
172
|
+
expect(renderIFrameSpy).not.toHaveBeenCalled();
|
|
173
|
+
observer.disconnect();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('handleInsertionIntoDOM override', () => {
|
|
178
|
+
test('should replace the original iframe when renderIFrame inserts DOM', async () => {
|
|
179
|
+
renderIFrameSpy.mockRestore();
|
|
180
|
+
|
|
181
|
+
const replaceSpy = jest.fn();
|
|
182
|
+
|
|
183
|
+
renderIFrameSpy = jest.spyOn(
|
|
184
|
+
TsEmbed.prototype as any,
|
|
185
|
+
'renderIFrame',
|
|
186
|
+
).mockImplementation(async function (this: any) {
|
|
187
|
+
const newIframe = document.createElement('iframe');
|
|
188
|
+
newIframe.src = 'https://replaced.example.com';
|
|
189
|
+
this.frameToReplace.replaceWith = replaceSpy;
|
|
190
|
+
this.handleInsertionIntoDOM(newIframe);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const observer = startAutoMCPFrameRenderer();
|
|
194
|
+
|
|
195
|
+
const iframe = document.createElement('iframe');
|
|
196
|
+
iframe.src = `https://${thoughtSpotHost}/?${Param.Tsmcp}=true`;
|
|
197
|
+
document.body.appendChild(iframe);
|
|
198
|
+
|
|
199
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
200
|
+
|
|
201
|
+
expect(replaceSpy).toHaveBeenCalled();
|
|
202
|
+
observer.disconnect();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('viewConfig forwarding', () => {
|
|
207
|
+
test('should accept empty viewConfig', () => {
|
|
208
|
+
const observer = startAutoMCPFrameRenderer({});
|
|
209
|
+
expect(observer).toBeInstanceOf(MutationObserver);
|
|
210
|
+
observer.disconnect();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('should accept no arguments (default empty config)', () => {
|
|
214
|
+
const observer = startAutoMCPFrameRenderer();
|
|
215
|
+
expect(observer).toBeInstanceOf(MutationObserver);
|
|
216
|
+
observer.disconnect();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('getMCPIframeSrc URL construction', () => {
|
|
221
|
+
test('should strip tsmcp param and merge embed params into rendered src', async () => {
|
|
222
|
+
let capturedSrc = '';
|
|
223
|
+
renderIFrameSpy.mockRestore();
|
|
224
|
+
renderIFrameSpy = jest.spyOn(
|
|
225
|
+
TsEmbed.prototype as any,
|
|
226
|
+
'renderIFrame',
|
|
227
|
+
).mockImplementation(async function (this: any, src: string) {
|
|
228
|
+
capturedSrc = src;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const observer = startAutoMCPFrameRenderer();
|
|
232
|
+
|
|
233
|
+
const iframe = document.createElement('iframe');
|
|
234
|
+
iframe.src = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true&customParam=hello#/embed/viz`;
|
|
235
|
+
document.body.appendChild(iframe);
|
|
236
|
+
|
|
237
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
238
|
+
|
|
239
|
+
expect(capturedSrc).not.toContain(`${Param.Tsmcp}=true`);
|
|
240
|
+
expect(capturedSrc).toContain('customParam=hello');
|
|
241
|
+
observer.disconnect();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('should preserve hash from original iframe src', async () => {
|
|
245
|
+
let capturedSrc = '';
|
|
246
|
+
renderIFrameSpy.mockRestore();
|
|
247
|
+
renderIFrameSpy = jest.spyOn(
|
|
248
|
+
TsEmbed.prototype as any,
|
|
249
|
+
'renderIFrame',
|
|
250
|
+
).mockImplementation(async function (this: any, src: string) {
|
|
251
|
+
capturedSrc = src;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const observer = startAutoMCPFrameRenderer();
|
|
255
|
+
|
|
256
|
+
const iframe = document.createElement('iframe');
|
|
257
|
+
iframe.src = `https://${thoughtSpotHost}/v2/?${Param.Tsmcp}=true#/embed/viz/lb123`;
|
|
258
|
+
document.body.appendChild(iframe);
|
|
259
|
+
|
|
260
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
261
|
+
|
|
262
|
+
expect(capturedSrc).toContain('/embed/viz/lb123');
|
|
263
|
+
observer.disconnect();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { AutoMCPFrameRendererViewConfig, Param } from "../types";
|
|
2
|
+
import { TsEmbed } from "./ts-embed";
|
|
3
|
+
import { getQueryParamString } from "../utils";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Starts an automatic renderer that watches the DOM for iframes containing
|
|
8
|
+
* the `tsmcp=true` query parameter and replaces them with fully configured
|
|
9
|
+
* ThoughtSpot embed iframes. The query parameter is automatically added by
|
|
10
|
+
* the ThoughtSpot MCP server.
|
|
11
|
+
*
|
|
12
|
+
* A {@link MutationObserver} is set up on `document.body` to detect both
|
|
13
|
+
* directly added iframes and iframes nested within added container elements.
|
|
14
|
+
* Each matching iframe is replaced in-place with a new ThoughtSpot embed
|
|
15
|
+
* iframe that merges the original iframe's query parameters with the SDK
|
|
16
|
+
* embed parameters.
|
|
17
|
+
*
|
|
18
|
+
* Call {@link MutationObserver.disconnect | observer.disconnect()} on the
|
|
19
|
+
* returned observer to stop monitoring the DOM.
|
|
20
|
+
*
|
|
21
|
+
* @param viewConfig - Optional configuration for the auto-rendered embeds.
|
|
22
|
+
* Accepts all properties from {@link AutoMCPFrameRendererViewConfig}.
|
|
23
|
+
* Defaults to an empty config.
|
|
24
|
+
* @returns A {@link MutationObserver} instance that is actively observing
|
|
25
|
+
* `document.body`. Disconnect it when monitoring is no longer needed.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```js
|
|
29
|
+
* import { startAutoMCPFrameRenderer } from '@thoughtspot/visual-embed-sdk';
|
|
30
|
+
*
|
|
31
|
+
* // Start watching the DOM for tsmcp iframes
|
|
32
|
+
* const observer = startAutoMCPFrameRenderer({
|
|
33
|
+
* // optional view config overrides
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* // Later, stop watching
|
|
37
|
+
* observer.disconnect();
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* Detailed example of how to use the auto-frame renderer:
|
|
42
|
+
* [Python React Agent Simple UI](https://github.com/thoughtspot/developer-examples/tree/main/mcp/python-react-agent-simple-ui)
|
|
43
|
+
*/
|
|
44
|
+
export function startAutoMCPFrameRenderer(viewConfig: AutoMCPFrameRendererViewConfig = {}) {
|
|
45
|
+
|
|
46
|
+
const replaceWithMCPIframe = (iframe: HTMLIFrameElement) => {
|
|
47
|
+
const autoMCPFrameRenderer = new AutoFrameRenderer(viewConfig);
|
|
48
|
+
autoMCPFrameRenderer.replaceIframe(iframe);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const observer = new MutationObserver((mutations) => {
|
|
52
|
+
for (const mutation of mutations) {
|
|
53
|
+
for (const node of Array.from(mutation.addedNodes)) {
|
|
54
|
+
if (node instanceof HTMLIFrameElement && isTSMCPIframe(node)) {
|
|
55
|
+
replaceWithMCPIframe(node);
|
|
56
|
+
}
|
|
57
|
+
if (node instanceof HTMLElement) {
|
|
58
|
+
node.querySelectorAll('iframe').forEach((iframe) => {
|
|
59
|
+
if (isTSMCPIframe(iframe)) {
|
|
60
|
+
replaceWithMCPIframe(iframe);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
69
|
+
|
|
70
|
+
return observer;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isTSMCPIframe(iframe: HTMLIFrameElement) {
|
|
74
|
+
try {
|
|
75
|
+
const url = new URL(iframe.src);
|
|
76
|
+
return url.searchParams.get(Param.Tsmcp) === 'true';
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// The iframe src might not be a valid URL (e.g., 'about:blank').
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Embed component that automatically replaces a plain iframe with a
|
|
85
|
+
* ThoughtSpot embed iframe. It merges the SDK's embed parameters with
|
|
86
|
+
* the original iframe's query parameters (stripping the `tsmcp` marker)
|
|
87
|
+
* and swaps the original iframe element in the DOM.
|
|
88
|
+
*
|
|
89
|
+
* This class is used internally by {@link startAutoMCPFrameRenderer} and
|
|
90
|
+
* is not intended to be instantiated directly.
|
|
91
|
+
*/
|
|
92
|
+
class AutoFrameRenderer extends TsEmbed {
|
|
93
|
+
private frameToReplace: HTMLIFrameElement;
|
|
94
|
+
|
|
95
|
+
constructor(protected viewConfig: AutoMCPFrameRendererViewConfig) {
|
|
96
|
+
viewConfig.embedComponentType = 'auto-frame-renderer';
|
|
97
|
+
const container = document.createElement('div');
|
|
98
|
+
super(container, viewConfig);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Builds the final iframe `src` by merging the SDK embed parameters
|
|
103
|
+
* with the query parameters already present on the source iframe URL.
|
|
104
|
+
* The `tsmcp` marker param is removed so it does not propagate to the
|
|
105
|
+
* ThoughtSpot application.
|
|
106
|
+
*
|
|
107
|
+
* @param sourceSrc - The original iframe's `src` URL string.
|
|
108
|
+
* @returns The constructed URL to use for the ThoughtSpot embed iframe.
|
|
109
|
+
*/
|
|
110
|
+
private getMCPIframeSrc(sourceSrc: string) {
|
|
111
|
+
const queryParams = this.getEmbedParamsObject();
|
|
112
|
+
const sourceURL = new URL(sourceSrc);
|
|
113
|
+
const existingQueryParams = sourceURL.searchParams;
|
|
114
|
+
const existingQueryParamsObject = Object.fromEntries(existingQueryParams);
|
|
115
|
+
delete existingQueryParamsObject[Param.Tsmcp];
|
|
116
|
+
|
|
117
|
+
const mergedQueryParams = { ...queryParams, ...existingQueryParamsObject };
|
|
118
|
+
const mergedQueryParamsString = getQueryParamString(mergedQueryParams);
|
|
119
|
+
const frameSrc = `${this.getEmbedBasePath(mergedQueryParamsString)}${sourceURL.hash.replace('#', '')}`;
|
|
120
|
+
return frameSrc;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Overrides the base insertion behavior so the new embed iframe
|
|
125
|
+
* replaces the original iframe in-place rather than being appended
|
|
126
|
+
* to a container element. Falls back to the default behavior when
|
|
127
|
+
* no iframe has been set for replacement.
|
|
128
|
+
*/
|
|
129
|
+
protected handleInsertionIntoDOM(child: string | Node): void {
|
|
130
|
+
if (this.frameToReplace) {
|
|
131
|
+
this.frameToReplace.replaceWith(child);
|
|
132
|
+
} else {
|
|
133
|
+
super.handleInsertionIntoDOM(child);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Replaces the given iframe with a new ThoughtSpot embed iframe.
|
|
139
|
+
*
|
|
140
|
+
* The original iframe's `src` is used to derive the embed URL, and
|
|
141
|
+
* once the new iframe is rendered it takes the original's place in
|
|
142
|
+
* the DOM tree.
|
|
143
|
+
*
|
|
144
|
+
* @param iframe - The existing `<iframe>` element to replace.
|
|
145
|
+
*/
|
|
146
|
+
public async replaceIframe(iframe: HTMLIFrameElement): Promise<void> {
|
|
147
|
+
this.frameToReplace = iframe;
|
|
148
|
+
const src = this.getMCPIframeSrc(iframe.src);
|
|
149
|
+
await this.renderIFrame(src);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
package/src/embed/base.spec.ts
CHANGED
package/src/embed/base.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
|
|
2
|
+
|
|
3
3
|
/**
|
|
4
4
|
* Copyright (c) 2022
|
|
5
5
|
*
|
|
@@ -36,7 +36,6 @@ import { getEmbedConfig, setEmbedConfig } from './embedConfig';
|
|
|
36
36
|
import { getQueryParamString, getValueFromWindow, isWindowUndefined, storeValueInWindow } from '../utils';
|
|
37
37
|
import { resetAllCachedServices } from '../utils/resetServices';
|
|
38
38
|
import { reload } from '../utils/processTrigger';
|
|
39
|
-
import { ERROR_MESSAGE } from '../errors';
|
|
40
39
|
|
|
41
40
|
const CONFIG_DEFAULTS: Partial<EmbedConfig> = {
|
|
42
41
|
loginFailedMessage: 'Not logged in',
|
|
@@ -116,7 +115,7 @@ export const prefetch = (
|
|
|
116
115
|
additionalFlags?: { [key: string]: string | number | boolean },
|
|
117
116
|
): void => {
|
|
118
117
|
if (url === '') {
|
|
119
|
-
|
|
118
|
+
|
|
120
119
|
logger.warn('The prefetch method does not have a valid URL');
|
|
121
120
|
} else {
|
|
122
121
|
const features = prefetchFeatures || [PrefetchFeatures.FullApp];
|
|
@@ -142,9 +141,9 @@ export const prefetch = (
|
|
|
142
141
|
iFrame.style.height = '0';
|
|
143
142
|
iFrame.style.border = '0';
|
|
144
143
|
|
|
145
|
-
// Make it 'fixed' to keep it in a different stacking
|
|
146
|
-
//
|
|
147
|
-
//
|
|
144
|
+
// Make it 'fixed' to keep it in a different stacking
|
|
145
|
+
// context. This should solve the focus behaviours inside
|
|
146
|
+
// the iframe from interfering with main body.
|
|
148
147
|
iFrame.style.position = 'fixed';
|
|
149
148
|
// Push it out of viewport.
|
|
150
149
|
iFrame.style.top = '100vh';
|
|
@@ -323,7 +322,7 @@ export const renderInQueue = (fn: (next?: (val?: any) => void) => Promise<any>):
|
|
|
323
322
|
return renderQueue;
|
|
324
323
|
}
|
|
325
324
|
// Sending an empty function to keep it consistent with the above usage.
|
|
326
|
-
return fn(() => {});
|
|
325
|
+
return fn(() => {});
|
|
327
326
|
};
|
|
328
327
|
|
|
329
328
|
/**
|
|
@@ -48,7 +48,7 @@ export class HostEventClient {
|
|
|
48
48
|
|
|
49
49
|
if (!response) {
|
|
50
50
|
const error = `No answer found${parameters.vizId ? ` for vizId: ${parameters.vizId}` : ''}.`;
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
throw { error };
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -57,7 +57,7 @@ export class HostEventClient {
|
|
|
57
57
|
|| (response.value as any)?.error;
|
|
58
58
|
|
|
59
59
|
if (errors) {
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
throw { error: response.error };
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -914,7 +914,8 @@ describe('Liveboard/viz embed tests', () => {
|
|
|
914
914
|
} as LiveboardViewConfig);
|
|
915
915
|
liveboardEmbed.render();
|
|
916
916
|
await executeAfterWait(() => {
|
|
917
|
-
// URL should be:
|
|
917
|
+
// URL should be:
|
|
918
|
+
// #/embed/viz/{id}/tab/{tabId}/{vizId}?view={viewId}
|
|
918
919
|
expect(getIFrameSrc()).toMatch(
|
|
919
920
|
new RegExp(
|
|
920
921
|
`#/embed/viz/${liveboardId}/tab/${activeTabId}/${vizId}\\?view=${personalizedViewId}`,
|
|
@@ -1010,7 +1011,8 @@ describe('Liveboard/viz embed tests', () => {
|
|
|
1010
1011
|
} as LiveboardViewConfig);
|
|
1011
1012
|
liveboardEmbed.render();
|
|
1012
1013
|
await executeAfterWait(() => {
|
|
1013
|
-
// URL: #/embed/viz/{id}/tab/{tabId}?view={viewId} (view at
|
|
1014
|
+
// URL: #/embed/viz/{id}/tab/{tabId}?view={viewId} (view at
|
|
1015
|
+
// END, not middle)
|
|
1014
1016
|
expect(getIFrameSrc()).toMatch(
|
|
1015
1017
|
new RegExp(
|
|
1016
1018
|
`#/embed/viz/${liveboardId}/tab/${activeTabId}\\?view=${workaroundViewId}`,
|
package/src/embed/liveboard.ts
CHANGED
|
@@ -371,7 +371,8 @@ export interface LiveboardViewConfig extends BaseViewConfig, LiveboardOtherViewC
|
|
|
371
371
|
*/
|
|
372
372
|
isLiveboardStylingAndGroupingEnabled?: boolean;
|
|
373
373
|
/**
|
|
374
|
-
* This flag is used to enable/disable the png embedding of liveboard in scheduled
|
|
374
|
+
* This flag is used to enable/disable the png embedding of liveboard in scheduled
|
|
375
|
+
* mails
|
|
375
376
|
*
|
|
376
377
|
* Supported embed types: `AppEmbed`, `LiveboardEmbed`
|
|
377
378
|
* @type {boolean}
|
|
@@ -730,8 +731,9 @@ export class LiveboardEmbed extends V1Embed {
|
|
|
730
731
|
activeTabId: string,
|
|
731
732
|
personalizedViewId?: string,
|
|
732
733
|
) {
|
|
733
|
-
// Extract view from liveboardId if passed along with it (legacy
|
|
734
|
-
// View must be appended as query param at the end, not
|
|
734
|
+
// Extract view from liveboardId if passed along with it (legacy
|
|
735
|
+
// approach) View must be appended as query param at the end, not
|
|
736
|
+
// embedded in path
|
|
735
737
|
let liveboardGuid = liveboardId;
|
|
736
738
|
let legacyViewId: string | undefined;
|
|
737
739
|
|
|
@@ -742,7 +744,8 @@ export class LiveboardEmbed extends V1Embed {
|
|
|
742
744
|
legacyViewId = params.get('view') || undefined;
|
|
743
745
|
}
|
|
744
746
|
|
|
745
|
-
// personalizedViewId takes precedence over legacyViewId (when passed
|
|
747
|
+
// personalizedViewId takes precedence over legacyViewId (when passed
|
|
748
|
+
// as part of liveboardId)
|
|
746
749
|
const effectiveViewId = personalizedViewId || legacyViewId;
|
|
747
750
|
|
|
748
751
|
let suffix = `/embed/viz/${liveboardGuid}`;
|
package/src/embed/sage.ts
CHANGED
|
@@ -147,7 +147,7 @@ export class SageEmbed extends V1Embed {
|
|
|
147
147
|
*/
|
|
148
148
|
protected viewConfig: SageViewConfig;
|
|
149
149
|
|
|
150
|
-
|
|
150
|
+
|
|
151
151
|
constructor(domSelector: DOMSelector, viewConfig: SageViewConfig) {
|
|
152
152
|
viewConfig.embedComponentType = 'SageEmbed';
|
|
153
153
|
super(domSelector, viewConfig);
|
package/src/embed/search.spec.ts
CHANGED
|
@@ -527,7 +527,7 @@ describe('Search embed tests', () => {
|
|
|
527
527
|
test('should set dataPanelCustomGroupsAccordionInitialState to EXPAND_FIRST when passed', async () => {
|
|
528
528
|
const searchEmbed = new SearchBarEmbed(getRootEl() as any, {
|
|
529
529
|
...defaultViewConfig,
|
|
530
|
-
|
|
530
|
+
|
|
531
531
|
});
|
|
532
532
|
searchEmbed.render();
|
|
533
533
|
await executeAfterWait(() => {
|
|
@@ -541,7 +541,7 @@ describe('Search embed tests', () => {
|
|
|
541
541
|
test('should set dataPanelCustomGroupsAccordionInitialState to EXPAND_FIRST when passed', async () => {
|
|
542
542
|
const searchEmbed = new SearchEmbed(getRootEl(), {
|
|
543
543
|
...defaultViewConfig,
|
|
544
|
-
|
|
544
|
+
|
|
545
545
|
dataPanelCustomGroupsAccordionInitialState:
|
|
546
546
|
DataPanelCustomColumnGroupsAccordionState.EXPAND_FIRST,
|
|
547
547
|
});
|
|
@@ -3056,7 +3056,8 @@ describe('Unit test case for ts embed', () => {
|
|
|
3056
3056
|
expect.any(Object),
|
|
3057
3057
|
true
|
|
3058
3058
|
);
|
|
3059
|
-
// Check that logger.error was called with the token refresh
|
|
3059
|
+
// Check that logger.error was called with the token refresh
|
|
3060
|
+
// error
|
|
3060
3061
|
const errorCalls = (logger.error as jest.Mock).mock.calls.filter(
|
|
3061
3062
|
(call) => call[0]?.includes(ERROR_MESSAGE.INVALID_TOKEN_ERROR) && call[0]?.includes('Token fetch failed')
|
|
3062
3063
|
);
|