@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.
Files changed (169) hide show
  1. package/cjs/package.json +1 -1
  2. package/cjs/src/auth.js +0 -3
  3. package/cjs/src/auth.js.map +1 -1
  4. package/cjs/src/auth.spec.js +1 -3
  5. package/cjs/src/auth.spec.js.map +1 -1
  6. package/cjs/src/authToken.js +0 -1
  7. package/cjs/src/authToken.js.map +1 -1
  8. package/cjs/src/authToken.spec.js +2 -1
  9. package/cjs/src/authToken.spec.js.map +1 -1
  10. package/cjs/src/embed/app.d.ts +2 -1
  11. package/cjs/src/embed/app.d.ts.map +1 -1
  12. package/cjs/src/embed/app.js.map +1 -1
  13. package/cjs/src/embed/auto-frame-renderer.d.ts +41 -0
  14. package/cjs/src/embed/auto-frame-renderer.d.ts.map +1 -0
  15. package/cjs/src/embed/auto-frame-renderer.js +145 -0
  16. package/cjs/src/embed/auto-frame-renderer.js.map +1 -0
  17. package/cjs/src/embed/auto-frame-renderer.spec.d.ts +2 -0
  18. package/cjs/src/embed/auto-frame-renderer.spec.d.ts.map +1 -0
  19. package/cjs/src/embed/auto-frame-renderer.spec.js +195 -0
  20. package/cjs/src/embed/auto-frame-renderer.spec.js.map +1 -0
  21. package/cjs/src/embed/base.d.ts.map +1 -1
  22. package/cjs/src/embed/base.js +4 -7
  23. package/cjs/src/embed/base.js.map +1 -1
  24. package/cjs/src/embed/hostEventClient/host-event-client.js +0 -2
  25. package/cjs/src/embed/hostEventClient/host-event-client.js.map +1 -1
  26. package/cjs/src/embed/liveboard.d.ts +2 -1
  27. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  28. package/cjs/src/embed/liveboard.js +5 -3
  29. package/cjs/src/embed/liveboard.js.map +1 -1
  30. package/cjs/src/embed/liveboard.spec.js +4 -2
  31. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  32. package/cjs/src/embed/sage.js +0 -1
  33. package/cjs/src/embed/sage.js.map +1 -1
  34. package/cjs/src/embed/search.spec.js +0 -2
  35. package/cjs/src/embed/search.spec.js.map +1 -1
  36. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  37. package/cjs/src/embed/ts-embed.js +3 -3
  38. package/cjs/src/embed/ts-embed.js.map +1 -1
  39. package/cjs/src/embed/ts-embed.spec.js +2 -1
  40. package/cjs/src/embed/ts-embed.spec.js.map +1 -1
  41. package/cjs/src/index.d.ts +3 -2
  42. package/cjs/src/index.d.ts.map +1 -1
  43. package/cjs/src/index.js +3 -1
  44. package/cjs/src/index.js.map +1 -1
  45. package/cjs/src/react/index.js +0 -1
  46. package/cjs/src/react/index.js.map +1 -1
  47. package/cjs/src/react/index.spec.js +6 -3
  48. package/cjs/src/react/index.spec.js.map +1 -1
  49. package/cjs/src/types.d.ts +56 -32
  50. package/cjs/src/types.d.ts.map +1 -1
  51. package/cjs/src/types.js +34 -18
  52. package/cjs/src/types.js.map +1 -1
  53. package/cjs/src/utils/graphql/answerService/answerService.d.ts +5 -4
  54. package/cjs/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
  55. package/cjs/src/utils/graphql/answerService/answerService.js +7 -11
  56. package/cjs/src/utils/graphql/answerService/answerService.js.map +1 -1
  57. package/cjs/src/utils/graphql/preview-service.js +0 -1
  58. package/cjs/src/utils/graphql/preview-service.js.map +1 -1
  59. package/cjs/src/utils/processData.js +0 -5
  60. package/cjs/src/utils/processData.js.map +1 -1
  61. package/dist/{index-DkizS4xM.js → index-DyX-x6uN.js} +1 -1
  62. package/dist/src/embed/app.d.ts +2 -1
  63. package/dist/src/embed/app.d.ts.map +1 -1
  64. package/dist/src/embed/auto-frame-renderer.d.ts +41 -0
  65. package/dist/src/embed/auto-frame-renderer.d.ts.map +1 -0
  66. package/dist/src/embed/auto-frame-renderer.spec.d.ts +2 -0
  67. package/dist/src/embed/auto-frame-renderer.spec.d.ts.map +1 -0
  68. package/dist/src/embed/base.d.ts.map +1 -1
  69. package/dist/src/embed/liveboard.d.ts +2 -1
  70. package/dist/src/embed/liveboard.d.ts.map +1 -1
  71. package/dist/src/embed/ts-embed.d.ts.map +1 -1
  72. package/dist/src/index.d.ts +3 -2
  73. package/dist/src/index.d.ts.map +1 -1
  74. package/dist/src/types.d.ts +56 -32
  75. package/dist/src/types.d.ts.map +1 -1
  76. package/dist/src/utils/graphql/answerService/answerService.d.ts +5 -4
  77. package/dist/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
  78. package/dist/tsembed-react.es.js +55 -57
  79. package/dist/tsembed-react.js +54 -56
  80. package/dist/tsembed.es.js +194 -57
  81. package/dist/tsembed.js +193 -55
  82. package/dist/visual-embed-sdk-react-full.d.ts +54 -33
  83. package/dist/visual-embed-sdk-react.d.ts +54 -33
  84. package/dist/visual-embed-sdk.d.ts +103 -38
  85. package/lib/package.json +1 -1
  86. package/lib/src/auth.js +0 -3
  87. package/lib/src/auth.js.map +1 -1
  88. package/lib/src/auth.spec.js +1 -3
  89. package/lib/src/auth.spec.js.map +1 -1
  90. package/lib/src/authToken.js +0 -1
  91. package/lib/src/authToken.js.map +1 -1
  92. package/lib/src/authToken.spec.js +2 -1
  93. package/lib/src/authToken.spec.js.map +1 -1
  94. package/lib/src/embed/app.d.ts +2 -1
  95. package/lib/src/embed/app.d.ts.map +1 -1
  96. package/lib/src/embed/app.js.map +1 -1
  97. package/lib/src/embed/auto-frame-renderer.d.ts +41 -0
  98. package/lib/src/embed/auto-frame-renderer.d.ts.map +1 -0
  99. package/lib/src/embed/auto-frame-renderer.js +141 -0
  100. package/lib/src/embed/auto-frame-renderer.js.map +1 -0
  101. package/lib/src/embed/auto-frame-renderer.spec.d.ts +2 -0
  102. package/lib/src/embed/auto-frame-renderer.spec.d.ts.map +1 -0
  103. package/lib/src/embed/auto-frame-renderer.spec.js +192 -0
  104. package/lib/src/embed/auto-frame-renderer.spec.js.map +1 -0
  105. package/lib/src/embed/base.d.ts.map +1 -1
  106. package/lib/src/embed/base.js +4 -7
  107. package/lib/src/embed/base.js.map +1 -1
  108. package/lib/src/embed/hostEventClient/host-event-client.js +0 -2
  109. package/lib/src/embed/hostEventClient/host-event-client.js.map +1 -1
  110. package/lib/src/embed/liveboard.d.ts +2 -1
  111. package/lib/src/embed/liveboard.d.ts.map +1 -1
  112. package/lib/src/embed/liveboard.js +5 -3
  113. package/lib/src/embed/liveboard.js.map +1 -1
  114. package/lib/src/embed/liveboard.spec.js +4 -2
  115. package/lib/src/embed/liveboard.spec.js.map +1 -1
  116. package/lib/src/embed/sage.js +0 -1
  117. package/lib/src/embed/sage.js.map +1 -1
  118. package/lib/src/embed/search.spec.js +0 -2
  119. package/lib/src/embed/search.spec.js.map +1 -1
  120. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  121. package/lib/src/embed/ts-embed.js +3 -3
  122. package/lib/src/embed/ts-embed.js.map +1 -1
  123. package/lib/src/embed/ts-embed.spec.js +2 -1
  124. package/lib/src/embed/ts-embed.spec.js.map +1 -1
  125. package/lib/src/index.d.ts +3 -2
  126. package/lib/src/index.d.ts.map +1 -1
  127. package/lib/src/index.js +1 -0
  128. package/lib/src/index.js.map +1 -1
  129. package/lib/src/react/index.js +0 -1
  130. package/lib/src/react/index.js.map +1 -1
  131. package/lib/src/react/index.spec.js +6 -3
  132. package/lib/src/react/index.spec.js.map +1 -1
  133. package/lib/src/types.d.ts +56 -32
  134. package/lib/src/types.d.ts.map +1 -1
  135. package/lib/src/types.js +34 -18
  136. package/lib/src/types.js.map +1 -1
  137. package/lib/src/utils/graphql/answerService/answerService.d.ts +5 -4
  138. package/lib/src/utils/graphql/answerService/answerService.d.ts.map +1 -1
  139. package/lib/src/utils/graphql/answerService/answerService.js +7 -11
  140. package/lib/src/utils/graphql/answerService/answerService.js.map +1 -1
  141. package/lib/src/utils/graphql/preview-service.js +0 -1
  142. package/lib/src/utils/graphql/preview-service.js.map +1 -1
  143. package/lib/src/utils/processData.js +0 -5
  144. package/lib/src/utils/processData.js.map +1 -1
  145. package/lib/src/visual-embed-sdk.d.ts +11466 -0
  146. package/package.json +1 -1
  147. package/src/auth.spec.ts +1 -1
  148. package/src/auth.ts +3 -3
  149. package/src/authToken.spec.ts +2 -1
  150. package/src/authToken.ts +1 -1
  151. package/src/embed/app.ts +2 -1
  152. package/src/embed/auto-frame-renderer.spec.ts +266 -0
  153. package/src/embed/auto-frame-renderer.ts +152 -0
  154. package/src/embed/base.spec.ts +1 -1
  155. package/src/embed/base.ts +7 -8
  156. package/src/embed/hostEventClient/host-event-client.ts +2 -2
  157. package/src/embed/liveboard.spec.ts +4 -2
  158. package/src/embed/liveboard.ts +7 -4
  159. package/src/embed/sage.ts +1 -1
  160. package/src/embed/search.spec.ts +2 -2
  161. package/src/embed/ts-embed.spec.ts +2 -1
  162. package/src/embed/ts-embed.ts +3 -3
  163. package/src/index.ts +3 -0
  164. package/src/react/index.spec.tsx +6 -3
  165. package/src/react/index.tsx +1 -1
  166. package/src/types.ts +57 -32
  167. package/src/utils/graphql/answerService/answerService.ts +8 -7
  168. package/src/utils/graphql/preview-service.ts +1 -1
  169. package/src/utils/processData.ts +5 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thoughtspot/visual-embed-sdk",
