UNPKG

31.6 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
6
7var bson = require('bson');
8var chatty = require('@looker/chatty');
9var _isEqual = _interopDefault(require('lodash/isEqual'));
10var _isEmpty = _interopDefault(require('lodash/isEmpty'));
11
12// Given an object `Target`, find all property names of type `Type`
13// Given an object `Target`, filter out all properties that aren't of type `Type`
14function createElement(name, props = {}, children = []) {
15 const element = document.createElement(name);
16
17 for (const [name, value] of Object.entries(props)) {
18 if (name === 'style') {
19 Object.assign(element.style, props.style);
20 } else {
21 element.setAttribute(name, value);
22 }
23 }
24
25 for (const child of Array.isArray(children) ? children : [children]) {
26 element.append(child);
27 }
28
29 return element;
30}
31
32/**
33 * Shared options for embedding
34 */
35
36/**
37 * The set of options that you can use when both creating an {@link EmbedSDK} object or using {@link EmbedSDK.createChart}.
38 */
39
40/**
41 * The set of options that you can use when both creating an {@link EmbedSDK} object or using {@link EmbedSDK.createDashboard}.
42 */
43//TODO find a way to reuse types defined in "packages/charts-frontend/src/utils/chart/events/event-payload-types.ts"
44let THEME_ENUM;
45
46(function (THEME_ENUM) {
47 THEME_ENUM["DARK"] = "dark";
48 THEME_ENUM["LIGHT"] = "light";
49})(THEME_ENUM || (THEME_ENUM = {}));
50
51let SCALING_ENUM;
52
53(function (SCALING_ENUM) {
54 SCALING_ENUM["FIXED"] = "fixed";
55 SCALING_ENUM["SCALE"] = "scale";
56})(SCALING_ENUM || (SCALING_ENUM = {}));
57
58/**
59 * Retrieve embed options that are shared.
60 *
61 * Validates the values passed in as well.
62 */
63const getSharedEmbedOptions = options => {
64 const {
65 background,
66 baseUrl,
67 autoRefresh,
68 maxDataAge,
69 width,
70 height,
71 theme,
72 showAttribution,
73 getUserToken
74 } = options;
75
76 if (typeof baseUrl !== 'string' || baseUrl.length === 0) {
77 throw new Error('Base URL must be a valid URL');
78 }
79
80 if (background !== undefined && typeof background !== 'string') {
81 throw new Error('background must be a string if specified');
82 }
83
84 if (autoRefresh !== undefined && typeof autoRefresh !== 'boolean') {
85 throw new Error('autoRefresh must be a boolean if specified');
86 }
87
88 if (maxDataAge !== undefined && typeof maxDataAge !== 'number') {
89 throw new Error('maxDataAge must be a number if specified');
90 }
91
92 if (width !== undefined && !['number', 'string'].includes(typeof width)) {
93 throw new Error('Width must be a string or number if specified');
94 }
95
96 if (height !== undefined && !['number', 'string'].includes(typeof height)) {
97 throw new Error('Height must be a string or number if specified');
98 }
99
100 if (theme !== undefined && typeof theme !== 'string') {
101 throw new Error('Theme must be a string if specified');
102 }
103
104 if (showAttribution !== undefined && typeof showAttribution !== 'boolean') {
105 throw new Error('Attribution must be a boolean value if specified');
106 }
107
108 if (getUserToken !== undefined && typeof getUserToken !== 'function') {
109 throw new Error('getUserToken must be a function');
110 }
111
112 return {
113 background,
114 baseUrl,
115 autoRefresh,
116 maxDataAge,
117 width,
118 height,
119 theme,
120 showAttribution,
121 getUserToken
122 };
123};
124const getPathname = (url, pathname) => {
125 return [url.pathname, url.pathname.slice(-1) === '/' ? '' : '/', // Add trailing slash if not there
126 pathname].join('');
127};
128/**
129 * Constructs the chart iframe URL from the baseUrl, chartId & tenantId
130 */
131
132const getChartUrl = options => {
133 try {
134 const url = new URL(options.baseUrl);
135 url.pathname = getPathname(url, 'embed/charts');
136 url.search = `id=${options.chartId}&sdk=2`;
137
138 if (options.autoRefresh === false) {
139 url.search += `&autorefresh=false`;
140 } else if (options.autoRefresh === undefined) {
141 url.search += options.refreshInterval ? `&autorefresh=${options.refreshInterval}` : '';
142 }
143
144 if (options.maxDataAge !== undefined) {
145 url.search += `&maxDataAge=${options.maxDataAge}`;
146 }
147
148 if (options.filter) {
149 url.search += `&filter=${encodeURIComponent(bson.EJSON.stringify(options.filter, {
150 relaxed: false
151 }))}`;
152 }
153
154 if (options.theme) {
155 url.search += `&theme=${options.theme}`;
156 }
157
158 if (options.showAttribution === false) {
159 url.search += `&attribution=false`;
160 }
161
162 return url.toString();
163 } catch (e) {
164 throw new Error('Base URL must be a valid URL');
165 }
166};
167/**
168 * Constructs the dashboard iframe URL from the baseUrl, dashboardId & tenantId
169 */
170
171const getDashboardUrl = options => {
172 try {
173 const url = new URL(options.baseUrl);
174 url.pathname = getPathname(url, 'embed/dashboards');
175 url.search = `id=${options.dashboardId}&sdk=1`;
176
177 if (options.autoRefresh === false) {
178 url.search += `&autoRefresh=false`;
179 }
180
181 if (options.maxDataAge !== undefined) {
182 url.search += `&maxDataAge=${options.maxDataAge}`;
183 }
184
185 if (options.showTitleAndDesc === true) {
186 url.search += `&showTitleAndDesc=true`;
187 }
188
189 if (options.widthMode) {
190 url.search += `&scalingWidth=${options.widthMode}`;
191 }
192
193 if (options.heightMode) {
194 url.search += `&scalingHeight=${options.heightMode}`;
195 }
196
197 if (options.theme) {
198 url.search += `&theme=${options.theme}`;
199 }
200
201 if (options.chartsBackground) {
202 url.search += `&chartsBackground=${options.chartsBackground}`;
203 }
204
205 if (options.showAttribution === false) {
206 url.search += `&attribution=false`;
207 }
208
209 return url.toString();
210 } catch (e) {
211 throw new Error('Base URL must be a valid URL');
212 }
213};
214/*
215 Parses a CSS Measurement from an unknown value
216 - if it's a string, we trust that it is well-formed
217 - if it's a number, we assume the units are pixels
218 - otherwise we return null
219*/
220
221const parseCSSMeasurement = value => {
222 if (typeof value === 'string') return value;
223 if (typeof value === 'number') return `${value}px`;
224 return null;
225};
226/**
227 * Returns the background after validation checks
228 * or default background based on theme if not set
229 */
230
231const getBackground = (background, theme, lightBackground, darkBackground) => {
232 if (typeof background === 'string' && background.length > 0) return background;
233 if (theme === 'dark') return darkBackground;
234 return lightBackground;
235};
236
237function _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; }
238
239class BaseEmbedItem {
240 constructor() {
241 _defineProperty(this, "iframe", void 0);
242
243 _defineProperty(this, "connection", void 0);
244
245 _defineProperty(this, "name", void 0);
246
247 _defineProperty(this, "ERRORS", void 0);
248
249 _defineProperty(this, "COLOUR", void 0);
250
251 _defineProperty(this, "options", void 0);
252 }
253
254 /**
255 * Renders an embeddable item into the given `container`.
256 *
257 * This method should only be called once, and successive attempts to call `render`
258 * will fail with an error.
259 *
260 * @returns a promise that will resolve once the item has successfully been embedded
261 */
262 async render(container) {
263 if (this.iframe) {
264 throw new Error(this.ERRORS.IFRAME);
265 } // Create styled container
266
267
268 const embedRoot = this._configureEmbedRoot(createElement('div', {
269 style: {
270 position: 'relative',
271 overflow: 'hidden',
272 minHeight: Boolean(this.options.height) ? 0 : '15px',
273 width: parseCSSMeasurement(this.options.width) || '100%',
274 height: parseCSSMeasurement(this.options.height) || '100%'
275 }
276 })); // Create host
277
278
279 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
280
281
282 host.iframe.setAttribute('aria-label', this.name);
283 Object.assign(host.iframe.style, {
284 position: 'absolute',
285 top: 0,
286 left: 0,
287 border: 0,
288 width: '100%',
289 height: '100%'
290 }); // Remove any existing nodes in our target container
291
292 while (container.firstChild) container.removeChild(container.firstChild);
293
294 container.appendChild(embedRoot); // connect to iframe
295
296 this.connection = await host.connect();
297 this.iframe = host.iframe;
298
299 this._setBackground(this.options.background, this.options.theme); // configure token if needed
300
301
302 await this._retrieveAndSetToken(); // Ready to actually render Embedded Item
303
304 await this._send('ready');
305 }
306 /**
307 * @returns whether auto refreshing is enabled
308 */
309
310
311 async isAutoRefresh() {
312 const [result] = await this._send('get', 'autoRefresh'); // autoRefresh from embed chart may be a number when refreshInterval is set
313
314 return typeof result === 'number' || typeof result === 'boolean' ? Boolean(result) : Promise.reject('unexpected response received from iframe');
315 }
316 /**
317 * Enable/Disable auto refreshing.
318 */
319
320
321 async setAutoRefresh(value) {
322 if (typeof value !== 'boolean') {
323 return Promise.reject('autoRefresh property value should be a boolean');
324 }
325
326 await this._send('set', 'autoRefresh', value);
327 }
328 /**
329 * @returns the number of seconds before a chart or dashboard's data expires
330 */
331
332
333 async getMaxDataAge() {
334 const [result] = await this._send('get', 'maxDataAge');
335 return typeof result === 'number' ? result : Promise.reject('unexpected response received from iframe');
336 }
337 /**
338 * Set the number of seconds a chart or dashboard's data expires.
339 */
340
341
342 async setMaxDataAge(value) {
343 if (typeof value !== 'number') {
344 return Promise.reject('maxDataAge property value should be a number');
345 }
346
347 await this._send('set', 'maxDataAge', value);
348 }
349 /**
350 * Sets the color scheme to apply to the chart or dashboard.
351 *
352 * 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.
353 */
354
355
356 async setTheme(value) {
357 if (typeof value !== 'string') {
358 return Promise.reject('theme property value should be a string');
359 } // if invalid theme string is provided, default it to light
360
361
362 const newTheme = Object.values(THEME_ENUM).includes(value) ? value : THEME_ENUM.LIGHT;
363 await this._send('set', 'theme', newTheme);
364
365 this._setBackground(this.options.background, newTheme);
366 }
367 /**
368 * @returns the current theme applied to the chart or dashboard
369 */
370
371
372 async getTheme() {
373 const [result] = await this._send('get', 'theme');
374 return typeof result === 'string' ? result : Promise.reject('unexpected response received from iframe');
375 }
376
377 _configureHost(hostBuilder) {
378 return hostBuilder.on('refreshToken', () => this._retrieveAndSetToken());
379 }
380
381 _configureEmbedRoot(embedRoot) {
382 return embedRoot;
383 }
384
385 _setBackground(background, theme) {
386 this.iframe.style.backgroundColor = getBackground(background, theme, this.COLOUR.LIGHT, this.COLOUR.DARK);
387 }
388
389 async _retrieveAndSetToken() {
390 if (this.options.getUserToken) {
391 const token = await this.options.getUserToken();
392 await this._send('set', 'token', token);
393 }
394 }
395 /**
396 * Send message to embedded app.
397 */
398
399
400 _send(eventName, ...payload) {
401 if (this.connection) {
402 return this.connection.sendAndReceive(eventName, ...payload);
403 }
404
405 return Promise.reject(this.ERRORS.SEND);
406 }
407
408}
409
410function _defineProperty$1(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; }
411
412let eventHandlerIndex = Date.now();
413function EventSource(Sender) {
414 var _temp;
415
416 return _temp = class extends Sender {
417 constructor(...args) {
418 super(...args);
419
420 _defineProperty$1(this, "_eventHandlers", {
421 click: {} // refresh: {} To be added soon
422
423 });
424 }
425
426 /**
427 * Handle the event sent from embedded app.
428 */
429 _handleEvent(event, payload, handlerIds) {
430 const handlers = this._eventHandlers[event];
431
432 for (const id of handlerIds) {
433 try {
434 var _handlers$id;
435
436 // since communication between host and SDK is async,
437 // it's possible that some handlers have been removed;
438 // thus needs to check if handler still exists before calling
439 (_handlers$id = handlers[id]) === null || _handlers$id === void 0 ? void 0 : _handlers$id.handle(payload);
440 } catch (error) {
441 console.warn(`Error calling handler for event [${event}]: ${error}`);
442 }
443 }
444 }
445 /**
446 * Sets an event listener
447 * @param event - the event you are subscribing to
448 * @param eventHandler - the callback to be executed when the event is triggered
449 * @param options - optional options object, can be used to customise when handler is called
450 */
451
452
453 addEventListener(event, eventHandler, options) {
454 var _h$options$includes;
455
456 const handlers = this._eventHandlers[event];
457
458 if (!handlers) {
459 throw new Error(`Not supported event: ${event}`);
460 }
461
462 const h = {
463 handle: eventHandler,
464 options: {
465 includes: options === null || options === void 0 ? void 0 : options.includes
466 }
467 };
468
469 if ((_h$options$includes = h.options.includes) !== null && _h$options$includes !== void 0 && _h$options$includes.every(f => _isEmpty(f))) {
470 // eslint-disable-next-line no-console
471 console.warn('Empty includes filters out all events. Event handler will never be called. Is this intended?');
472 } // ignore if same handler and options have been added already
473
474
475 if (!Object.keys(handlers).some(id => _isEqual(handlers[id], h))) {
476 const handlerId = (++eventHandlerIndex).toString(36);
477 handlers[handlerId] = h;
478 return this._send('eventHandler', event, {
479 handlerId,
480 options: h.options
481 });
482 }
483
484 return Promise.resolve();
485 }
486 /**
487 * Removes an event listener
488 * @param event - the event you are unsubscribing from
489 * @param eventHandler - the event listener function you are unsubscribing from
490 * @param options - optional options object used when addEventListener
491 */
492
493
494 removeEventListener(event, eventHandler, options) {
495 const handlers = this._eventHandlers[event];
496
497 if (!handlers) {
498 throw new Error(`Not supported event: ${event}`);
499 }
500
501 const h = {
502 handle: eventHandler,
503 options: {
504 includes: options === null || options === void 0 ? void 0 : options.includes
505 }
506 };
507 const handlerId = Object.keys(handlers).find(id => _isEqual(handlers[id], h));
508
509 if (handlerId) {
510 delete handlers[handlerId];
511 return this._send('eventHandler', event, {
512 handlerId
513 });
514 }
515
516 return Promise.resolve();
517 }
518
519 }, _temp;
520}
521
522function Refreshable(Sender) {
523 return class extends Sender {
524 /**
525 * Triggers a refresh of the chart or dashboard (if it has been embedded).
526 *
527 * @returns a promise that resolves once the chart or dashboard updated its data
528 */
529 async refresh() {
530 await this._send('refresh');
531 }
532
533 };
534}
535
536function _defineProperty$2(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; }
537
538const getChartOptions = options => {
539 if (typeof options !== 'object' || options === null) {
540 throw new Error('Options argument must be an object');
541 }
542
543 const sharedEmbedOptions = getSharedEmbedOptions(options);
544 const {
545 chartId,
546 filter,
547 refreshInterval
548 } = options; // Verify chart embed options
549
550 if (typeof chartId !== 'string' || chartId.length === 0) {
551 throw new Error('Chart ID must be specified');
552 }
553
554 if (filter !== undefined && (!filter || typeof filter !== 'object')) {
555 throw new Error('Filter must be an object if specified');
556 }
557
558 if (refreshInterval !== undefined && typeof refreshInterval !== 'number') {
559 throw new Error('refreshInterval interval must be a number if specified');
560 }
561
562 return { ...sharedEmbedOptions,
563 chartId,
564 filter,
565 refreshInterval
566 };
567};
568
569class ChartEventSender extends BaseEmbedItem {
570 /** @ignore */
571 constructor(options) {
572 super();
573
574 _defineProperty$2(this, "name", 'Embedded Chart');
575
576 _defineProperty$2(this, "ERRORS", {
577 SEND: 'Chart has not been rendered. Ensure that you wait for the promise returned by `chart.render()` before trying to manipulate a chart.',
578 IFRAME: 'A chart can only be rendered into a container once'
579 });
580
581 _defineProperty$2(this, "COLOUR", {
582 LIGHT: '#FFFFFF',
583 DARK: '#21313C'
584 });
585
586 _defineProperty$2(this, "options", void 0);
587
588 this.options = getChartOptions(options);
589 }
590
591 getEmbedUrl() {
592 return getChartUrl(this.options);
593 }
594
595}
596/**
597 * # Chart
598 *
599 * Allows you to interact and embed charts into your application.
600 *
601 * ```js
602 * const sdk = new EmbedSDK({ ... });
603 * const chart = sdk.createChart({ ... });
604 *
605 * // renders a chart
606 * chart.render(document.getElementById('embed-chart'));
607 *
608 * // dynamically set a filter
609 * chart.setFilter({ age: { $gt: 50 } });
610 * ```
611 */
612
613
614class Chart extends Refreshable(EventSource(ChartEventSender)) {
615 /**
616 * @returns the number of seconds a chart will wait before refreshing
617 * @deprecated This method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes.
618 */
619 async getRefreshInterval() {
620 const [result] = await this._send('get', 'autorefresh');
621 console.warn("The 'getRefreshInterval' method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes.");
622 return typeof result === 'number' ? result : Promise.reject('unexpected response received from iframe');
623 }
624 /**
625 * Set the number of seconds a chart will wait before refreshing.
626 *
627 * The minimum refresh interval is 10 seconds. To disable, set the refresh interval to 0.
628 * @deprecated This method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes.
629 */
630
631
632 async setRefreshInterval(value) {
633 if (typeof value !== 'number') {
634 return Promise.reject('refreshInterval property value should be a number');
635 }
636
637 console.warn("The 'setRefreshInterval' method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes.");
638 await this._send('set', 'autorefresh', value);
639 }
640 /**
641 * @returns the current filter applied to the embedded chart.
642 */
643
644
645 async getFilter() {
646 const [result] = await this._send('get', 'filter');
647 return typeof result === 'object' && result !== null ? result : Promise.reject('unexpected response received from iframe');
648 }
649 /**
650 * Sets the filter to apply to the embedded chart.
651 *
652 * This expects an object that contains a valid [query operators](https://docs.mongodb.com/manual/reference/operator/query/#query-selectors).
653 * Any fields referenced in this filter are expected to be whitelisted in the "Embed Chart" dialog for each Chart you wish to filter on.
654 */
655
656
657 async setFilter(value) {
658 if (typeof value !== 'object' || value === null || Array.isArray(value)) {
659 return Promise.reject('filter property value should be an object');
660 }
661
662 await this._send('set', 'filter', bson.EJSON.stringify(value, {
663 relaxed: false
664 }));
665 }
666 /**
667 * @returns the current highlight applied to the embedded chart.
668 */
669
670
671 async getHighlight() {
672 const [result] = await this._send('get', 'highlight');
673 return typeof result === 'object' && result !== null ? result : Promise.reject('unexpected response received from iframe');
674 }
675 /**
676 * Sets the highlight to apply to the embedded chart.
677 *
678 * This is the exact same object that can be used in 'setFilter'.
679 * However, it [doesn't support some query expressions](https://docs.mongodb.com/charts/saas/embedded-chart-options/)
680 * @param value The highlight object to be applied to the chart
681 */
682
683
684 async setHighlight(value) {
685 if (typeof value !== 'object' || value === null || Array.isArray(value)) {
686 return Promise.reject('highlight property value should be an object');
687 }
688
689 await this._send('set', 'highlight', bson.EJSON.stringify(value, {
690 relaxed: false
691 }));
692 }
693
694 _configureHost(hostBuilder) {
695 return super._configureHost(hostBuilder).on('event', this._handleEvent.bind(this));
696 }
697 /**
698 * @returns the data of the embedded chart.
699 */
700
701
702 async getData() {
703 const [result] = await this._send('get', 'data');
704 return typeof result === 'object' && result !== null ? result : Promise.reject('unexpected response received from iframe');
705 }
706
707}
708
709class DashboardChartEventSender {
710 constructor(chartId, dashboard) {
711 this.chartId = chartId;
712 this.dashboard = dashboard;
713 }
714 /**
715 * Send message to embedded app via dashboard.
716 */
717
718
719 _send(msgName, ...payload) {
720 return this.dashboard._send(msgName, ...payload, this.chartId);
721 }
722
723}
724
725class DashboardChart extends Refreshable(EventSource(DashboardChartEventSender)) {}
726
727function _defineProperty$3(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; }
728
729const getDashboardOptions = options => {
730 if (typeof options !== 'object' || options === null) {
731 throw new Error('Options argument must be an object');
732 }
733
734 const sharedEmbedOptions = getSharedEmbedOptions(options);
735 const {
736 dashboardId,
737 chartsBackground,
738 widthMode,
739 heightMode,
740 showTitleAndDesc
741 } = options; // Verify dashboard embed options
742
743 if (typeof dashboardId !== 'string' || dashboardId.length === 0) {
744 throw new Error('dashboardId must be specified');
745 }
746
747 if (chartsBackground !== undefined && typeof chartsBackground !== 'string') {
748 throw new Error('chartsBackground must be a string if specified');
749 }
750
751 if (widthMode !== undefined && typeof widthMode !== 'string') {
752 throw new Error('widthMode must be a string if specified');
753 }
754
755 if (widthMode !== undefined && widthMode !== SCALING_ENUM.FIXED && widthMode !== SCALING_ENUM.SCALE) {
756 throw new Error(`widthMode must be "${SCALING_ENUM.FIXED}" or "${SCALING_ENUM.SCALE}"`);
757 }
758
759 if (heightMode !== undefined && typeof heightMode !== 'string') {
760 throw new Error('heightMode must be a string if specified');
761 }
762
763 if (heightMode !== undefined && heightMode !== SCALING_ENUM.FIXED && heightMode !== SCALING_ENUM.SCALE) {
764 throw new Error(`heightMode must be "${SCALING_ENUM.FIXED}" or "${SCALING_ENUM.SCALE}"`);
765 }
766
767 if (showTitleAndDesc !== undefined && typeof showTitleAndDesc !== 'boolean') {
768 throw new Error('showTitleAndDesc must be a boolean value if specified');
769 }
770
771 return { ...sharedEmbedOptions,
772 dashboardId,
773 chartsBackground,
774 widthMode,
775 heightMode,
776 showTitleAndDesc
777 };
778};
779
780class DashboardEventSender extends BaseEmbedItem {
781 /** @ignore */
782 constructor(options) {
783 super();
784
785 _defineProperty$3(this, "name", 'Embedded Dashboard');
786
787 _defineProperty$3(this, "ERRORS", {
788 SEND: 'Dashboard has not been rendered. Ensure that you wait for the promise returned by `dashboard.render()` before trying to manipulate a dashboard.',
789 IFRAME: 'A dashboard can only be rendered into a container once'
790 });
791
792 _defineProperty$3(this, "COLOUR", {
793 LIGHT: '#F1F5F4',
794 DARK: '#12212C'
795 });
796
797 _defineProperty$3(this, "options", void 0);
798
799 this.options = getDashboardOptions(options);
800 }
801
802 getEmbedUrl() {
803 return getDashboardUrl(this.options);
804 }
805
806}
807/**
808 * # Dashboard
809 *
810 * Allows you to interact and embed dashboards into your application.
811 *
812 * ```js
813 * const sdk = new EmbedSDK({ ... });
814 * const dashboard = sdk.createDashboard({ ... });
815 *
816 * // renders a dashboard
817 * dashboard.render(document.getElementById('embed-dashboard'));
818 *
819 * ```
820 */
821
822
823class Dashboard extends Refreshable(DashboardEventSender) {
824 constructor(...args) {
825 super(...args);
826
827 _defineProperty$3(this, "charts", {});
828 }
829
830 /**
831 * @returns current chartsBackground or empty string if not set
832 */
833 async getChartsBackground() {
834 const [result] = await this._send('get', 'chartsBackground');
835 return typeof result === 'string' ? result : Promise.reject('unexpected response received from iframe');
836 }
837 /**
838 * Set a custom background color for all charts.
839 * To clear existing value, set it to empty string.
840 */
841
842
843 async setChartsBackground(value) {
844 if (typeof value !== 'string') {
845 return Promise.reject('chartsBackground property value should be a string');
846 }
847
848 await this._send('set', 'chartsBackground', value);
849 }
850 /**
851 * @returns whether attribution logo should be shown
852 */
853
854
855 async isShowAttribution() {
856 const [result] = await this._send('get', 'attribution');
857 return typeof result === 'boolean' ? Boolean(result) : Promise.reject('unexpected response received from iframe');
858 }
859 /**
860 * Enable/Disable attribution logo.
861 */
862
863
864 async setShowAttribution(value) {
865 if (typeof value !== 'boolean') {
866 return Promise.reject('showAttribution property value should be a boolean');
867 }
868
869 await this._send('set', 'attribution', value);
870 }
871 /**
872 * @returns get width scaling mode of embedded dashboard
873 */
874
875
876 async getWidthMode() {
877 const [result] = await this._send('get', 'scalingWidth');
878 return result === SCALING_ENUM.FIXED || result === SCALING_ENUM.SCALE ? result : Promise.reject('unexpected response received from iframe');
879 }
880 /**
881 * Set width scaling mode for embedded dashboard
882 */
883
884
885 async setWidthMode(value) {
886 if (!['fixed', 'scale'].includes(value)) {
887 return Promise.reject('widthMode property value should be a string value of "fixed" or "scale"');
888 }
889
890 await this._send('set', 'scalingWidth', value);
891 }
892 /**
893 * @returns get height scaling mode of embedded dashboard
894 */
895
896
897 async getHeightMode() {
898 const [result] = await this._send('get', 'scalingHeight');
899 return result === 'fixed' || result === 'scale' ? result : Promise.reject('unexpected response received from iframe');
900 }
901 /**
902 * Set height scaling mode for embedded dashboard
903 */
904
905
906 async setHeightMode(value) {
907 if (!['fixed', 'scale'].includes(value)) {
908 return Promise.reject('heightMode property value should be a string value of "fixed" or "scale"');
909 }
910
911 await this._send('set', 'scalingHeight', value);
912 }
913 /**
914 * @returns get the dashboard chart with specified id
915 */
916
917
918 async getChart(id) {
919 if (!this.charts[id]) {
920 const [chartIds] = await this._send('get', 'charts', [id]);
921
922 if (!Array.isArray(chartIds)) {
923 return Promise.reject('unexpected response received from iframe');
924 }
925
926 if (chartIds.length !== 1) {
927 return Promise.reject('Invalid chart id: ' + id);
928 }
929
930 this.charts[id] = new DashboardChart(id, this);
931 }
932
933 return this.charts[id];
934 }
935 /**
936 * @returns all charts on the dashboard
937 */
938
939
940 async getAllCharts() {
941 const [chartIds] = await this._send('get', 'charts');
942
943 if (!Array.isArray(chartIds)) {
944 return Promise.reject('unexpected response received from iframe');
945 }
946
947 const charts = [];
948 chartIds.forEach(id => {
949 if (!this.charts[id]) {
950 this.charts[id] = new DashboardChart(id, this);
951 }
952
953 charts.push(this.charts[id]);
954 });
955 return charts;
956 }
957
958 _configureHost(hostBuilder) {
959 return super._configureHost(hostBuilder).on('event', (event, payload, handlerIds) => {
960 const chartId = payload.chartId;
961
962 this.charts[chartId]._handleEvent(event, payload, handlerIds);
963 });
964 }
965
966}
967
968// Disabled temporarily to fix: https://github.com/mongodb-js/charts-embed-sdk/issues/14
969// Until we come up with a better way to have strong typing for the Stitch client, while
970// also not breaking normal TSC compiles of the SDK
971// import type { StitchAppClient } from 'mongodb-stitch-browser-sdk';
972const isJWTExpired = jwt => {
973 try {
974 const [header, payload, signature] = jwt.split('.');
975 const {
976 exp
977 } = JSON.parse(atob(payload)); // Check the current time against the expiry (minus 5 minutes) in the token
978
979 return Date.now() / 1000 >= exp - 300;
980 } catch (e) {
981 throw new Error('Failed to parse Realm token. Is the StitchClient configured correctly?');
982 }
983};
984/**
985 * A helper utility to support using [Realm Authentication](https://docs.mongodb.com/stitch/) with MongoDB Charts
986 *
987 * ```js
988 * const client = Stitch.initializeDefaultAppClient('<your-client-app-id>');
989 * client.auth.loginWithCredential(...)
990 *
991 * const sdk = new ChartsEmbedSDK({
992 * getUserToken: () => getRealmUserToken(client)
993 * })
994 * ```
995 */
996
997
998async function getRealmUserToken(stitchAppClient) {
999 const client = stitchAppClient;
1000
1001 if (!client.auth || !client.auth.authInfo) {
1002 throw new Error('Unfamiliar Stitch client version');
1003 }
1004
1005 if (!client.auth.isLoggedIn) {
1006 throw new Error('Could not find a logged-in StitchUser. Is the StitchClient configured correctly?');
1007 }
1008
1009 if (!client.auth.authInfo.accessToken) {
1010 throw new Error('Could not find a valid JWT. Is the StitchClient configured correctly?');
1011 }
1012
1013 if (isJWTExpired(client.auth.authInfo.accessToken)) {
1014 // Attempt to refresh token using progression from public -> private apis
1015 if (client.auth.refreshCustomData) {
1016 await client.auth.refreshCustomData(); // supported from 4.8.0
1017 } else if (client.auth.refreshAccessToken) {
1018 await client.auth.refreshAccessToken(); // supported from 4.0.0
1019 } else {
1020 throw new Error('Could not refresh token. Unfamiliar Stitch client version');
1021 }
1022 }
1023
1024 return client.auth.authInfo.accessToken;
1025}
1026
1027function _defineProperty$4(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; }
1028/**
1029 * Creates an instance of the embedding SDK
1030 */
1031
1032class EmbedSDK {
1033 /**
1034 * Accepts an optional {@link EmbedChartOptions} object to use as the
1035 * default options for any charts created using this SDK instance.
1036 *
1037 * ```js
1038 * const sdk = new EmbedSDK({
1039 * baseUrl: "https://charts.mongodb.com",
1040 * })
1041 * ```
1042 */
1043 constructor(options) {
1044 _defineProperty$4(this, "defaultOptions", void 0);
1045
1046 this.defaultOptions = options;
1047 }
1048 /**
1049 * Creates a new {@link Chart} instance that allows you to
1050 * interact with and embed charts into your application
1051 */
1052
1053
1054 createChart(options) {
1055 return new Chart({ ...this.defaultOptions,
1056 ...options
1057 });
1058 }
1059 /**
1060 * Creates a new {@link Dashboard} instance that allows you
1061 * to embed a dashboard into your application
1062 */
1063
1064
1065 createDashboard(options) {
1066 return new Dashboard({ ...this.defaultOptions,
1067 ...options
1068 });
1069 }
1070
1071}
1072
1073exports.default = EmbedSDK;
1074exports.getRealmUserToken = getRealmUserToken;