@mongodb-js/charts-embed-dom
Version:
JavaScript library for embedding MongoDB Charts
1,314 lines (1,048 loc) • 38.7 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var chatty = require('@looker/chatty');
var bson = require('bson');
var _isEqual = require('lodash/isEqual');
var _isEmpty = require('lodash/isEmpty');
function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
var _isEqual__default = /*#__PURE__*/_interopDefault(_isEqual);
var _isEmpty__default = /*#__PURE__*/_interopDefault(_isEmpty);
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
// Given an object `Target`, find all property names of type `Type`
// Given an object `Target`, filter out all properties that aren't of type `Type`
function createElement(name) {
let props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
let children = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
const element = document.createElement(name);
for (const [name, value] of Object.entries(props)) {
if (name === 'style') {
Object.assign(element.style, props.style);
} else {
element.setAttribute(name, value);
}
}
for (const child of Array.isArray(children) ? children : [children]) {
element.append(child);
}
return element;
}
let THEME;
(function (THEME) {
THEME["DARK"] = "dark";
THEME["LIGHT"] = "light";
})(THEME || (THEME = {}));
let SCALING;
(function (SCALING) {
SCALING["FIXED"] = "fixed";
SCALING["SCALE"] = "scale";
})(SCALING || (SCALING = {}));
let ENCODING;
(function (ENCODING) {
ENCODING["BASE64"] = "base64";
ENCODING["BINARY"] = "binary";
})(ENCODING || (ENCODING = {}));
const SDK_PROTOCOL_CHART = '3';
const SDK_PROTOCOL_DASHBOARD = '3';
const isPlainObject = value => {
return typeof value === 'object' && value !== null && !Array.isArray(value);
};
/**
* Retrieves embed options that are shared.
*
* Validates the values passed in as well.
*/
const getSharedEmbedOptions = options => {
const {
background,
baseUrl,
autoRefresh,
maxDataAge,
width,
height,
theme,
showAttribution,
getUserToken,
filter
} = options;
if (typeof baseUrl !== 'string' || baseUrl.length === 0) {
throw new Error('Base URL must be a valid URL');
}
if (background !== undefined && typeof background !== 'string') {
throw new Error('background must be a string if specified');
}
if (autoRefresh !== undefined && typeof autoRefresh !== 'boolean') {
throw new Error('autoRefresh must be a boolean if specified');
}
if (maxDataAge !== undefined && typeof maxDataAge !== 'number') {
throw new Error('maxDataAge must be a number if specified');
}
if (width !== undefined && !['number', 'string'].includes(typeof width)) {
throw new Error('Width must be a string or number if specified');
}
if (height !== undefined && !['number', 'string'].includes(typeof height)) {
throw new Error('Height must be a string or number if specified');
}
if (theme !== undefined && typeof theme !== 'string') {
throw new Error('Theme must be a string if specified');
}
if (showAttribution !== undefined && typeof showAttribution !== 'boolean') {
throw new Error('Attribution must be a boolean value if specified');
}
if (getUserToken !== undefined && typeof getUserToken !== 'function') {
throw new Error('getUserToken must be a function');
}
if (filter !== undefined && !isPlainObject(filter)) {
throw new Error('Filter must be an object if specified');
}
return {
background,
baseUrl,
autoRefresh,
maxDataAge,
width,
height,
theme,
showAttribution,
getUserToken,
filter
};
};
const getPathname = (url, pathname) => {
return [url.pathname, url.pathname.slice(-1) === '/' ? '' : '/', // Add trailing slash if not there
pathname].join('');
};
/**
* Constructs the chart iframe URL from the baseUrl, chartId & tenantId
*/
const getChartUrl = options => {
try {
const url = new URL(options.baseUrl);
url.pathname = getPathname(url, 'embed/charts');
url.search = `id=${options.chartId}&sdk=${SDK_PROTOCOL_CHART}`;
if (options.autoRefresh === false) {
url.search += `&autorefresh=false`;
} else if (options.autoRefresh === undefined) {
url.search += options.refreshInterval ? `&autorefresh=${options.refreshInterval}` : '';
}
if (options.maxDataAge !== undefined) {
url.search += `&maxDataAge=${options.maxDataAge}`;
}
if (options.filter) {
url.search += `&filter=${encodeURIComponent(bson.EJSON.stringify(options.filter, {
relaxed: false
}))}`;
}
if (options.theme) {
url.search += `&theme=${options.theme}`;
}
if (options.showAttribution === false) {
url.search += `&attribution=false`;
}
return url.toString();
} catch (e) {
throw new Error('Base URL must be a valid URL');
}
};
/**
* Constructs the dashboard iframe URL from the baseUrl, dashboardId & tenantId
*/
const getDashboardUrl = options => {
try {
const url = new URL(options.baseUrl);
url.pathname = getPathname(url, 'embed/dashboards');
url.search = `id=${options.dashboardId}&sdk=${SDK_PROTOCOL_DASHBOARD}`;
if (options.autoRefresh === false) {
url.search += `&autoRefresh=false`;
}
if (options.maxDataAge !== undefined) {
url.search += `&maxDataAge=${options.maxDataAge}`;
}
if (options.showTitleAndDesc === true) {
url.search += `&showTitleAndDesc=true`;
}
if (options.widthMode) {
url.search += `&scalingWidth=${options.widthMode}`;
}
if (options.heightMode) {
url.search += `&scalingHeight=${options.heightMode}`;
}
if (options.theme) {
url.search += `&theme=${options.theme}`;
}
if (options.chartsBackground) {
url.search += `&chartsBackground=${options.chartsBackground}`;
}
if (options.background) {
url.search += `&background=${options.background}`;
}
if (options.showAttribution === false) {
url.search += `&attribution=false`;
}
if (options.filter) {
url.search += `&filter=${encodeURIComponent(bson.EJSON.stringify(options.filter, {
relaxed: false
}))}`;
}
return url.toString();
} catch (e) {
throw new Error('Base URL must be a valid URL');
}
};
/*
Parses a CSS Measurement from an unknown value
- if it's a string, we trust that it is well-formed
- if it's a number, we assume the units are pixels
- otherwise we return null
*/
const parseCSSMeasurement = value => {
if (typeof value === 'string') return value;
if (typeof value === 'number') return `${value}px`;
return null;
};
/**
* Returns the background after validation checks
* or default background based on theme if not set
*/
const getBackground = (background, theme, lightBackground, darkBackground) => {
if (typeof background === 'string' && background.length > 0) return background;
if (theme === 'dark') return darkBackground;
return lightBackground;
};
class BaseEmbedItem {
constructor() {
_defineProperty(this, "iframe", void 0);
_defineProperty(this, "connection", void 0);
_defineProperty(this, "name", void 0);
_defineProperty(this, "ERRORS", void 0);
_defineProperty(this, "COLOUR", void 0);
_defineProperty(this, "options", void 0);
}
/**
* Renders an embeddable item into the given `container`.
*
* This method should only be called once, and successive attempts to call `render`
* will fail with an error.
*
* @returns a promise that will resolve once the item has successfully been embedded
*/
async render(container) {
if (this.iframe) {
throw new Error(this.ERRORS.IFRAME);
} // Create styled container
const embedRoot = this._configureEmbedRoot(createElement('div', {
style: {
position: 'relative',
overflow: 'hidden',
minHeight: Boolean(this.options.height) ? 0 : '15px',
width: parseCSSMeasurement(this.options.width) || '100%',
height: parseCSSMeasurement(this.options.height) || '100%'
}
})); // Create host
const host = this._configureHost(chatty.Chatty.createHost(this.getEmbedUrl()).withSandboxAttribute('allow-scripts').withSandboxAttribute('allow-same-origin').withSandboxAttribute('allow-popups').withSandboxAttribute('allow-popups-to-escape-sandbox').appendTo(embedRoot)).build(); // Customise IFrame styles
host.iframe.setAttribute('aria-label', this.name);
Object.assign(host.iframe.style, {
position: 'absolute',
top: 0,
left: 0,
border: 0,
width: '100%',
height: '100%'
}); // Remove any existing nodes in our target container
while (container.firstChild) container.removeChild(container.firstChild);
container.appendChild(embedRoot); // connect to iframe
this.connection = await host.connect();
this.iframe = host.iframe;
this._setBackground(this.options.background, this.options.theme); // configure token if needed
await this._retrieveAndSetToken();
}
/**
* @returns whether auto refreshing is enabled
*/
async isAutoRefresh() {
const [result] = await this._send('get', 'autoRefresh'); // autoRefresh from embed chart may be a number when refreshInterval is set
return typeof result === 'number' || typeof result === 'boolean' ? Boolean(result) : Promise.reject('unexpected response received from iframe');
}
/**
* Enable/Disable auto refreshing.
*/
async setAutoRefresh(value) {
if (typeof value !== 'boolean') {
return Promise.reject('autoRefresh property value should be a boolean');
}
await this._send('set', 'autoRefresh', value);
}
/**
* @returns the number of seconds before a chart or dashboard's data expires
*/
async getMaxDataAge() {
const [result] = await this._send('get', 'maxDataAge');
return typeof result === 'number' ? result : Promise.reject('unexpected response received from iframe');
}
/**
* Set the number of seconds a chart or dashboard's data expires.
*/
async setMaxDataAge(value) {
if (typeof value !== 'number') {
return Promise.reject('maxDataAge property value should be a number');
}
await this._send('set', 'maxDataAge', value);
}
/**
* Sets the color scheme to apply to the chart or dashboard.
*
* If the theme is set to 'dark' and you have specified a custom background color, you should ensure that your background color has appropriate contrast.
*/
async setTheme(value) {
if (typeof value !== 'string') {
return Promise.reject('theme property value should be a string');
} // if invalid theme string is provided, default it to light
const newTheme = Object.values(THEME).includes(value) ? value : THEME.LIGHT;
await this._send('set', 'theme', newTheme);
this._setBackground(this.options.background, newTheme);
}
/**
* @returns the current theme applied to the chart or dashboard
*/
async getTheme() {
const [result] = await this._send('get', 'theme');
return typeof result === 'string' ? result : Promise.reject('unexpected response received from iframe');
}
_configureHost(hostBuilder) {
return hostBuilder.on('refreshToken', () => this._retrieveAndSetToken());
}
_configureEmbedRoot(embedRoot) {
return embedRoot;
}
_setBackground(background, theme) {
this.iframe.style.backgroundColor = getBackground(background, theme, this.COLOUR.LIGHT, this.COLOUR.DARK);
}
async _retrieveAndSetToken() {
if (this.options.getUserToken) {
const token = await this.options.getUserToken();
await this._send('set', 'token', token);
}
}
/**
* Send message to embedded app.
*/
_send(eventName) {
if (this.connection) {
for (var _len = arguments.length, payload = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
payload[_key - 1] = arguments[_key];
}
return this.connection.sendAndReceive(eventName, ...payload);
}
return Promise.reject(this.ERRORS.SEND);
}
/**
* Get the image data of embeded entity in base64 or binary encoding
* @param {GetImageOptions} options options for image generation
* @returns {string | Blob} image encoded with base64 or binary
*/
async getImage(options) {
const {
encoding
} = options || {};
if (encoding !== ENCODING.BASE64 && encoding !== ENCODING.BINARY) {
return Promise.reject('Encoding must be either "base64" or "binary"');
}
const [result] = await this._send('get', 'image', {
background: this.options.background,
encoding
});
return typeof result === 'string' || result instanceof Blob ? result : Promise.reject('unexpected response received from iframe');
}
}
let eventHandlerIndex = Date.now();
function EventSource(Sender) {
return class extends Sender {
constructor() {
super(...arguments);
_defineProperty(this, "_eventHandlers", {
click: {} // refresh: {} To be added soon
});
}
/**
* Handle the event sent from embedded app.
*/
_handleEvent(event, payload, handlerIds) {
const handlers = this._eventHandlers[event];
for (const id of handlerIds) {
try {
var _handlers$id;
// since communication between host and SDK is async,
// it's possible that some handlers have been removed;
// thus needs to check if handler still exists before calling
(_handlers$id = handlers[id]) === null || _handlers$id === void 0 ? void 0 : _handlers$id.handle(payload);
} catch (error) {
console.warn(`Error calling handler for event [${event}]: ${error}`);
}
}
}
/**
* Sets an event listener
* @param event - the event you are subscribing to
* @param eventHandler - the callback to be executed when the event is triggered
* @param options - optional options object, can be used to customise when handler is called
*/
addEventListener(event, eventHandler, options) {
var _h$options$includes;
const handlers = this._eventHandlers[event];
if (!handlers) {
throw new Error(`Not supported event: ${event}`);
}
const h = {
handle: eventHandler,
options: {
includes: options === null || options === void 0 ? void 0 : options.includes
}
};
if ((_h$options$includes = h.options.includes) !== null && _h$options$includes !== void 0 && _h$options$includes.every(f => _isEmpty__default["default"](f))) {
// eslint-disable-next-line no-console
console.warn('Empty includes filters out all events. Event handler will never be called. Is this intended?');
} // ignore if same handler and options have been added already
if (!Object.keys(handlers).some(id => _isEqual__default["default"](handlers[id], h))) {
const handlerId = (++eventHandlerIndex).toString(36);
handlers[handlerId] = h;
return this._send('eventHandler', event, {
handlerId,
options: h.options
});
}
return Promise.resolve();
}
/**
* Removes an event listener
* @param event - the event you are unsubscribing from
* @param eventHandler - the event listener function you are unsubscribing from
* @param options - optional options object used when addEventListener
*/
removeEventListener(event, eventHandler, options) {
const handlers = this._eventHandlers[event];
if (!handlers) {
throw new Error(`Not supported event: ${event}`);
}
const h = {
handle: eventHandler,
options: {
includes: options === null || options === void 0 ? void 0 : options.includes
}
};
const handlerId = Object.keys(handlers).find(id => _isEqual__default["default"](handlers[id], h));
if (handlerId) {
delete handlers[handlerId];
return this._send('eventHandler', event, {
handlerId
});
}
return Promise.resolve();
}
};
}
function Refreshable(Sender) {
return class extends Sender {
/**
* Triggers a refresh of the chart or dashboard (if it has been embedded).
*
* @returns a promise that resolves once the chart or dashboard updated its data
*/
async refresh() {
await this._send('refresh');
}
};
}
/**
* A class extender for common chart methods
* @param Sender the parent class to be extended
*/
function CommonChart(Sender) {
return class extends Sender {
/**
* Sets a highlight to the state of the chart entity in the Charts app component.
* The chart entity can be an embedded chart or an embedded dashboard chart.
* The highlight gets applied to the embedded chart or dashboard chart.
*
* This is the exact same object that can be used in 'setFilter'.
* However, it [doesn't support some query expressions] (https://www.mongodb.com/docs/charts/filter-embedded-charts/#filter-syntax)
* And there are some specifics about what is highlightable (https://www.mongodb.com/docs/charts/highlight-chart-elements/)
*
* @param value The highlight object to be applied to the chart
* @returns a promise that resolves once the highlight is saved and the component rerendered
*/
async setHighlight(value) {
if (!isPlainObject(value)) {
return Promise.reject('highlight property value should be an object');
}
await this._send('set', 'highlight', bson.EJSON.stringify(value, {
relaxed: false
}));
}
/**
* Returns the current highlight applied to the embedded chart or dashboard chart
* The highlight value is taken from the state of the chart entity in the Charts app component.
*
* @returns a promise that resolves once the highlight is taken from the Charts state
*/
async getHighlight() {
const [result] = await this._send('get', 'highlight');
return isPlainObject(result) ? result : Promise.reject('unexpected response received from iframe');
}
/**
* @returns the data of the embedded chart or dashboard chart
*/
async getData() {
const [result] = await this._send('get', 'data');
return typeof result === 'object' && result !== null ? result : Promise.reject('unexpected response received from iframe');
}
};
}
function Filterable(Sender) {
return class extends Sender {
/**
* Sets a filter to the state of the chart/dashboard entity in the Charts app component.
* The chart entity can be an embedded chart, embedded dashboard chart or embedded dashboard.
* The filter gets applied to the embedded chart/dashboard.
*
* This expects an object that contains a valid [query operators](https://www.mongodb.com/docs/manual/reference/operator/query/#query-selectors).
* Any fields referenced in this filter are expected to be allowed for filtering in the "Embed Chart" dialog or "Embed Dashboard" dialog for each chart/dashboard you wish to filter on.
*
* @param value The filter object to be applied to the chart/dashboard
* @returns a promise that resolves once the filter is saved and the component rerendered
*/
async setFilter(value) {
if (!isPlainObject(value)) {
return Promise.reject('filter property value should be an object');
}
await this._send('set', 'filter', bson.EJSON.stringify(value, {
relaxed: false
}));
}
/**
* Returns the current filter applied to the embedded chart
* The filter value is taken from the state of the chart entity in the Charts app component.
*
* @returns a promise that resolves once the filter is taken from the Charts state
*/
async getFilter() {
const [result] = await this._send('get', 'filter');
return isPlainObject(result) ? result : Promise.reject('unexpected response received from iframe');
}
};
}
const getChartOptions = options => {
if (typeof options !== 'object' || options === null) {
throw new Error('Options argument must be an object');
}
const sharedEmbedOptions = getSharedEmbedOptions(options);
const {
chartId,
refreshInterval,
renderingSpec
} = options; // Verify chart embed options
if (typeof chartId !== 'string' || chartId.length === 0) {
throw new Error('chartId must be specified');
}
if (refreshInterval !== undefined && typeof refreshInterval !== 'number') {
throw new Error('refreshInterval interval must be a number if specified');
}
if (renderingSpec !== undefined && !isPlainObject(renderingSpec)) {
throw new Error('renderingSpec must be an object if specified');
}
if (renderingSpec !== undefined && !renderingSpec.version) {
throw new Error('renderingSpec must contain a version key');
}
return { ...sharedEmbedOptions,
chartId,
refreshInterval,
renderingSpec
};
};
class ChartEventSender extends BaseEmbedItem {
/** @ignore */
constructor(options) {
super();
_defineProperty(this, "name", 'Embedded Chart');
_defineProperty(this, "ERRORS", {
SEND: 'Chart has not been rendered. Ensure that you wait for the promise returned by `chart.render()` before trying to manipulate a chart.',
IFRAME: 'A chart can only be rendered into a container once'
});
_defineProperty(this, "COLOUR", {
LIGHT: '#FFFFFF',
DARK: '#21313C'
});
_defineProperty(this, "options", void 0);
this.options = getChartOptions(options);
}
getEmbedUrl() {
return getChartUrl(this.options);
}
}
/**
* # Chart
*
* Allows you to interact and embed charts into your application.
*
* ```js
* const sdk = new EmbedSDK({ ... });
* const chart = sdk.createChart({ ... });
*
* // renders a chart
* chart.render(document.getElementById('embed-chart'));
*
* // dynamically set a filter
* chart.setFilter({ age: { $gt: 50 } });
* ```
*/
class Chart extends CommonChart(Filterable(Refreshable(EventSource(ChartEventSender)))) {
constructor() {
super(...arguments);
_defineProperty(this, "renderingSpec", void 0);
}
/**
* Sends the `ready` event to Charts to render the embedded chart in the component
* @param container where the chart will render
*/
async render(container) {
await super.render(container);
const renderingSpec = this.options.renderingSpec;
const initialState = renderingSpec ? {
renderingSpec
} : {}; // Ready to actually render Embedded Chart
await this._send('ready', initialState);
if (initialState.renderingSpec) {
// store users rendering spec
this.renderingSpec = initialState.renderingSpec;
}
}
/**
* @returns the number of seconds a chart will wait before refreshing
* @deprecated This method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes.
*/
async getRefreshInterval() {
const [result] = await this._send('get', 'autorefresh');
console.warn("The 'getRefreshInterval' method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes.");
return typeof result === 'number' ? result : Promise.reject('unexpected response received from iframe');
}
/**
* Set the number of seconds a chart will wait before refreshing.
*
* The minimum refresh interval is 10 seconds. To disable, set the refresh interval to 0.
* @deprecated This method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes.
*/
async setRefreshInterval(value) {
if (typeof value !== 'number') {
return Promise.reject('refreshInterval property value should be a number');
}
console.warn("The 'setRefreshInterval' method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes.");
await this._send('set', 'autorefresh', value);
}
_configureHost(hostBuilder) {
return super._configureHost(hostBuilder).on('event', this._handleEvent.bind(this));
}
/**
* Sets a set of customizations on the rendered chart
* @param value customization settings and values
* @returns a promise that resolves once the rendering spec is saved and the component rerendered
*/
async setRenderingSpecOverride(value) {
if (!isPlainObject(value)) {
return Promise.reject('renderingSpec property value should be an object');
}
if (!value.version) {
return Promise.reject('renderingSpec should contain a "version" key');
}
await this._send('set', 'renderingSpec', value);
this.renderingSpec = value;
}
/**
* Get the channel data from the current chart
* @returns a promise that resolves to the channel data on the current chart
*/
async getChannels() {
const [result] = await this._send('get', 'channels');
return !!result && typeof result === 'object' ? result : Promise.reject('Unexpected response from iframe');
}
/**
* Get the customizable axes data from the current chart
* @returns a promise that resolves to the axis data on the current chart
*/
async getCustomizableAxes() {
const [result] = await this._send('get', 'axes');
return !!result && typeof result === 'object' ? result : Promise.reject('Unexpected response from iframe');
}
/**
* Gets the customizations applied to a chart after initial render
* @returns the customized rendering spec or undefined.
*/
getRenderingSpecOverride() {
return this.renderingSpec;
}
}
class DashboardChartEventSender {
constructor(chartId, dashboard) {
this.chartId = chartId;
this.dashboard = dashboard;
}
/**
* Send message to embedded app via dashboard.
*/
_send(msgName) {
for (var _len = arguments.length, payload = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
payload[_key - 1] = arguments[_key];
}
return this.dashboard._send(msgName, ...payload, this.chartId);
}
}
class DashboardChart extends CommonChart(Filterable(Refreshable(EventSource(DashboardChartEventSender)))) {}
const getDashboardOptions = options => {
if (typeof options !== 'object' || options === null) {
throw new Error('Options argument must be an object');
}
const sharedEmbedOptions = getSharedEmbedOptions(options);
const {
dashboardId,
chartsBackground,
widthMode,
heightMode,
showTitleAndDesc,
charts
} = options; // Verify dashboard embed options
if (typeof dashboardId !== 'string' || dashboardId.length === 0) {
throw new Error('dashboardId must be specified');
}
if (chartsBackground !== undefined && typeof chartsBackground !== 'string') {
throw new Error('chartsBackground must be a string if specified');
}
if (widthMode !== undefined && typeof widthMode !== 'string') {
throw new Error('widthMode must be a string if specified');
}
if (widthMode !== undefined && widthMode !== SCALING.FIXED && widthMode !== SCALING.SCALE) {
throw new Error(`widthMode must be "${SCALING.FIXED}" or "${SCALING.SCALE}"`);
}
if (heightMode !== undefined && typeof heightMode !== 'string') {
throw new Error('heightMode must be a string if specified');
}
if (heightMode !== undefined && heightMode !== SCALING.FIXED && heightMode !== SCALING.SCALE) {
throw new Error(`heightMode must be "${SCALING.FIXED}" or "${SCALING.SCALE}"`);
}
if (showTitleAndDesc !== undefined && typeof showTitleAndDesc !== 'boolean') {
throw new Error('showTitleAndDesc must be a boolean value if specified');
}
if (charts !== undefined && !Array.isArray(charts)) {
throw new Error('charts embedding option must be an array if specified');
}
if (charts !== undefined) {
// check if chartIds are unique
const uniqueIds = {};
charts.forEach(ch => uniqueIds[ch.chartId] = uniqueIds.chartId);
if (Object.keys(uniqueIds).length !== charts.length) {
throw new Error('charts embedding option must contain unique chartIds');
}
}
return { ...sharedEmbedOptions,
dashboardId,
chartsBackground,
widthMode,
heightMode,
showTitleAndDesc,
charts
};
};
class DashboardEventSender extends BaseEmbedItem {
/** @ignore */
constructor(options) {
super();
_defineProperty(this, "name", 'Embedded Dashboard');
_defineProperty(this, "ERRORS", {
SEND: 'Dashboard has not been rendered. Ensure that you wait for the promise returned by `dashboard.render()` before trying to manipulate a dashboard.',
IFRAME: 'A dashboard can only be rendered into a container once'
});
_defineProperty(this, "COLOUR", {
LIGHT: '#F1F5F4',
DARK: '#12212C'
});
_defineProperty(this, "options", void 0);
this.options = getDashboardOptions(options);
}
getEmbedUrl() {
return getDashboardUrl(this.options);
}
}
/**
* # Dashboard
*
* Allows you to interact and embed dashboards into your application.
*
* ```js
* const sdk = new EmbedSDK({ ... });
* const dashboard = sdk.createDashboard({ ... });
*
* // renders a dashboard
* dashboard.render(document.getElementById('embed-dashboard'));
*
* ```
*/
class Dashboard extends Filterable(Refreshable(DashboardEventSender)) {
constructor() {
super(...arguments);
_defineProperty(this, "charts", {});
}
/**
* Sends the `ready` event to Charts together with the chart options
* to render the embedded dashboard in the container
* @param container where the dashboard will render
*/
async render(container) {
await super.render(container); // Ready to actually render Embedded Dashboard
await this._send('ready', this.options.charts);
}
/**
* @returns current chartsBackground or empty string if not set
*/
async getChartsBackground() {
const [result] = await this._send('get', 'chartsBackground');
return typeof result === 'string' ? result : Promise.reject('unexpected response received from iframe');
}
/**
* Set a custom background color for all charts.
* To clear existing value, set it to empty string.
*/
async setChartsBackground(value) {
if (typeof value !== 'string') {
return Promise.reject('chartsBackground property value should be a string');
}
await this._send('set', 'chartsBackground', value);
}
/**
* @returns whether attribution logo should be shown
*/
async isShowAttribution() {
const [result] = await this._send('get', 'attribution');
return typeof result === 'boolean' ? Boolean(result) : Promise.reject('unexpected response received from iframe');
}
/**
* Enable/Disable attribution logo.
*/
async setShowAttribution(value) {
if (typeof value !== 'boolean') {
return Promise.reject('showAttribution property value should be a boolean');
}
await this._send('set', 'attribution', value);
}
/**
* @returns get width scaling mode of embedded dashboard
*/
async getWidthMode() {
const [result] = await this._send('get', 'scalingWidth');
return result === SCALING.FIXED || result === SCALING.SCALE ? result : Promise.reject('unexpected response received from iframe');
}
/**
* Set width scaling mode for embedded dashboard
*/
async setWidthMode(value) {
if (!['fixed', 'scale'].includes(value)) {
return Promise.reject('widthMode property value should be a string value of "fixed" or "scale"');
}
await this._send('set', 'scalingWidth', value);
}
/**
* @returns get height scaling mode of embedded dashboard
*/
async getHeightMode() {
const [result] = await this._send('get', 'scalingHeight');
return result === 'fixed' || result === 'scale' ? result : Promise.reject('unexpected response received from iframe');
}
/**
* Set height scaling mode for embedded dashboard
*/
async setHeightMode(value) {
if (!['fixed', 'scale'].includes(value)) {
return Promise.reject('heightMode property value should be a string value of "fixed" or "scale"');
}
await this._send('set', 'scalingHeight', value);
}
/**
* @returns get the dashboard chart with specified id
*/
async getChart(id) {
if (!this.charts[id]) {
const [chartIds] = await this._send('get', 'charts', [id]);
if (!Array.isArray(chartIds)) {
return Promise.reject('unexpected response received from iframe');
}
if (chartIds.length !== 1) {
return Promise.reject('Invalid chart id: ' + id);
}
this.charts[id] = new DashboardChart(id, this);
}
return this.charts[id];
}
/**
* @returns all charts on the dashboard
*/
async getAllCharts() {
const [chartIds] = await this._send('get', 'charts');
if (!Array.isArray(chartIds)) {
return Promise.reject('unexpected response received from iframe');
}
const charts = [];
chartIds.forEach(id => {
if (!this.charts[id]) {
this.charts[id] = new DashboardChart(id, this);
}
charts.push(this.charts[id]);
});
return charts;
}
_configureHost(hostBuilder) {
return super._configureHost(hostBuilder).on('event', (event, payload, handlerIds) => {
const chartId = payload.chartId;
this.charts[chartId]._handleEvent(event, payload, handlerIds);
});
}
}
// Disabled temporarily to fix: https://github.com/mongodb-js/charts-embed-sdk/issues/14
// Until we come up with a better way to have strong typing for the Stitch client, while
// also not breaking normal TSC compiles of the SDK
// import type { StitchAppClient } from 'mongodb-stitch-browser-sdk';
const isJWTExpired = jwt => {
try {
const [header, payload, signature] = jwt.split('.');
const {
exp
} = JSON.parse(atob(payload)); // Check the current time against the expiry (minus 5 minutes) in the token
return Date.now() / 1000 >= exp - 300;
} catch (e) {
throw new Error('Not a vaid JWT token. Is the StitchClient/RealmClient configured correctly?');
}
};
/**
* A helper utility to support using [Realm Authentication](https://www.mongodb.com/docs/realm/) with MongoDB Charts with two npm packages:
*
* Using "mongodb-stitch-browser-sdk"
*
* ```js
* const client = Stitch.initializeDefaultAppClient('<your-client-app-id>');
* client.auth.loginWithCredential(...)
*
* const sdk = new ChartsEmbedSDK({
* getUserToken: () => getRealmUserToken(client)
* })
* ```
*
* or using "realm-web"
*
* ```js
* const client = new Realm.App({id: '<your-client-app-id>'});
* client.logIn(...)
*
* const sdk = new ChartsEmbedSDK({
* getUserToken: () => getRealmUserToken(client)
* })
* ```
*
*/
async function getRealmUserToken(appClient) {
// Authentication with Realm was first implemented to work for apps using mongodb-stitch-browser-sdk.
// Later, it got deprecated and a new sdk was published by the Realm team - realm-web.
// Here, we are checking which Stitch/Realm sdk is used and use that path to pass the auth token successfully
let token; // if the user is using the "mongodb-stitch-browser-sdk"
if ('auth' in appClient) {
const stitchClient = appClient;
if (!stitchClient.auth.authInfo) {
throw new Error('Unfamiliar Stitch client version');
}
if (!stitchClient.auth.isLoggedIn) {
throw new Error('Could not find a logged-in StitchUser. Is the StitchClient configured correctly?');
}
if (!stitchClient.auth.authInfo.accessToken) {
throw new Error('Could not find a valid JWT. Is the StitchClient configured correctly?');
}
if (isJWTExpired(stitchClient.auth.authInfo.accessToken)) {
// Attempt to refresh token using progression from public -> private apis
if (stitchClient.auth.refreshCustomData) {
await stitchClient.auth.refreshCustomData(); // supported from 4.8.0
} else if (stitchClient.auth.refreshAccessToken) {
await stitchClient.auth.refreshAccessToken(); // supported from 4.0.0
} else {
throw new Error('Could not refresh token. Unfamiliar Stitch client version');
}
}
token = stitchClient.auth.authInfo.accessToken;
} // if the user is using realm-web
else if ('authenticator' in appClient) {
const realmClient = appClient;
if (!realmClient.currentUser) {
throw new Error('Unfamiliar Realm client version');
}
if (!realmClient.currentUser.isLoggedIn) {
throw new Error('Could not find a logged-in RealmUser. Is the RealmClient configured correctly?');
}
if (!realmClient.currentUser.accessToken) {
throw new Error('Could not find a valid JWT. Is the RealmClient configured correctly?');
}
if (isJWTExpired(realmClient.currentUser.accessToken)) {
// Attempt to refresh token
if (realmClient.currentUser.refreshCustomData) {
await realmClient.currentUser.refreshCustomData();
} else {
throw new Error('Could not refresh token. Unfamiliar Realm client version');
}
}
token = realmClient.currentUser.accessToken;
} else {
throw new Error('Unfamiliar Stitch or Realm client version');
}
return token;
}
/**
* Creates an instance of the embedding SDK
*/
class EmbedSDK {
/**
* Accepts an optional {@link EmbedChartOptions} object to use as the
* default options for any charts created using this SDK instance.
*
* ```js
* const sdk = new EmbedSDK({
* baseUrl: "https://charts.mongodb.com",
* })
* ```
*/
constructor(options) {
_defineProperty(this, "defaultOptions", void 0);
this.defaultOptions = options;
}
/**
* Creates a new {@link Chart} instance that allows you to
* interact with and embed charts into your application
*/
createChart(options) {
return new Chart({ ...this.defaultOptions,
...options
});
}
/**
* Creates a new {@link Dashboard} instance that allows you
* to embed a dashboard into your application
*/
createDashboard(options) {
return new Dashboard({ ...this.defaultOptions,
...options
});
}
}
exports["default"] = EmbedSDK;
exports.getRealmUserToken = getRealmUserToken;