@thoughtspot/ts-chart-sdk 0.0.1-alpha.0

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 (63) hide show
  1. package/dist/ts-chart-sdk.d.ts +312 -0
  2. package/lib/dist/ts-chart-sdk.d.ts +312 -0
  3. package/lib/index.d.ts +8 -0
  4. package/lib/index.d.ts.map +1 -0
  5. package/lib/index.js +8 -0
  6. package/lib/index.js.map +1 -0
  7. package/lib/main/custom-chart-context.d.ts +38 -0
  8. package/lib/main/custom-chart-context.d.ts.map +1 -0
  9. package/lib/main/custom-chart-context.js +176 -0
  10. package/lib/main/custom-chart-context.js.map +1 -0
  11. package/lib/main/custom-chart-context.spec.d.ts +2 -0
  12. package/lib/main/custom-chart-context.spec.d.ts.map +1 -0
  13. package/lib/main/custom-chart-context.spec.js +315 -0
  14. package/lib/main/custom-chart-context.spec.js.map +1 -0
  15. package/lib/main/post-message-event-bridge.d.ts +6 -0
  16. package/lib/main/post-message-event-bridge.d.ts.map +1 -0
  17. package/lib/main/post-message-event-bridge.js +41 -0
  18. package/lib/main/post-message-event-bridge.js.map +1 -0
  19. package/lib/main/post-message-event-bridge.spec.d.ts +2 -0
  20. package/lib/main/post-message-event-bridge.spec.d.ts.map +1 -0
  21. package/lib/main/post-message-event-bridge.spec.js +117 -0
  22. package/lib/main/post-message-event-bridge.spec.js.map +1 -0
  23. package/lib/test/test-utils.d.ts +7 -0
  24. package/lib/test/test-utils.d.ts.map +1 -0
  25. package/lib/test/test-utils.js +11 -0
  26. package/lib/test/test-utils.js.map +1 -0
  27. package/lib/types/answer-column.types.d.ts +69 -0
  28. package/lib/types/answer-column.types.d.ts.map +1 -0
  29. package/lib/types/answer-column.types.js +56 -0
  30. package/lib/types/answer-column.types.js.map +1 -0
  31. package/lib/types/chart-to-ts-event.types.d.ts +48 -0
  32. package/lib/types/chart-to-ts-event.types.d.ts.map +1 -0
  33. package/lib/types/chart-to-ts-event.types.js +16 -0
  34. package/lib/types/chart-to-ts-event.types.js.map +1 -0
  35. package/lib/types/common.types.d.ts +51 -0
  36. package/lib/types/common.types.d.ts.map +1 -0
  37. package/lib/types/common.types.js +2 -0
  38. package/lib/types/common.types.js.map +1 -0
  39. package/lib/types/configurator.types.d.ts +15 -0
  40. package/lib/types/configurator.types.d.ts.map +1 -0
  41. package/lib/types/configurator.types.js +2 -0
  42. package/lib/types/configurator.types.js.map +1 -0
  43. package/lib/types/ts-to-chart-event.types.d.ts +67 -0
  44. package/lib/types/ts-to-chart-event.types.d.ts.map +1 -0
  45. package/lib/types/ts-to-chart-event.types.js +12 -0
  46. package/lib/types/ts-to-chart-event.types.js.map +1 -0
  47. package/lib/types/visual-prop.types.d.ts +47 -0
  48. package/lib/types/visual-prop.types.d.ts.map +1 -0
  49. package/lib/types/visual-prop.types.js +2 -0
  50. package/lib/types/visual-prop.types.js.map +1 -0
  51. package/package.json +88 -0
  52. package/src/index.ts +7 -0
  53. package/src/main/custom-chart-context.spec.ts +432 -0
  54. package/src/main/custom-chart-context.ts +587 -0
  55. package/src/main/post-message-event-bridge.spec.ts +215 -0
  56. package/src/main/post-message-event-bridge.ts +88 -0
  57. package/src/test/test-utils.ts +13 -0
  58. package/src/types/answer-column.types.ts +174 -0
  59. package/src/types/chart-to-ts-event.types.ts +159 -0
  60. package/src/types/common.types.ts +187 -0
  61. package/src/types/configurator.types.ts +102 -0
  62. package/src/types/ts-to-chart-event.types.ts +257 -0
  63. package/src/types/visual-prop.types.ts +271 -0
