UNPKG

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