@thoughtspot/visual-embed-sdk
Version:
ThoughtSpot Embed SDK
145 lines • 5.93 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.startAutoMCPFrameRenderer = void 0;
const types_1 = require("../types");
const ts_embed_1 = require("./ts-embed");
const utils_1 = require("../utils");
/**
* Starts an automatic renderer that watches the DOM for iframes containing
* the `tsmcp=true` query parameter and replaces them with fully configured
* ThoughtSpot embed iframes. The query parameter is automatically added by
* the ThoughtSpot MCP server.
*
* A {@link MutationObserver} is set up on `document.body` to detect both
* directly added iframes and iframes nested within added container elements.
* Each matching iframe is replaced in-place with a new ThoughtSpot embed
* iframe that merges the original iframe's query parameters with the SDK
* embed parameters.
*
* Call {@link MutationObserver.disconnect | observer.disconnect()} on the
* returned observer to stop monitoring the DOM.
*
* @param viewConfig - Optional configuration for the auto-rendered embeds.
* Accepts all properties from {@link AutoMCPFrameRendererViewConfig}.
* Defaults to an empty config.
* @returns A {@link MutationObserver} instance that is actively observing
* `document.body`. Disconnect it when monitoring is no longer needed.
*
* @example
* ```js
* import { startAutoMCPFrameRenderer } from '@thoughtspot/visual-embed-sdk';
*
* // Start watching the DOM for tsmcp iframes
* const observer = startAutoMCPFrameRenderer({
* // optional view config overrides
* });
*
* // Later, stop watching
* observer.disconnect();
* ```
*
* @example
* Detailed example of how to use the auto-frame renderer:
* [Python React Agent Simple UI](https://github.com/thoughtspot/developer-examples/tree/main/mcp/python-react-agent-simple-ui)
*/
function startAutoMCPFrameRenderer(viewConfig = {}) {
const replaceWithMCPIframe = (iframe) => {
const autoMCPFrameRenderer = new AutoFrameRenderer(viewConfig);
autoMCPFrameRenderer.replaceIframe(iframe);
};
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of Array.from(mutation.addedNodes)) {
if (node instanceof HTMLIFrameElement && isTSMCPIframe(node)) {
replaceWithMCPIframe(node);
}
if (node instanceof HTMLElement) {
node.querySelectorAll('iframe').forEach((iframe) => {
if (isTSMCPIframe(iframe)) {
replaceWithMCPIframe(iframe);
}
});
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
return observer;
}
exports.startAutoMCPFrameRenderer = startAutoMCPFrameRenderer;
function isTSMCPIframe(iframe) {
try {
const url = new URL(iframe.src);
return url.searchParams.get(types_1.Param.Tsmcp) === 'true';
}
catch (e) {
// The iframe src might not be a valid URL (e.g., 'about:blank').
return false;
}
}
/**
* Embed component that automatically replaces a plain iframe with a
* ThoughtSpot embed iframe. It merges the SDK's embed parameters with
* the original iframe's query parameters (stripping the `tsmcp` marker)
* and swaps the original iframe element in the DOM.
*
* This class is used internally by {@link startAutoMCPFrameRenderer} and
* is not intended to be instantiated directly.
*/
class AutoFrameRenderer extends ts_embed_1.TsEmbed {
constructor(viewConfig) {
viewConfig.embedComponentType = 'auto-frame-renderer';
const container = document.createElement('div');
super(container, viewConfig);
this.viewConfig = viewConfig;
}
/**
* Builds the final iframe `src` by merging the SDK embed parameters
* with the query parameters already present on the source iframe URL.
* The `tsmcp` marker param is removed so it does not propagate to the
* ThoughtSpot application.
*
* @param sourceSrc - The original iframe's `src` URL string.
* @returns The constructed URL to use for the ThoughtSpot embed iframe.
*/
getMCPIframeSrc(sourceSrc) {
const queryParams = this.getEmbedParamsObject();
const sourceURL = new URL(sourceSrc);
const existingQueryParams = sourceURL.searchParams;
const existingQueryParamsObject = Object.fromEntries(existingQueryParams);
delete existingQueryParamsObject[types_1.Param.Tsmcp];
const mergedQueryParams = { ...queryParams, ...existingQueryParamsObject };
const mergedQueryParamsString = (0, utils_1.getQueryParamString)(mergedQueryParams);
const frameSrc = `${this.getEmbedBasePath(mergedQueryParamsString)}${sourceURL.hash.replace('#', '')}`;
return frameSrc;
}
/**
* Overrides the base insertion behavior so the new embed iframe
* replaces the original iframe in-place rather than being appended
* to a container element. Falls back to the default behavior when
* no iframe has been set for replacement.
*/
handleInsertionIntoDOM(child) {
if (this.frameToReplace) {
this.frameToReplace.replaceWith(child);
}
else {
super.handleInsertionIntoDOM(child);
}
}
/**
* Replaces the given iframe with a new ThoughtSpot embed iframe.
*
* The original iframe's `src` is used to derive the embed URL, and
* once the new iframe is rendered it takes the original's place in
* the DOM tree.
*
* @param iframe - The existing `<iframe>` element to replace.
*/
async replaceIframe(iframe) {
this.frameToReplace = iframe;
const src = this.getMCPIframeSrc(iframe.src);
await this.renderIFrame(src);
}
}
//# sourceMappingURL=auto-frame-renderer.js.map