@@ -0,0 +1,587 @@
1
+ /**
2
+ * @file Custom Chart Context
3
+ * @fileoverview
4
+ * @author Chetan Agrawal <chetan.agrawal@thoughtspot.com>
5
+ *
6
+ * Copyright: ThoughtSpot Inc. 2023
7
+ */
8
+ import _ from 'lodash';
9
+ import {
10
+ ChartToTSEventsPayloadMap,
11
+ ErrorType,
12
+ } from '../types/chart-to-ts-event.types';
13
+ import {
14
+ ChartConfig,
15
+ ChartModel,
16
+ ValidationResponse,
17
+ VisualProps,
18
+ } from '../types/common.types';
19
+ import { ChartConfigEditorDefinition } from '../types/configurator.types';
20
+ import {
21
+ ChartConfigValidateEventPayload,
22
+ ChartModelUpdateEventPayload,
23
+ DataUpdateEventPayload,
24
+ GetDataQueryPayload,
25
+ GetDataQueryResponsePayload,
26
+ InitializeEventPayload,
27
+ InitializeEventResponsePayload,
28
+ Query,
29
+ TSToChartEvent,
30
+ TSToChartEventsPayloadMap,
31
+ TSToChartInternalEventsPayloadMap,
32
+ VisualPropsUpdateEventPayload,
33
+ VisualPropsValidateEventPayload,
34
+ } from '../types/ts-to-chart-event.types';
35
+ import { VisualPropEditorDefinition } from '../types/visual-prop.types';
36
+ import * as PostMessageEventBridge from './post-message-event-bridge';
37
+
38
+ let isInitialized = false;
39
+
40
+ export type CustomChartContextProps = {
41
+ /**
42
+ * Generate the default axis configuration for rendering the chart on first load.
43
+ *
44
+ * @param chartModel
45
+ * @returns {@link ChartConfig[]}
46
+ * @version SDK: 0.1 | ThoughtSpot:
47
+ */
48
+ getDefaultChartConfig: (chartModel: ChartModel) => ChartConfig[];
49
+ /**
50
+ * Generate query in the form of array of chart columns to fetch the data.
51
+ *
52
+ * @param ChartConfig[] chartConfig
53
+ * @returns {@link Array<Query>}
54
+ * @version SDK: 0.1 | ThoughtSpot:
55
+ */
56
+ getQueriesFromChartConfig: (chartConfig: ChartConfig[]) => Query[];
57
+ /**
58
+ * Main Render function that will render the chart based on the chart context provided
59
+ *
60
+ * @version SDK: 0.1 | ThoughtSpot:
61
+ * @param {CustomChartContext} ctx the chart context sdk object
62
+ * @returns {@link Promise<void>} Promise object to resolve once the chart is rendered
63
+ */
64
+ renderChart: (ctx: CustomChartContext) => Promise<void>;
65
+ /**
66
+ * required to validate the current chart configuration
67
+ * that chart user has updated on the chart config editor
68
+ *
69
+ * @version SDK: 0.1 | ThoughtSpot:
70
+ * @param updatedConfig
71
+ * @param chartModel
72
+ * @returns {@link ValidationResponse}
73
+ */
74
+ validateConfig?: (
75
+ updatedConfig: ChartConfig[],
76
+ chartModel: ChartModel,
77
+ ) => ValidationResponse;
78
+
79
+ /**
80
+ * required to validate the custom visual props that
81
+ * the chart user has updated on the chart settings editor.
82
+ *
83
+ * @param updatedVisualProps
84
+ * @param chartModel
85
+ * @returns {@link ValidationResponse}
86
+ * @version SDK: 0.1 | ThoughtSpot:
87
+ */
88
+ validateVisualProps?: (
89
+ updatedVisualProps: VisualProps,
90
+ chartModel: ChartModel,
91
+ ) => ValidationResponse;
92
+
93
+ /**
94
+ * Definition to help edit/customize the chart config from chart config editor on the
95
+ * TS app. If not provided, chart queries will not be configurable in editor
96
+ *
97
+ * @version SDK: 0.1 | ThoughtSpot:
98
+ */
99
+ chartConfigEditorDefinition?: ChartConfigEditorDefinition[];
100
+
101
+ /**
102
+ * Definition to help edit/customize the visual properties from chart settings editor
103
+ * on the TS app. If not provided, visual properties will not be configurable in
104
+ * editor
105
+ *
106
+ * @version SDK: 0.1 | ThoughtSpot:
107
+ */
108
+ visualPropEditorDefinition?: VisualPropEditorDefinition;
109
+ };
110
+
111
+ /**
112
+ * Default configuration options for all the chart context properties
113
+ */
114
+ const DEFAULT_CHART_CONTEXT_PROPS: Partial<CustomChartContextProps> = {
115
+ validateConfig: () => ({ isValid: true }),
116
+ validateVisualProps: () => ({ isValid: true }),
117
+ chartConfigEditorDefinition: undefined,
118
+ visualPropEditorDefinition: undefined,
119
+ };
120
+
121
+ export class CustomChartContext {
122
+ /**
123
+ * Id to map to the parent chart component.
124
+ * This is used to differentiate between multiple chart componenets rendered on the
125
+ * parent app Example: liveboards with multiple charts
126
+ *
127
+ * @hidden
128
+ * @internal
129
+ * @version SDK: 0.1 | ThoughtSpot:
130
+ */
131
+ private componentId = '';
132
+
133
+ /**
134
+ * host app url
135
+ *
136
+ * @hidden
137
+ * @internal
138
+ * @version SDK: 0.1 | ThoughtSpot:
139
+ */
140
+ private hostUrl = '';
141
+
142
+ /**
143
+ * Chart Model object. This contains the complete metadata persisted with TS
144
+ * application Also contains data.
145
+ *
146
+ * @version SDK: 0.1 | ThoughtSpot:
147
+ */
148
+ private chartModel: ChartModel = {} as any;
149
+
150
+ /**
151
+ * Chart Props Object to define the workflow of the application
152
+ *
153
+ * @version SDK: 0.1 | ThoughtSpot:
154
+ */
155
+ private chartContextProps: CustomChartContextProps =
156
+ DEFAULT_CHART_CONTEXT_PROPS as CustomChartContextProps;
157
+
158
+ /**
159
+ * Map of event listeners for the TS to chart events
160
+ *
161
+ * @version SDK: 0.1 | ThoughtSpot:
162
+ */
163
+ // TODO: define a better type mapping here.
164
+ private eventListeners: Record<string, ((...args: any[]) => void)[]> = {};
165
+
166
+ /**
167
+ * This is a promise object to wait on till the chart context
168
+ * is initialized by the parent app
169
+ *
170
+ * @hidden
171
+ * @internal
172
+ * @version SDK: 0.1 | ThoughtSpot:
173
+ */
174
+
175
+ private hasInitializedPromise: Promise<void>;
176
+
177
+ /**
178
+ * This function triggers on initialization completion from TS app.
179
+ *
180
+ * @hidden
181
+ * @internal
182
+ * @version SDK: 0.1 | ThoughtSpot:
183
+ */
184
+ private triggerInitResolve: () => void = _.noop;
185
+
186
+ /**
187
+ * Constructor to only accept context props as payload
188
+ *
189
+ * @param {CustomChartContextProps} chartContextProps
190
+ */
191
+ constructor(chartContextProps: CustomChartContextProps) {
192
+ this.chartContextProps = {
193
+ ...DEFAULT_CHART_CONTEXT_PROPS,
194
+ ...chartContextProps,
195
+ };
196
+ this.registerEventProcessor();
197
+ this.hasInitializedPromise = new Promise((resolve) => {
198
+ this.triggerInitResolve = resolve;
199
+ });
200
+ }
201
+
202
+ /**
203
+ * Function to expose and wait on the initialize promise object.
204
+ * This will resolve immediately if the chart context is already initialized by TS
205
+ * App.
206
+ *
207
+ * @version SDK: 0.1 | ThoughtSpot:
208
+ */
209
+ public initialize = (): Promise<void> => {
210
+ console.log('Chart Context: initialization start');
211
+ return this.hasInitializedPromise;
212
+ };
213
+
214
+ /**
215
+ * Add event listeners on the chart context with the required callback.
216
+ *
217
+ * @param {T} eventType
218
+ * @param {TSToChartEventsPayloadMap[T]} callbackFn
219
+ * @returns void
220
+ * @version SDK: 0.1 | ThoughtSpot:
221
+ */
222
+ public on<T extends keyof TSToChartEventsPayloadMap>(
223
+ eventType: T,
224
+ callbackFn: TSToChartEventsPayloadMap[T],
225
+ ): void {
226
+ // TODO(chetan): [FIX ME] This function doesnt fail if the function
227
+ // callback is mapped with different argument types
228
+ if (_.isNil(this.eventListeners[eventType])) {
229
+ this.eventListeners[eventType] = [];
230
+ }
231
+ this.eventListeners[eventType].push(callbackFn);
232
+ }
233
+
234
+ /**
235
+ * Add internal event listeners on the chart context with the required callback.
236
+ *
237
+ * @param {T} eventType
238
+ * @param {TSToChartInternalEventsPayloadMap[T]} callbackFn
239
+ * @returns void
240
+ * @version SDK: 0.1 | ThoughtSpot:
241
+ */
242
+ private onInternal<T extends keyof TSToChartInternalEventsPayloadMap>(
243
+ eventType: T,
244
+ callbackFn: TSToChartInternalEventsPayloadMap[T],
245
+ ): void {
246
+ if (_.isNil(this.eventListeners[eventType])) {
247
+ this.eventListeners[eventType] = [];
248
+ }
249
+ this.eventListeners[eventType].push(callbackFn);
250
+ }
251
+
252
+ /**
253
+ * Destroy the chart context object and stop listening to the parent post message
254
+ * events
255
+ *
256
+ * @version SDK: 0.1 | ThoughtSpot:
257
+ */
258
+ public destroy() {
259
+ PostMessageEventBridge.destroyMessageListener(this.eventProcessor);
260
+ isInitialized = false;
261
+ }
262
+
263
+ /**
264
+ * Getter for the chart model object
265
+ *
266
+ * @version SDK: 0.1 | ThoughtSpot:
267
+ */
268
+ public getChartModel = (): ChartModel => this.chartModel;
269
+
270
+ /**
271
+ * Function to emit Chart to TS Events to the TS application.
272
+
273
+ * @param {T} eventType Type of the event
274
+ * @param {ChartToTSEventsPayloadMap[T]} eventPayload Event payload bound
275
+ * to the type of the event
276
+ * @returns Promise
277
+ */
278
+ public emitEvent<T extends keyof ChartToTSEventsPayloadMap>(
279
+ eventType: T,
280
+ eventPayload: ChartToTSEventsPayloadMap[T],
281
+ ): Promise<any> {
282
+ if (!isInitialized) {
283
+ console.log(
284
+ 'Chart Context: not initialized the context, something went wrong',
285
+ );
286
+ return Promise.reject();
287
+ }
288
+ return PostMessageEventBridge.postMessageToHostApp(
289
+ this.componentId,
290
+ this.hostUrl,
291
+ eventPayload,
292
+ eventType,
293
+ );
294
+ }
295
+
296
+ /**
297
+ * This registers the current chart context to the post message event bridge
298
+ * Process all the functions via the eventProcess callback
299
+ */
300
+ private registerEventProcessor = () => {
301
+ if (isInitialized) {
302
+ console.error(
303
+ 'The context is already initialized. you cannot have multiple contexts',
304
+ );
305
+ throw new Error(ErrorType.MultipleContextsNotSupported);
306
+ }
307
+ PostMessageEventBridge.initMessageListener(this.eventProcessor);
308
+
309
+ this.registerEvents();
310
+ };
311
+
312
+ /**
313
+ * Process received host messages from the post message event bridge
314
+ *
315
+ * @param event : Message Event Object
316
+ */
317
+ private eventProcessor = (event: MessageEvent) => {
318
+ if (event.data.source !== 'ts-host-app') {
319
+ return;
320
+ }
321
+
322
+ console.log(
323
+ 'Chart Context: message received:',
324
+ event.data.eventType,
325
+ event.data,
326
+ );
327
+
328
+ const messageResponse = this.executeEventListenerCBs(event);
329
+
330
+ // respond back to parent to confirm/ack the receipt
331
+ if (!_.isNil(event.ports[0])) {
332
+ if (!_.isNil(messageResponse)) {
333
+ event.ports[0].postMessage({
334
+ ...(messageResponse as any),
335
+ });
336
+ } else {
337
+ event.ports[0].postMessage({});
338
+ }
339
+ }
340
+ };
341
+
342
+ /**
343
+ * Register all internal and default external events for TSToChartEvent types
344
+ *
345
+ * @returns void
346
+ */
347
+ private registerEvents = (): void => {
348
+ // Register Internal Events
349
+ // These events will not be readable by the developer
350
+
351
+ /**
352
+ * This event is triggered when the TS app initializes the app
353
+ */
354
+ this.onInternal(
355
+ TSToChartEvent.Initialize,
356
+ (payload: InitializeEventPayload) =>
357
+ this.initializeContext(payload),
358
+ );
359
+
360
+ /**
361
+ * This event is triggered when the TS app asks for validating the updated visual
362
+ * props If {validateVisualProps} is not defined, default is always returned as
363
+ * true.
364
+ */
365
+ this.onInternal(
366
+ TSToChartEvent.VisualPropsValidate,
367
+ (payload: VisualPropsValidateEventPayload): ValidationResponse => {
368
+ if (this.chartContextProps.validateVisualProps) {
369
+ const validationResponse =
370
+ this.chartContextProps.validateVisualProps(
371
+ payload.visualProps,
372
+ this.chartModel,
373
+ );
374
+ return validationResponse;
375
+ }
376
+ // this will never be true
377
+ return { isValid: false };
378
+ },
379
+ );
380
+
381
+ /**
382
+ * This event is triggered when the TS app asks for validating the updated chart
383
+ * configuration If {validateConfig} is not defined, default is always returned as
384
+ * true.
385
+ */
386
+ this.onInternal(
387
+ TSToChartEvent.ChartConfigValidate,
388
+ (payload: ChartConfigValidateEventPayload): ValidationResponse => {
389
+ if (this.chartContextProps.validateConfig) {
390
+ const validationResponse =
391
+ this.chartContextProps.validateConfig(
392
+ payload.chartConfig,
393
+ this.chartModel,
394
+ );
395
+ return validationResponse;
396
+ }
397
+ // this will never be true
398
+ return { isValid: false };
399
+ },
400
+ );
401
+
402
+ /**
403
+ * This event is triggered when the TS app asks for the base queries required
404
+ * by the chart to render.
405
+ */
406
+ this.onInternal(
407
+ TSToChartEvent.GetDataQuery,
408
+ (payload: GetDataQueryPayload): GetDataQueryResponsePayload => {
409
+ const queries =
410
+ this.chartContextProps.getQueriesFromChartConfig(
411
+ payload.config,
412
+ );
413
+ return {
414
+ queries,
415
+ };
416
+ },
417
+ );
418
+
419
+ /**
420
+ * This event is triggered when the TS app re-renders the chart
421
+ */
422
+ this.onInternal(TSToChartEvent.TriggerRenderChart, async () => {
423
+ await this.chartContextProps.renderChart(this);
424
+ });
425
+
426
+ // Register External Events
427
+ // These events are readable by the developer
428
+
429
+ /**
430
+ * This event is triggered when the TS app sends the updated chart model.
431
+ * If event is not defined by developer, default is always sent to refresh the
432
+ * chart iframe.
433
+ */
434
+ this.on(
435
+ TSToChartEvent.ChartModelUpdate,
436
+ (
437
+ payload: ChartModelUpdateEventPayload,
438
+ ): { triggerRenderChart: boolean } => {
439
+ this.chartModel = payload.chartModel;
440
+ return {
441
+ triggerRenderChart: true,
442
+ };
443
+ },
444
+ );
445
+
446
+ /**
447
+ * This event is triggered when the TS app sends the updated data.
448
+ * If event is not defined by developer, default is always sent to refresh the
449
+ * chart iframe.
450
+ */
451
+ this.on(
452
+ TSToChartEvent.DataUpdate,
453
+ (
454
+ payload: DataUpdateEventPayload,
455
+ ): {
456
+ triggerRenderChart: boolean;
457
+ } => {
458
+ this.chartModel.data = payload.data;
459
+ return {
460
+ triggerRenderChart: true,
461
+ };
462
+ },
463
+ );
464
+
465
+ /**
466
+ * This event is triggered when the TS app sends the updated visual properties.
467
+ * If event is not defined by developer, default is always sent to refresh the
468
+ * chart iframe.
469
+ */
470
+ this.on(
471
+ TSToChartEvent.VisualPropsUpdate,
472
+ (
473
+ payload: VisualPropsUpdateEventPayload,
474
+ ): { triggerRenderChart: boolean } => {
475
+ this.chartModel.visualProps = payload.visualProps;
476
+ return {
477
+ triggerRenderChart: true,
478
+ };
479
+ },
480
+ );
481
+ };
482
+
483
+ /**
484
+ * private function is going to initialize the flow from TS app and send back
485
+ * the coinfiguration back to ts app to complete the handshake.
486
+ *
487
+ * @param {InitializeEventPayload} payload
488
+ * @returns InitializeEventResponsePayload
489
+ */
490
+ private initializeContext = (
491
+ payload: InitializeEventPayload,
492
+ ): InitializeEventResponsePayload => {
493
+ this.componentId = payload.componentId;
494
+ this.hostUrl = payload.hostUrl;
495
+ this.chartModel = payload.chartModel;
496
+
497
+ // context is now initialized
498
+ isInitialized = true;
499
+
500
+ // TODO: following can be done behind a promise
501
+ this.triggerInitResolve();
502
+
503
+ return this.publishChartContextPropsToHost();
504
+ };
505
+
506
+ private publishChartContextPropsToHost =
507
+ (): InitializeEventResponsePayload => {
508
+ // for first search, this would be null
509
+ const hasChartConfig = !_.isEmpty(
510
+ this.chartModel.config.chartConfig,
511
+ );
512
+ const { isValid } =
513
+ hasChartConfig && this.chartContextProps.validateConfig
514
+ ? this.chartContextProps.validateConfig(
515
+ this.chartModel.config.chartConfig ?? [],
516
+ this.chartModel,
517
+ )
518
+ : { isValid: false };
519
+
520
+ let defaultChartConfig: ChartConfig[] = [];
521
+ if (!isValid) {
522
+ defaultChartConfig =
523
+ this.chartContextProps.getDefaultChartConfig(
524
+ this.chartModel,
525
+ );
526
+ }
527
+
528
+ return {
529
+ isConfigValid: isValid,
530
+ defaultChartConfig,
531
+ chartConfigEditorDefinition:
532
+ this.chartContextProps.chartConfigEditorDefinition,
533
+ visualPropEditorDefinition:
534
+ this.chartContextProps.visualPropEditorDefinition,
535
+ };
536
+ };
537
+
538
+ /**
539
+ * Process each message events
540
+ *
541
+ * @param event : Message Event Object
542
+ * @returns response to be sent back to the message sender (host)
543
+ */
544
+ private executeEventListenerCBs = (event: MessageEvent): any => {
545
+ // do basic sanity
546
+ const payload = event.data.payload;
547
+ let response;
548
+ if (_.isArray(this.eventListeners[event.data.eventType])) {
549
+ this.eventListeners[event.data.eventType].forEach((callback) => {
550
+ // this is a problem today if we have multipe callbacks
551
+ // registered. only the last responsed will be sent back to the
552
+ // server
553
+ response = callback(payload);
554
+ });
555
+ } else {
556
+ response = {
557
+ hasError: true,
558
+ error: `Event type not recognised or processed: ${event.data.eventType}`,
559
+ };
560
+ }
561
+
562
+ console.log(
563
+ 'ChartContext: Response:',
564
+ event.data.eventType,
565
+ response,
566
+ this.eventListeners[event.data.eventType]?.length,
567
+ );
568
+ return response;
569
+ };
570
+ }
571
+
572
+ /**
573
+ * Get the initialized chart context object
574
+ *
575
+ * @param {CustomChartContextProps} customChartConfig
576
+ * @returns Promise
577
+ */
578
+ export const getChartContext = async (
579
+ customChartConfig: CustomChartContextProps,
580
+ ): Promise<CustomChartContext> => {
581
+ const ctx = new CustomChartContext(customChartConfig);
582
+ // wait for initialization here as the host app
583
+ // needs to first handshake with the client app.
584
+ await ctx.initialize();
585
+
586
+ return ctx;
587
+ };