blob: 7af13aa9085624656ce84affa432434ad15fdf1f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
DASHBOARD_UI_FILTER_CONFIG_URL_PARAM_KEY,
IFRAME_COMMS_MESSAGE_TYPE,
} from './const';
// We can swap this out for the actual switchboard package once it gets published
import { Switchboard } from '@superset-ui/switchboard';
import { getGuestTokenRefreshTiming } from './guestTokenRefresh';
/**
* The function to fetch a guest token from your Host App's backend server.
* The Host App backend must supply an API endpoint
* which returns a guest token with appropriate resource access.
*/
export type GuestTokenFetchFn = () => Promise<string>;
export type UiConfigType = {
hideTitle?: boolean;
hideTab?: boolean;
hideChartControls?: boolean;
emitDataMasks?: boolean;
filters?: {
[key: string]: boolean | undefined;
visible?: boolean;
expanded?: boolean;
};
urlParams?: {
[key: string]: any;
};
showRowLimitWarning?: boolean;
};
export type EmbedDashboardParams = {
/** The id provided by the embed configuration UI in Superset */
id: string;
/** The domain where Superset can be located, with protocol, such as: https://superset.example.com */
supersetDomain: string;
/** The html element within which to mount the iframe */
mountPoint: HTMLElement;
/** A function to fetch a guest token from the Host App's backend server */
fetchGuestToken: GuestTokenFetchFn;
/** The dashboard UI config: hideTitle, hideTab, hideChartControls, filters.visible, filters.expanded **/
dashboardUiConfig?: UiConfigType;
/** Are we in debug mode? */
debug?: boolean;
/** The iframe title attribute */
iframeTitle?: string;
/** additional iframe sandbox attributes ex (allow-top-navigation, allow-popups-to-escape-sandbox) **/
iframeSandboxExtras?: string[];
/** force a specific refererPolicy to be used in the iframe request **/
referrerPolicy?: ReferrerPolicy;
};
export type Size = {
width: number;
height: number;
};
export type ObserveDataMaskCallbackFn = (
dataMask: Record<string, any> & {
crossFiltersChanged: boolean;
nativeFiltersChanged: boolean;
},
) => void;
export type EmbeddedDashboard = {
getScrollSize: () => Promise<Size>;
unmount: () => void;
getDashboardPermalink: (anchor: string) => Promise<string>;
getActiveTabs: () => Promise<string[]>;
observeDataMask: (
callbackFn: ObserveDataMaskCallbackFn,
) => void;
getDataMask: () => Record<string, any>;
setThemeConfig: (themeConfig: Record<string, any>) => void;
};
/**
* Embeds a Superset dashboard into the page using an iframe.
*/
export async function embedDashboard({
id,
supersetDomain,
mountPoint,
fetchGuestToken,
dashboardUiConfig,
debug = false,
iframeTitle = 'Embedded Dashboard',
iframeSandboxExtras = [],
referrerPolicy,
}: EmbedDashboardParams): Promise<EmbeddedDashboard> {
function log(...info: unknown[]) {
if (debug) {
console.debug(`[superset-embedded-sdk][dashboard ${id}]`, ...info);
}
}
log('embedding');
if (supersetDomain.endsWith('/')) {
supersetDomain = supersetDomain.slice(0, -1);
}
function calculateConfig() {
let configNumber = 0;
if (dashboardUiConfig) {
if (dashboardUiConfig.hideTitle) {
configNumber += 1;
}
if (dashboardUiConfig.hideTab) {
configNumber += 2;
}
if (dashboardUiConfig.hideChartControls) {
configNumber += 8;
}
if (dashboardUiConfig.emitDataMasks) {
configNumber += 16;
}
if (dashboardUiConfig.showRowLimitWarning) {
configNumber += 32;
}
}
return configNumber;
}
async function mountIframe(): Promise<Switchboard> {
return new Promise(resolve => {
const iframe = document.createElement('iframe');
const dashboardConfigUrlParams = dashboardUiConfig
? { uiConfig: `${calculateConfig()}` }
: undefined;
const filterConfig = dashboardUiConfig?.filters || {};
const filterConfigKeys = Object.keys(filterConfig);
const filterConfigUrlParams = Object.fromEntries(
filterConfigKeys.map(key => [
DASHBOARD_UI_FILTER_CONFIG_URL_PARAM_KEY[key],
filterConfig[key],
]),
);
// Allow url query parameters from dashboardUiConfig.urlParams to override the ones from filterConfig
const urlParams = {
...dashboardConfigUrlParams,
...filterConfigUrlParams,
...dashboardUiConfig?.urlParams,
};
const urlParamsString = Object.keys(urlParams).length
? '?' + new URLSearchParams(urlParams).toString()
: '';
// set up the iframe's sandbox configuration
iframe.sandbox.add('allow-same-origin'); // needed for postMessage to work
iframe.sandbox.add('allow-scripts'); // obviously the iframe needs scripts
iframe.sandbox.add('allow-presentation'); // for fullscreen charts
iframe.sandbox.add('allow-downloads'); // for downloading charts as image
iframe.sandbox.add('allow-forms'); // for forms to submit
iframe.sandbox.add('allow-popups'); // for exporting charts as csv
// additional sandbox props
iframeSandboxExtras.forEach((key: string) => {
iframe.sandbox.add(key);
});
// force a specific refererPolicy to be used in the iframe request
if (referrerPolicy) {
iframe.referrerPolicy = referrerPolicy;
}
// add the event listener before setting src, to be 100% sure that we capture the load event
iframe.addEventListener('load', () => {
// MessageChannel allows us to send and receive messages smoothly between our window and the iframe
// See https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API
const commsChannel = new MessageChannel();
const ourPort = commsChannel.port1;
const theirPort = commsChannel.port2;
// Send one of the message channel ports to the iframe to initialize embedded comms
// See https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
// we know the content window isn't null because we are in the load event handler.
iframe.contentWindow!.postMessage(
{ type: IFRAME_COMMS_MESSAGE_TYPE, handshake: 'port transfer' },
supersetDomain,
[theirPort],
);
log('sent message channel to the iframe');
// return our port from the promise
resolve(
new Switchboard({
port: ourPort,
name: 'superset-embedded-sdk',
debug,
}),
);
});
iframe.src = `${supersetDomain}/embedded/${id}${urlParamsString}`;
iframe.title = iframeTitle;
//@ts-ignore
mountPoint.replaceChildren(iframe);
log('placed the iframe');
});
}
const [guestToken, ourPort]: [string, Switchboard] = await Promise.all([
fetchGuestToken(),
mountIframe(),
]);
ourPort.emit('guestToken', { guestToken });
log('sent guest token');
async function refreshGuestToken() {
const newGuestToken = await fetchGuestToken();
ourPort.emit('guestToken', { guestToken: newGuestToken });
setTimeout(refreshGuestToken, getGuestTokenRefreshTiming(newGuestToken));
}
setTimeout(refreshGuestToken, getGuestTokenRefreshTiming(guestToken));
function unmount() {
log('unmounting');
//@ts-ignore
mountPoint.replaceChildren();
}
const getScrollSize = () => ourPort.get<Size>('getScrollSize');
const getDashboardPermalink = (anchor: string) =>
ourPort.get<string>('getDashboardPermalink', { anchor });
const getActiveTabs = () => ourPort.get<string[]>('getActiveTabs');
const getDataMask = () => ourPort.get<Record<string, any>>('getDataMask');
const observeDataMask = (
callbackFn: ObserveDataMaskCallbackFn,
) => {
ourPort.start();
ourPort.defineMethod('observeDataMask', callbackFn);
};
// TODO: Add proper types once theming branch is merged
const setThemeConfig = async (themeConfig: Record<string, any>): Promise<void> => {
try {
ourPort.emit('setThemeConfig', { themeConfig });
log('Theme config sent successfully (or at least message dispatched)');
} catch (error) {
log(
'Error sending theme config. Ensure the iframe side implements the "setThemeConfig" method.',
);
throw error;
}
};
return {
getScrollSize,
unmount,
getDashboardPermalink,
getActiveTabs,
observeDataMask,
getDataMask,
setThemeConfig
};
}