UNPKG

22.3 kBJavaScriptView Raw
1"use strict";
2
3function _interopDefault(ex) {
4 return ex && "object" == typeof ex && "default" in ex ? ex.default : ex;
5}
6
7Object.defineProperty(exports, "__esModule", {
8 value: !0
9});
10
11var bson = require("bson"), chatty = require("@looker/chatty"), _isEqual = _interopDefault(require("lodash/isEqual")), _isEmpty = _interopDefault(require("lodash/isEmpty"));
12
13function createElement(name, props = {}, children = []) {
14 const element = document.createElement(name);
15 for (const [name, value] of Object.entries(props)) "style" === name ? Object.assign(element.style, props.style) : element.setAttribute(name, value);
16 for (const child of Array.isArray(children) ? children : [ children ]) element.append(child);
17 return element;
18}
19
20let THEME_ENUM, SCALING_ENUM;
21
22!function(THEME_ENUM) {
23 THEME_ENUM.DARK = "dark", THEME_ENUM.LIGHT = "light";
24}(THEME_ENUM || (THEME_ENUM = {})), function(SCALING_ENUM) {
25 SCALING_ENUM.FIXED = "fixed", SCALING_ENUM.SCALE = "scale";
26}(SCALING_ENUM || (SCALING_ENUM = {}));
27
28const getSharedEmbedOptions = options => {
29 const {background: background, baseUrl: baseUrl, autoRefresh: autoRefresh, maxDataAge: maxDataAge, width: width, height: height, theme: theme, showAttribution: showAttribution, getUserToken: getUserToken} = options;
30 if ("string" != typeof baseUrl || 0 === baseUrl.length) throw new Error("Base URL must be a valid URL");
31 if (void 0 !== background && "string" != typeof background) throw new Error("background must be a string if specified");
32 if (void 0 !== autoRefresh && "boolean" != typeof autoRefresh) throw new Error("autoRefresh must be a boolean if specified");
33 if (void 0 !== maxDataAge && "number" != typeof maxDataAge) throw new Error("maxDataAge must be a number if specified");
34 if (void 0 !== width && ![ "number", "string" ].includes(typeof width)) throw new Error("Width must be a string or number if specified");
35 if (void 0 !== height && ![ "number", "string" ].includes(typeof height)) throw new Error("Height must be a string or number if specified");
36 if (void 0 !== theme && "string" != typeof theme) throw new Error("Theme must be a string if specified");
37 if (void 0 !== showAttribution && "boolean" != typeof showAttribution) throw new Error("Attribution must be a boolean value if specified");
38 if (void 0 !== getUserToken && "function" != typeof getUserToken) throw new Error("getUserToken must be a function");
39 return {
40 background: background,
41 baseUrl: baseUrl,
42 autoRefresh: autoRefresh,
43 maxDataAge: maxDataAge,
44 width: width,
45 height: height,
46 theme: theme,
47 showAttribution: showAttribution,
48 getUserToken: getUserToken
49 };
50}, getPathname = (url, pathname) => [ url.pathname, "/" === url.pathname.slice(-1) ? "" : "/", pathname ].join(""), getChartUrl = options => {
51 try {
52 const url = new URL(options.baseUrl);
53 return url.pathname = getPathname(url, "embed/charts"), url.search = `id=${options.chartId}&sdk=2`,
54 !1 === options.autoRefresh ? url.search += "&autorefresh=false" : void 0 === options.autoRefresh && (url.search += options.refreshInterval ? `&autorefresh=${options.refreshInterval}` : ""),
55 void 0 !== options.maxDataAge && (url.search += `&maxDataAge=${options.maxDataAge}`),
56 options.filter && (url.search += `&filter=${encodeURIComponent(bson.EJSON.stringify(options.filter, {
57 relaxed: !1
58 }))}`), options.theme && (url.search += `&theme=${options.theme}`), !1 === options.showAttribution && (url.search += "&attribution=false"),
59 url.toString();
60 } catch (e) {
61 throw new Error("Base URL must be a valid URL");
62 }
63}, getDashboardUrl = options => {
64 try {
65 const url = new URL(options.baseUrl);
66 return url.pathname = getPathname(url, "embed/dashboards"), url.search = `id=${options.dashboardId}&sdk=1`,
67 !1 === options.autoRefresh && (url.search += "&autoRefresh=false"), void 0 !== options.maxDataAge && (url.search += `&maxDataAge=${options.maxDataAge}`),
68 !0 === options.showTitleAndDesc && (url.search += "&showTitleAndDesc=true"), options.widthMode && (url.search += `&scalingWidth=${options.widthMode}`),
69 options.heightMode && (url.search += `&scalingHeight=${options.heightMode}`), options.theme && (url.search += `&theme=${options.theme}`),
70 options.chartsBackground && (url.search += `&chartsBackground=${options.chartsBackground}`),
71 !1 === options.showAttribution && (url.search += "&attribution=false"), url.toString();
72 } catch (e) {
73 throw new Error("Base URL must be a valid URL");
74 }
75}, parseCSSMeasurement = value => "string" == typeof value ? value : "number" == typeof value ? `${value}px` : null, getBackground = (background, theme, lightBackground, darkBackground) => "string" == typeof background && background.length > 0 ? background : "dark" === theme ? darkBackground : lightBackground;
76
77function _defineProperty(obj, key, value) {
78 return key in obj ? Object.defineProperty(obj, key, {
79 value: value,
80 enumerable: !0,
81 configurable: !0,
82 writable: !0
83 }) : obj[key] = value, obj;
84}
85
86class BaseEmbedItem {
87 constructor() {
88 _defineProperty(this, "iframe", void 0), _defineProperty(this, "connection", void 0),
89 _defineProperty(this, "name", void 0), _defineProperty(this, "ERRORS", void 0),
90 _defineProperty(this, "COLOUR", void 0), _defineProperty(this, "options", void 0);
91 }
92 async render(container) {
93 if (this.iframe) throw new Error(this.ERRORS.IFRAME);
94 const embedRoot = this._configureEmbedRoot(createElement("div", {
95 style: {
96 position: "relative",
97 overflow: "hidden",
98 minHeight: Boolean(this.options.height) ? 0 : "15px",
99 width: parseCSSMeasurement(this.options.width) || "100%",
100 height: parseCSSMeasurement(this.options.height) || "100%"
101 }
102 })), 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();
103 for (host.iframe.setAttribute("aria-label", this.name), Object.assign(host.iframe.style, {
104 position: "absolute",
105 top: 0,
106 left: 0,
107 border: 0,
108 width: "100%",
109 height: "100%"
110 }); container.firstChild; ) container.removeChild(container.firstChild);
111 container.appendChild(embedRoot), this.connection = await host.connect(), this.iframe = host.iframe,
112 this._setBackground(this.options.background, this.options.theme), await this._retrieveAndSetToken(),
113 await this._send("ready");
114 }
115 async isAutoRefresh() {
116 const [result] = await this._send("get", "autoRefresh");
117 return "number" == typeof result || "boolean" == typeof result ? Boolean(result) : Promise.reject("unexpected response received from iframe");
118 }
119 async setAutoRefresh(value) {
120 if ("boolean" != typeof value) return Promise.reject("autoRefresh property value should be a boolean");
121 await this._send("set", "autoRefresh", value);
122 }
123 async getMaxDataAge() {
124 const [result] = await this._send("get", "maxDataAge");
125 return "number" == typeof result ? result : Promise.reject("unexpected response received from iframe");
126 }
127 async setMaxDataAge(value) {
128 if ("number" != typeof value) return Promise.reject("maxDataAge property value should be a number");
129 await this._send("set", "maxDataAge", value);
130 }
131 async setTheme(value) {
132 if ("string" != typeof value) return Promise.reject("theme property value should be a string");
133 const newTheme = Object.values(THEME_ENUM).includes(value) ? value : THEME_ENUM.LIGHT;
134 await this._send("set", "theme", newTheme), this._setBackground(this.options.background, newTheme);
135 }
136 async getTheme() {
137 const [result] = await this._send("get", "theme");
138 return "string" == typeof result ? result : Promise.reject("unexpected response received from iframe");
139 }
140 _configureHost(hostBuilder) {
141 return hostBuilder.on("refreshToken", () => this._retrieveAndSetToken());
142 }
143 _configureEmbedRoot(embedRoot) {
144 return embedRoot;
145 }
146 _setBackground(background, theme) {
147 this.iframe.style.backgroundColor = getBackground(background, theme, this.COLOUR.LIGHT, this.COLOUR.DARK);
148 }
149 async _retrieveAndSetToken() {
150 if (this.options.getUserToken) {
151 const token = await this.options.getUserToken();
152 await this._send("set", "token", token);
153 }
154 }
155 _send(eventName, ...payload) {
156 return this.connection ? this.connection.sendAndReceive(eventName, ...payload) : Promise.reject(this.ERRORS.SEND);
157 }
158}
159
160function _defineProperty$1(obj, key, value) {
161 return key in obj ? Object.defineProperty(obj, key, {
162 value: value,
163 enumerable: !0,
164 configurable: !0,
165 writable: !0
166 }) : obj[key] = value, obj;
167}
168
169let eventHandlerIndex = Date.now();
170
171function EventSource(Sender) {
172 return class extends Sender {
173 constructor(...args) {
174 super(...args), _defineProperty$1(this, "_eventHandlers", {
175 click: {}
176 });
177 }
178 _handleEvent(event, payload, handlerIds) {
179 const handlers = this._eventHandlers[event];
180 for (const id of handlerIds) try {
181 var _handlers$id;
182 null === (_handlers$id = handlers[id]) || void 0 === _handlers$id || _handlers$id.handle(payload);
183 } catch (error) {
184 console.warn(`Error calling handler for event [${event}]: ${error}`);
185 }
186 }
187 addEventListener(event, eventHandler, options) {
188 var _h$options$includes;
189 const handlers = this._eventHandlers[event];
190 if (!handlers) throw new Error(`Not supported event: ${event}`);
191 const h = {
192 handle: eventHandler,
193 options: {
194 includes: null == options ? void 0 : options.includes
195 }
196 };
197 if (null !== (_h$options$includes = h.options.includes) && void 0 !== _h$options$includes && _h$options$includes.every(f => _isEmpty(f)) && console.warn("Empty includes filters out all events. Event handler will never be called. Is this intended?"),
198 !Object.keys(handlers).some(id => _isEqual(handlers[id], h))) {
199 const handlerId = (++eventHandlerIndex).toString(36);
200 return handlers[handlerId] = h, this._send("eventHandler", event, {
201 handlerId: handlerId,
202 options: h.options
203 });
204 }
205 return Promise.resolve();
206 }
207 removeEventListener(event, eventHandler, options) {
208 const handlers = this._eventHandlers[event];
209 if (!handlers) throw new Error(`Not supported event: ${event}`);
210 const h = {
211 handle: eventHandler,
212 options: {
213 includes: null == options ? void 0 : options.includes
214 }
215 }, handlerId = Object.keys(handlers).find(id => _isEqual(handlers[id], h));
216 return handlerId ? (delete handlers[handlerId], this._send("eventHandler", event, {
217 handlerId: handlerId
218 })) : Promise.resolve();
219 }
220 };
221}
222
223function Refreshable(Sender) {
224 return class extends Sender {
225 async refresh() {
226 await this._send("refresh");
227 }
228 };
229}
230
231function _defineProperty$2(obj, key, value) {
232 return key in obj ? Object.defineProperty(obj, key, {
233 value: value,
234 enumerable: !0,
235 configurable: !0,
236 writable: !0
237 }) : obj[key] = value, obj;
238}
239
240const getChartOptions = options => {
241 if ("object" != typeof options || null === options) throw new Error("Options argument must be an object");
242 const sharedEmbedOptions = getSharedEmbedOptions(options), {chartId: chartId, filter: filter, refreshInterval: refreshInterval} = options;
243 if ("string" != typeof chartId || 0 === chartId.length) throw new Error("Chart ID must be specified");
244 if (void 0 !== filter && (!filter || "object" != typeof filter)) throw new Error("Filter must be an object if specified");
245 if (void 0 !== refreshInterval && "number" != typeof refreshInterval) throw new Error("refreshInterval interval must be a number if specified");
246 return {
247 ...sharedEmbedOptions,
248 chartId: chartId,
249 filter: filter,
250 refreshInterval: refreshInterval
251 };
252};
253
254class ChartEventSender extends BaseEmbedItem {
255 constructor(options) {
256 super(), _defineProperty$2(this, "name", "Embedded Chart"), _defineProperty$2(this, "ERRORS", {
257 SEND: "Chart has not been rendered. Ensure that you wait for the promise returned by `chart.render()` before trying to manipulate a chart.",
258 IFRAME: "A chart can only be rendered into a container once"
259 }), _defineProperty$2(this, "COLOUR", {
260 LIGHT: "#FFFFFF",
261 DARK: "#21313C"
262 }), _defineProperty$2(this, "options", void 0), this.options = getChartOptions(options);
263 }
264 getEmbedUrl() {
265 return getChartUrl(this.options);
266 }
267}
268
269class Chart extends(Refreshable(EventSource(ChartEventSender))){
270 async getRefreshInterval() {
271 const [result] = await this._send("get", "autorefresh");
272 return console.warn("The 'getRefreshInterval' method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes."),
273 "number" == typeof result ? result : Promise.reject("unexpected response received from iframe");
274 }
275 async setRefreshInterval(value) {
276 if ("number" != typeof value) return Promise.reject("refreshInterval property value should be a number");
277 console.warn("The 'setRefreshInterval' method is deprecated. Please use the 'autoRefresh' option with the 'maxDataAge' option to configure how often the chart refreshes."),
278 await this._send("set", "autorefresh", value);
279 }
280 async getFilter() {
281 const [result] = await this._send("get", "filter");
282 return "object" == typeof result && null !== result ? result : Promise.reject("unexpected response received from iframe");
283 }
284 async setFilter(value) {
285 if ("object" != typeof value || null === value || Array.isArray(value)) return Promise.reject("filter property value should be an object");
286 await this._send("set", "filter", bson.EJSON.stringify(value, {
287 relaxed: !1
288 }));
289 }
290 async getHighlight() {
291 const [result] = await this._send("get", "highlight");
292 return "object" == typeof result && null !== result ? result : Promise.reject("unexpected response received from iframe");
293 }
294 async setHighlight(value) {
295 if ("object" != typeof value || null === value || Array.isArray(value)) return Promise.reject("highlight property value should be an object");
296 await this._send("set", "highlight", bson.EJSON.stringify(value, {
297 relaxed: !1
298 }));
299 }
300 _configureHost(hostBuilder) {
301 return super._configureHost(hostBuilder).on("event", this._handleEvent.bind(this));
302 }
303 async getData() {
304 const [result] = await this._send("get", "data");
305 return "object" == typeof result && null !== result ? result : Promise.reject("unexpected response received from iframe");
306 }
307}
308
309class DashboardChartEventSender {
310 constructor(chartId, dashboard) {
311 this.chartId = chartId, this.dashboard = dashboard;
312 }
313 _send(msgName, ...payload) {
314 return this.dashboard._send(msgName, ...payload, this.chartId);
315 }
316}
317
318class DashboardChart extends(Refreshable(EventSource(DashboardChartEventSender))){}
319
320function _defineProperty$3(obj, key, value) {
321 return key in obj ? Object.defineProperty(obj, key, {
322 value: value,
323 enumerable: !0,
324 configurable: !0,
325 writable: !0
326 }) : obj[key] = value, obj;
327}
328
329const getDashboardOptions = options => {
330 if ("object" != typeof options || null === options) throw new Error("Options argument must be an object");
331 const sharedEmbedOptions = getSharedEmbedOptions(options), {dashboardId: dashboardId, chartsBackground: chartsBackground, widthMode: widthMode, heightMode: heightMode, showTitleAndDesc: showTitleAndDesc} = options;
332 if ("string" != typeof dashboardId || 0 === dashboardId.length) throw new Error("dashboardId must be specified");
333 if (void 0 !== chartsBackground && "string" != typeof chartsBackground) throw new Error("chartsBackground must be a string if specified");
334 if (void 0 !== widthMode && "string" != typeof widthMode) throw new Error("widthMode must be a string if specified");
335 if (void 0 !== widthMode && widthMode !== SCALING_ENUM.FIXED && widthMode !== SCALING_ENUM.SCALE) throw new Error(`widthMode must be "${SCALING_ENUM.FIXED}" or "${SCALING_ENUM.SCALE}"`);
336 if (void 0 !== heightMode && "string" != typeof heightMode) throw new Error("heightMode must be a string if specified");
337 if (void 0 !== heightMode && heightMode !== SCALING_ENUM.FIXED && heightMode !== SCALING_ENUM.SCALE) throw new Error(`heightMode must be "${SCALING_ENUM.FIXED}" or "${SCALING_ENUM.SCALE}"`);
338 if (void 0 !== showTitleAndDesc && "boolean" != typeof showTitleAndDesc) throw new Error("showTitleAndDesc must be a boolean value if specified");
339 return {
340 ...sharedEmbedOptions,
341 dashboardId: dashboardId,
342 chartsBackground: chartsBackground,
343 widthMode: widthMode,
344 heightMode: heightMode,
345 showTitleAndDesc: showTitleAndDesc
346 };
347};
348
349class DashboardEventSender extends BaseEmbedItem {
350 constructor(options) {
351 super(), _defineProperty$3(this, "name", "Embedded Dashboard"), _defineProperty$3(this, "ERRORS", {
352 SEND: "Dashboard has not been rendered. Ensure that you wait for the promise returned by `dashboard.render()` before trying to manipulate a dashboard.",
353 IFRAME: "A dashboard can only be rendered into a container once"
354 }), _defineProperty$3(this, "COLOUR", {
355 LIGHT: "#F1F5F4",
356 DARK: "#12212C"
357 }), _defineProperty$3(this, "options", void 0), this.options = getDashboardOptions(options);
358 }
359 getEmbedUrl() {
360 return getDashboardUrl(this.options);
361 }
362}
363
364class Dashboard extends(Refreshable(DashboardEventSender)){
365 constructor(...args) {
366 super(...args), _defineProperty$3(this, "charts", {});
367 }
368 async getChartsBackground() {
369 const [result] = await this._send("get", "chartsBackground");
370 return "string" == typeof result ? result : Promise.reject("unexpected response received from iframe");
371 }
372 async setChartsBackground(value) {
373 if ("string" != typeof value) return Promise.reject("chartsBackground property value should be a string");
374 await this._send("set", "chartsBackground", value);
375 }
376 async isShowAttribution() {
377 const [result] = await this._send("get", "attribution");
378 return "boolean" == typeof result ? Boolean(result) : Promise.reject("unexpected response received from iframe");
379 }
380 async setShowAttribution(value) {
381 if ("boolean" != typeof value) return Promise.reject("showAttribution property value should be a boolean");
382 await this._send("set", "attribution", value);
383 }
384 async getWidthMode() {
385 const [result] = await this._send("get", "scalingWidth");
386 return result === SCALING_ENUM.FIXED || result === SCALING_ENUM.SCALE ? result : Promise.reject("unexpected response received from iframe");
387 }
388 async setWidthMode(value) {
389 if (![ "fixed", "scale" ].includes(value)) return Promise.reject('widthMode property value should be a string value of "fixed" or "scale"');
390 await this._send("set", "scalingWidth", value);
391 }
392 async getHeightMode() {
393 const [result] = await this._send("get", "scalingHeight");
394 return "fixed" === result || "scale" === result ? result : Promise.reject("unexpected response received from iframe");
395 }
396 async setHeightMode(value) {
397 if (![ "fixed", "scale" ].includes(value)) return Promise.reject('heightMode property value should be a string value of "fixed" or "scale"');
398 await this._send("set", "scalingHeight", value);
399 }
400 async getChart(id) {
401 if (!this.charts[id]) {
402 const [chartIds] = await this._send("get", "charts", [ id ]);
403 if (!Array.isArray(chartIds)) return Promise.reject("unexpected response received from iframe");
404 if (1 !== chartIds.length) return Promise.reject("Invalid chart id: " + id);
405 this.charts[id] = new DashboardChart(id, this);
406 }
407 return this.charts[id];
408 }
409 async getAllCharts() {
410 const [chartIds] = await this._send("get", "charts");
411 if (!Array.isArray(chartIds)) return Promise.reject("unexpected response received from iframe");
412 const charts = [];
413 return chartIds.forEach(id => {
414 this.charts[id] || (this.charts[id] = new DashboardChart(id, this)), charts.push(this.charts[id]);
415 }), charts;
416 }
417 _configureHost(hostBuilder) {
418 return super._configureHost(hostBuilder).on("event", (event, payload, handlerIds) => {
419 const chartId = payload.chartId;
420 this.charts[chartId]._handleEvent(event, payload, handlerIds);
421 });
422 }
423}
424
425const isJWTExpired = jwt => {
426 try {
427 const [header, payload, signature] = jwt.split("."), {exp: exp} = JSON.parse(atob(payload));
428 return Date.now() / 1e3 >= exp - 300;
429 } catch (e) {
430 throw new Error("Failed to parse Realm token. Is the StitchClient configured correctly?");
431 }
432};
433
434async function getRealmUserToken(stitchAppClient) {
435 const client = stitchAppClient;
436 if (!client.auth || !client.auth.authInfo) throw new Error("Unfamiliar Stitch client version");
437 if (!client.auth.isLoggedIn) throw new Error("Could not find a logged-in StitchUser. Is the StitchClient configured correctly?");
438 if (!client.auth.authInfo.accessToken) throw new Error("Could not find a valid JWT. Is the StitchClient configured correctly?");
439 if (isJWTExpired(client.auth.authInfo.accessToken)) if (client.auth.refreshCustomData) await client.auth.refreshCustomData(); else {
440 if (!client.auth.refreshAccessToken) throw new Error("Could not refresh token. Unfamiliar Stitch client version");
441 await client.auth.refreshAccessToken();
442 }
443 return client.auth.authInfo.accessToken;
444}
445
446function _defineProperty$4(obj, key, value) {
447 return key in obj ? Object.defineProperty(obj, key, {
448 value: value,
449 enumerable: !0,
450 configurable: !0,
451 writable: !0
452 }) : obj[key] = value, obj;
453}
454
455class EmbedSDK {
456 constructor(options) {
457 _defineProperty$4(this, "defaultOptions", void 0), this.defaultOptions = options;
458 }
459 createChart(options) {
460 return new Chart({
461 ...this.defaultOptions,
462 ...options
463 });
464 }
465 createDashboard(options) {
466 return new Dashboard({
467 ...this.defaultOptions,
468 ...options
469 });
470 }
471}
472
473exports.default = EmbedSDK, exports.getRealmUserToken = getRealmUserToken;