@ixon-cdk/iframe-adapter 1.2.0-next.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/index.js +450 -0
  3. package/package.json +14 -0
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # IXON Component Development Kit IFrame Adapter
2
+
3
+ This library contains an iframe adapter for the IXON Component Development Kit (CDK). It creates a bridge between a component that mounts an `<iframe>` element and the application that is loaded inside of it. This allows the embedded application to use the same ComponentContext API's that are provided to the component.
package/index.js ADDED
@@ -0,0 +1,450 @@
1
+ const EVENT_NS = 'cdk:cmpctx:';
2
+
3
+ const EVENTS = {
4
+ // Lifecycle
5
+ Create: `${EVENT_NS}create`,
6
+ Created: `${EVENT_NS}created`,
7
+ Destroy: `${EVENT_NS}destroy`,
8
+
9
+ // ResourceDataClient
10
+ ResourceDataClientCreate: `${EVENT_NS}rdc:create`,
11
+ ResourceDataClientDestroy: `${EVENT_NS}rdc:destroy`,
12
+ ResourceDataClientQuery: `${EVENT_NS}rdc:query`,
13
+ ResourceDataClientQueryResult: `${EVENT_NS}rdc:queryresult`,
14
+ ResourceDataClientRender: `${EVENT_NS}rdc:render`,
15
+ ResourceDataClientRenderResult: `${EVENT_NS}rdc:renderresult`,
16
+
17
+ // VPN
18
+ ShowVpnStatusDetails: `${EVENT_NS}showvpnstatusdetails`,
19
+ ToggleVpn: `${EVENT_NS}togglevpn`,
20
+ VpnClientStatusChange: `${EVENT_NS}vpnclientstatuschange`,
21
+
22
+ // Miscellaneous
23
+ NavigateByUrl: `${EVENT_NS}navigatebyurl`,
24
+ SetTimeRange: `${EVENT_NS}settimerange`,
25
+ TimeRangeChange: `${EVENT_NS}timerangechange`,
26
+ };
27
+
28
+ class NotImplementedError extends Error {
29
+ constructor() {
30
+ super('Method is not implemented');
31
+ this.name = 'NotImplementedError';
32
+ this.message = 'Method is not implemented';
33
+ this.stack = 'NotImplementedError: Method is not implemented';
34
+ }
35
+ }
36
+
37
+ class ComponentContext {
38
+ ontimerangechange = null;
39
+ onvpnclientstatuschange = null;
40
+
41
+ constructor(serializableContext) {
42
+ this._getApiLinks = () => new Map(serializableContext._apiLinkEntries);
43
+ this.appData = serializableContext.appData;
44
+ this.componentBaseUrl = serializableContext.componentBaseUrl;
45
+ this.destroyed = serializableContext.destroyed;
46
+ this.mode = serializableContext.mode;
47
+ this.inputs = serializableContext.inputs;
48
+ this.inputMap = new Map(Object.entries(this.inputs));
49
+ this.timeRange = serializableContext.timeRange;
50
+ this.timeRangeIsAdjustable = serializableContext.timeRangeIsAdjustable;
51
+ this.vpnClientStatus = serializableContext.vpnClientStatus;
52
+ this._messageHandler = this._messageHandler.bind(this);
53
+ window.addEventListener('message', this._messageHandler, false);
54
+ }
55
+
56
+ _messageHandler(event) {
57
+ if (event.origin !== window.origin) {
58
+ return;
59
+ }
60
+
61
+ if (event.source !== window.parent) {
62
+ return;
63
+ }
64
+
65
+ switch (event.data.type) {
66
+ case EVENTS.TimeRangeChange:
67
+ if (this.ontimerangechange) {
68
+ this.ontimerangechange(...event.data.payload);
69
+ }
70
+ break;
71
+ case EVENTS.VpnClientStatusChange:
72
+ if (this.onvpnclientstatuschange) {
73
+ this.onvpnclientstatuschange(...event.data.payload);
74
+ }
75
+ break;
76
+ }
77
+ }
78
+
79
+ createBackendComponentClient() {
80
+ throw new NotImplementedError();
81
+ }
82
+
83
+ createLoggingDataClient() {
84
+ throw new NotImplementedError();
85
+ }
86
+
87
+ createResourceDataClient() {
88
+ const clientId = crypto.randomUUID();
89
+ window.parent.postMessage(
90
+ {
91
+ type: EVENTS.ResourceDataClientCreate,
92
+ payload: { clientId },
93
+ },
94
+ window.origin,
95
+ );
96
+ return new ResourceDataClient(clientId);
97
+ }
98
+
99
+ createTooltip() {
100
+ throw new NotImplementedError();
101
+ }
102
+
103
+ destroy() {
104
+ window.parent.postMessage({ type: EVENTS.Destroy }, window.origin);
105
+ window.removeEventListener('message', this._messageHandler, false);
106
+ this.destroyed = true;
107
+ }
108
+
109
+ getApiUrl(rel, params) {
110
+ const link = this._getApiLinks().get(rel);
111
+ if (!link) {
112
+ throw new Error('IxApiError: entry not found');
113
+ }
114
+ // NOTE:
115
+ // This implementation part is an exact copy of the shared `getLinkUrl` utility
116
+ // function which can be found in `@lib/ixon/ixapi/ixapi.utils.ts
117
+ if (params) {
118
+ return Object.keys(params)
119
+ .sort()
120
+ .reduce((href, key) => {
121
+ if (href.includes(`{${key}}`)) {
122
+ return href.replace(`{${key}}`, params[key]);
123
+ }
124
+ const url = new URL(href);
125
+ url.searchParams.set(key, params[key]);
126
+ return decodeURIComponent(url.href);
127
+ }, link.href);
128
+ }
129
+ return link.href;
130
+ }
131
+
132
+ navigateByUrl(url) {
133
+ window.parent.postMessage(
134
+ { type: EVENTS.NavigateByUrl, payload: { url } },
135
+ window.origin,
136
+ );
137
+ }
138
+
139
+ saveAsFile() {
140
+ throw new NotImplementedError();
141
+ }
142
+
143
+ setTimeRange(timeRange) {
144
+ window.parent.postMessage(
145
+ {
146
+ type: EVENTS.SetTimeRange,
147
+ payload: { timeRange },
148
+ },
149
+ window.origin,
150
+ );
151
+ }
152
+
153
+ showVpnStatusDetails(agentId) {
154
+ window.parent.postMessage(
155
+ {
156
+ type: EVENTS.ShowVpnStatusDetails,
157
+ payload: { agentId },
158
+ },
159
+ window.origin,
160
+ );
161
+ }
162
+
163
+ testVpnAccess() {
164
+ throw new NotImplementedError();
165
+ }
166
+
167
+ toggleVpn(agentId) {
168
+ window.parent.postMessage(
169
+ { type: EVENTS.ToggleVpn, payload: { agentId } },
170
+ window.origin,
171
+ );
172
+ }
173
+
174
+ translate() {
175
+ throw new NotImplementedError();
176
+ }
177
+ }
178
+
179
+ class ResourceDataClient {
180
+ constructor(id) {
181
+ this.id = id;
182
+ }
183
+
184
+ query(query, listener) {
185
+ const queryId = crypto.randomUUID();
186
+
187
+ window.addEventListener('message', _queryHandler, false);
188
+
189
+ window.parent.postMessage(
190
+ {
191
+ type: EVENTS.ResourceDataClientQuery,
192
+ payload: { clientId: this.id, queryId, query },
193
+ },
194
+ window.origin,
195
+ );
196
+
197
+ return () => {
198
+ window.removeEventListener('message', _queryHandler, false);
199
+ };
200
+
201
+ function _queryHandler(event) {
202
+ if (event.origin !== window.origin) {
203
+ return;
204
+ }
205
+
206
+ if (event.source !== window.parent) {
207
+ return;
208
+ }
209
+
210
+ if (
211
+ event.data.type === EVENTS.ResourceDataClientQueryResult &&
212
+ event.data.payload.queryId === queryId
213
+ ) {
214
+ listener(event.data.payload.results);
215
+ }
216
+ }
217
+ }
218
+
219
+ render(query, listener) {
220
+ const queryId = crypto.randomUUID();
221
+
222
+ window.addEventListener('message', _queryHandler, false);
223
+
224
+ window.parent.postMessage(
225
+ {
226
+ type: EVENTS.ResourceDataClientRender,
227
+ payload: { clientId: this.id, queryId, query },
228
+ },
229
+ window.origin,
230
+ );
231
+
232
+ return () => {
233
+ window.removeEventListener('message', _queryHandler, false);
234
+ };
235
+
236
+ function _queryHandler(event) {
237
+ if (event.origin !== window.origin) {
238
+ return;
239
+ }
240
+
241
+ if (event.source !== window.parent) {
242
+ return;
243
+ }
244
+
245
+ if (
246
+ event.data.type === EVENTS.ResourceDataClientRenderResult &&
247
+ event.data.payload.queryId === queryId
248
+ ) {
249
+ listener(event.data.payload.results);
250
+ }
251
+ }
252
+ }
253
+
254
+ destroy() {
255
+ window.parent.postMessage(
256
+ {
257
+ type: EVENTS.ResourceDataClientDestroy,
258
+ payload: { clientId: this.id },
259
+ },
260
+ window.origin,
261
+ );
262
+ }
263
+ }
264
+
265
+ export function getComponentContext(options) {
266
+ const hasInputMigrations = !!(options && options.migrateInputs);
267
+
268
+ if (!hasInputMigrations && getComponentContext.__cache__) {
269
+ return Promise.resolve(getComponentContext.__cache__);
270
+ }
271
+
272
+ return new Promise(resolve => {
273
+ window.addEventListener(
274
+ 'message',
275
+ function messageHandler(event) {
276
+ if (event.origin !== window.origin) {
277
+ return;
278
+ }
279
+
280
+ if (event.source !== window.parent) {
281
+ return;
282
+ }
283
+
284
+ if (event.data && event.data.type === EVENTS.Created) {
285
+ let serializableContext = event.data.payload;
286
+
287
+ if (hasInputMigrations) {
288
+ serializableContext.inputs = options.migrateInputs(
289
+ serializableContext.inputs,
290
+ null,
291
+ );
292
+ }
293
+
294
+ const context = new ComponentContext(serializableContext);
295
+ resolve(context);
296
+
297
+ // memoize
298
+ if (!hasInputMigrations) {
299
+ getComponentContext.__cache__ = context;
300
+ }
301
+
302
+ window.removeEventListener('message', messageHandler, false);
303
+ }
304
+ },
305
+ false,
306
+ );
307
+
308
+ // dispatch "create" event
309
+ window.parent.postMessage({ type: EVENTS.Create }, window.origin);
310
+ });
311
+ }
312
+
313
+ export function connect({ iframe, context }) {
314
+ window.addEventListener('message', _adapterMessageHandler, false);
315
+
316
+ // Broadcasts time range changes to connected iframe
317
+ context.ontimerangechange = (...args) => {
318
+ iframe.contentWindow.postMessage({
319
+ type: EVENTS.TimeRangeChange,
320
+ payload: args,
321
+ });
322
+ };
323
+
324
+ // Broadcasts VPN Client status changes to connected iframe
325
+ context.onvpnclientstatuschange = (...args) => {
326
+ iframe.contentWindow.postMessage({
327
+ type: EVENTS.VpnClientStatusChange,
328
+ payload: args,
329
+ });
330
+ };
331
+
332
+ return {
333
+ destroy() {
334
+ window.removeEventListener('message', _adapterMessageHandler, false);
335
+ },
336
+ };
337
+
338
+ /**
339
+ * Returns a serializable object for the passed CompontContext instance
340
+ */
341
+ function _getSerializableContext(context) {
342
+ return {
343
+ _apiLinkEntries: [...context._getApiLinks().entries()],
344
+ appData: context.appData,
345
+ componentBaseUrl: context.componentBaseUrl,
346
+ destroyed: context.destroyed,
347
+ mode: context.mode,
348
+ inputs: context.inputs,
349
+ timeRange: context.timeRange,
350
+ timeRangeIsAdjustable: context.timeRangeIsAdjustable,
351
+ vpnClientStatus: context.vpnClientStatus,
352
+ };
353
+ }
354
+
355
+ function _adapterMessageHandler(event) {
356
+ _adapterMessageHandler._clients =
357
+ _adapterMessageHandler._clients || new Map([]);
358
+
359
+ if (event.origin !== window.origin) {
360
+ return;
361
+ }
362
+
363
+ if (event.source !== iframe.contentWindow) {
364
+ return;
365
+ }
366
+
367
+ switch (event.data.type) {
368
+ /**
369
+ * Lifecycle
370
+ */
371
+ case EVENTS.Create:
372
+ iframe.contentWindow.postMessage({
373
+ type: EVENTS.Created,
374
+ payload: _getSerializableContext(context),
375
+ });
376
+ break;
377
+ case EVENTS.Destroy:
378
+ context.destroy();
379
+ break;
380
+
381
+ /**
382
+ * ResourceDataClient
383
+ */
384
+ case EVENTS.ResourceDataClientCreate: {
385
+ const { clientId } = event.data.payload;
386
+ const client = context.createResourceDataClient(clientId);
387
+ _adapterMessageHandler._clients.set(clientId, client);
388
+ break;
389
+ }
390
+ case EVENTS.ResourceDataClientDestroy: {
391
+ const { clientId } = event.data.payload;
392
+ if (_adapterMessageHandler._clients.has(clientId)) {
393
+ _adapterMessageHandler._clients.get(clientId).destroy();
394
+ _adapterMessageHandler._clients.delete(clientId);
395
+ }
396
+ break;
397
+ }
398
+ case EVENTS.ResourceDataClientQuery: {
399
+ const { clientId, queryId, query } = event.data.payload;
400
+ let client = _adapterMessageHandler._clients.get(clientId);
401
+ if (client && !client._destroyed) {
402
+ client.query(query, results => {
403
+ if (client && !client._destroyed) {
404
+ iframe.contentWindow.postMessage({
405
+ type: EVENTS.ResourceDataClientQueryResult,
406
+ payload: { queryId, results },
407
+ });
408
+ }
409
+ });
410
+ }
411
+ break;
412
+ }
413
+ case EVENTS.ResourceDataClientRender: {
414
+ const { clientId, queryId, query } = event.data.payload;
415
+ let client = _adapterMessageHandler._clients.get(clientId);
416
+ if (client && !client._destroyed) {
417
+ client.render(query, results => {
418
+ if (client && !client._destroyed) {
419
+ iframe.contentWindow.postMessage({
420
+ type: EVENTS.ResourceDataClientRenderResult,
421
+ payload: { queryId, results },
422
+ });
423
+ }
424
+ });
425
+ }
426
+ break;
427
+ }
428
+
429
+ /**
430
+ * VPN
431
+ */
432
+ case EVENTS.ShowVpnStatusDetails:
433
+ context.showVpnStatusDetails(event.data.payload.agentId);
434
+ break;
435
+ case EVENTS.ToggleVpn:
436
+ context.toggleVpn(event.data.payload.agentId);
437
+ break;
438
+
439
+ /**
440
+ * Miscellaneous
441
+ */
442
+ case EVENTS.NavigateByUrl:
443
+ context.navigateByUrl(event.data.payload.url);
444
+ break;
445
+ case EVENTS.SetTimeRange:
446
+ context.setTimeRange(event.data.payload.timeRange);
447
+ break;
448
+ }
449
+ }
450
+ }
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@ixon-cdk/iframe-adapter",
3
+ "version": "1.2.0-next.4",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "author": "",
7
+ "license": "ISC",
8
+ "files": [
9
+ "!index.spec.js"
10
+ ],
11
+ "peerDependencies": {
12
+ "@ixon-cdk/types": "^1.2.0-next.4"
13
+ }
14
+ }