3
- "version": "1.46.2",
3
+ "version": "1.46.3",
4
4
  "description": "ThoughtSpot Embed SDK",
5
5
  "module": "lib/src/index.js",
6
6
  "main": "dist/tsembed.js",
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
- // eslint-disable-next-line prefer-promise-reject-errors, implicit-arrow-linebreak
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
- // eslint-disable-next-line import/no-mutable-exports
22
+
23
23
  export let loggedInStatus = false;
24
- // eslint-disable-next-line import/no-mutable-exports
24
+
25
25
  export let samlAuthWindow: Window = null;
26
- // eslint-disable-next-line import/no-mutable-exports
26
+
27
27
  export let samlCompletionPromise: Promise<void> = null;
28
28
 
29
29
  let releaseVersion = '';
@@ -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 disableTokenVerification is true)
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
- // eslint-disable-next-line no-alert
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 mails
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
+
@@ -1,4 +1,4 @@
1
- /* eslint-disable no-console */
1
+
2
2
  /* eslint-disable @typescript-eslint/no-shadow */
3
3
  import EventEmitter from 'eventemitter3';
4
4
  import { EmbedConfig } from '../index';
package/src/embed/base.ts CHANGED
@@ -1,5 +1,5 @@
1
- /* eslint-disable camelcase */
2
- /* eslint-disable import/no-mutable-exports */
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
- // eslint-disable-next-line no-console
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 context.
146
- // This should solve the focus behaviours inside the iframe from
147
- // interfering with main body.
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(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
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
- // eslint-disable-next-line no-throw-literal
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
- // eslint-disable-next-line no-throw-literal
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: #/embed/viz/{id}/tab/{tabId}/{vizId}?view={viewId}
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 END, not middle)
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}`,
@@ -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 mails
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 approach)
734
- // View must be appended as query param at the end, not embedded in path
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 as part of liveboardId)
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
- // eslint-disable-next-line no-useless-constructor
150
+
151
151
  constructor(domSelector: DOMSelector, viewConfig: SageViewConfig) {
152
152
  viewConfig.embedComponentType = 'SageEmbed';
153
153
  super(domSelector, viewConfig);
@@ -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
- // eslint-disable-next-line max-len
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
- // eslint-disable-next-line max-len
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 error
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
  );