@quicdata/analytics 0.0.2 → 0.0.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 (81) hide show
  1. package/core/auth.d.ts +19 -0
  2. package/core/auth.d.ts.map +1 -0
  3. package/core/auth.js +34 -0
  4. package/core/fetch-data.d.ts +115 -0
  5. package/core/fetch-data.d.ts.map +1 -0
  6. package/core/fetch-data.js +210 -0
  7. package/core/index.d.ts +9 -0
  8. package/core/index.d.ts.map +1 -0
  9. package/core/index.js +4 -0
  10. package/core/types.d.ts +250 -0
  11. package/core/types.d.ts.map +1 -0
  12. package/core/viewport-observer.d.ts +26 -0
  13. package/core/viewport-observer.d.ts.map +1 -0
  14. package/core/viewport-observer.js +38 -0
  15. package/core/widget-transform-runner.d.ts +22 -0
  16. package/core/widget-transform-runner.d.ts.map +1 -0
  17. package/core/widget-transform-runner.js +102 -0
  18. package/dashboard/dashboard-container.d.ts +100 -0
  19. package/dashboard/dashboard-container.d.ts.map +1 -0
  20. package/dashboard/dashboard-container.js +315 -0
  21. package/dashboard/index.d.ts +4 -0
  22. package/dashboard/index.d.ts.map +1 -0
  23. package/dashboard/index.js +2 -0
  24. package/designer/analytics-designer.d.ts +40 -0
  25. package/designer/analytics-designer.d.ts.map +1 -0
  26. package/designer/analytics-designer.js +267 -0
  27. package/designer/index.d.ts +5 -0
  28. package/designer/index.d.ts.map +1 -0
  29. package/designer/index.js +3 -0
  30. package/filters/filter-bar.d.ts +34 -0
  31. package/filters/filter-bar.d.ts.map +1 -0
  32. package/filters/filter-bar.js +233 -0
  33. package/filters/filter-button.d.ts +22 -0
  34. package/filters/filter-button.d.ts.map +1 -0
  35. package/filters/filter-button.js +86 -0
  36. package/filters/index.d.ts +7 -0
  37. package/filters/index.d.ts.map +1 -0
  38. package/filters/index.js +6 -0
  39. package/filters/widget-toolbar.d.ts +24 -0
  40. package/filters/widget-toolbar.d.ts.map +1 -0
  41. package/filters/widget-toolbar.js +216 -0
  42. package/index.d.ts +10 -0
  43. package/index.d.ts.map +1 -0
  44. package/index.js +6 -0
  45. package/package.json +27 -26
  46. package/widgets/analytics-report.d.ts +49 -0
  47. package/widgets/analytics-report.d.ts.map +1 -0
  48. package/widgets/analytics-report.js +306 -0
  49. package/widgets/analytics-widget.d.ts +39 -0
  50. package/widgets/analytics-widget.d.ts.map +1 -0
  51. package/widgets/analytics-widget.js +230 -0
  52. package/widgets/bar-chart.d.ts +13 -0
  53. package/widgets/bar-chart.d.ts.map +1 -0
  54. package/widgets/bar-chart.js +77 -0
  55. package/widgets/base-chart.d.ts +94 -0
  56. package/widgets/base-chart.d.ts.map +1 -0
  57. package/widgets/base-chart.js +535 -0
  58. package/widgets/index.d.ts +14 -0
  59. package/widgets/index.d.ts.map +1 -0
  60. package/widgets/index.js +14 -0
  61. package/widgets/line-chart.d.ts +13 -0
  62. package/widgets/line-chart.d.ts.map +1 -0
  63. package/widgets/line-chart.js +71 -0
  64. package/widgets/pie-chart.d.ts +13 -0
  65. package/widgets/pie-chart.d.ts.map +1 -0
  66. package/widgets/pie-chart.js +55 -0
  67. package/widgets/table.d.ts +101 -0
  68. package/widgets/table.d.ts.map +1 -0
  69. package/widgets/table.js +740 -0
  70. package/workers/widget-transform.worker.d.ts +21 -0
  71. package/workers/widget-transform.worker.d.ts.map +1 -0
  72. package/workers/widget-transform.worker.js +30 -0
  73. package/src/core/index.d.ts +0 -2
  74. package/src/core/index.d.ts.map +0 -1
  75. package/src/core/index.js +0 -3
  76. package/src/index.d.ts +0 -2
  77. package/src/index.d.ts.map +0 -1
  78. package/src/widgets/index.d.ts +0 -2
  79. package/src/widgets/index.d.ts.map +0 -1
  80. package/src/widgets/index.js +0 -3
  81. /package/{src/index.js → core/types.js} +0 -0
@@ -0,0 +1,740 @@
1
+ var TableWidget_1;
2
+ import { __decorate } from "tslib";
3
+ import { LitElement, html, css, nothing } from 'lit';
4
+ import { customElement, property, state } from 'lit/decorators.js';
5
+ import { buildWidgetDataUrl, fetchWidgetData, fetchWidgetDefinition, } from '../core/fetch-data.js';
6
+ import { EVENT_DASHBOARD_FILTER_CHANGE, EVENT_DASHBOARD_REFRESH, } from '../dashboard/index.js';
7
+ import { observeViewport } from '../core/viewport-observer.js';
8
+ import '../filters/index.js';
9
+ /**
10
+ * Table widget. Fetches data from data-url (Laravel analytics API)
11
+ * or uses inline data, then renders an HTML table.
12
+ * When dashboard is set, merges dashboard params with dataParams and listens for
13
+ * analytics-dashboard-filter-change and analytics-dashboard-refresh.
14
+ */
15
+ let TableWidget = class TableWidget extends LitElement {
16
+ static { TableWidget_1 = this; }
17
+ static { this.styles = css `
18
+ :host {
19
+ display: flex;
20
+ flex-direction: column;
21
+ width: 100%;
22
+ height: 100%;
23
+ overflow: hidden;
24
+ }
25
+ table {
26
+ width: 100%;
27
+ border-collapse: collapse;
28
+ font-size: var(--table-font-size, 0.875rem);
29
+ }
30
+ th,
31
+ td {
32
+ padding: 0.5rem 0.75rem;
33
+ text-align: left;
34
+ border-bottom: 1px solid #e5e7eb;
35
+ }
36
+ th[rowspan],
37
+ td[rowspan] {
38
+ vertical-align: top;
39
+ }
40
+ th {
41
+ font-weight: 600;
42
+ color: #374151;
43
+ background: #f9fafb;
44
+ }
45
+ tr.totals-row td {
46
+ font-weight: 600;
47
+ background: #f3f4f6;
48
+ border-top: 2px solid #e5e7eb;
49
+ }
50
+ tr.totals-row.totals-row-sticky td {
51
+ position: sticky;
52
+ z-index: 1;
53
+ box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.06);
54
+ }
55
+ tr.totals-row.totals-row-sticky-bottom td {
56
+ bottom: 0;
57
+ }
58
+ tr.totals-row.totals-row-sticky-top td {
59
+ top: 0;
60
+ }
61
+ tr:hover td:not(.rowspan-placeholder) {
62
+ background: #f9fafb;
63
+ }
64
+ .error {
65
+ color: #c00;
66
+ padding: 0.5rem;
67
+ font-size: 0.875rem;
68
+ }
69
+ .loading-body {
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ min-height: 80px;
74
+ padding: 1rem;
75
+ }
76
+ .loading-overlay {
77
+ position: absolute;
78
+ inset: 0;
79
+ z-index: 2;
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ background: rgba(255, 255, 255, 0.65);
84
+ backdrop-filter: blur(4px);
85
+ -webkit-backdrop-filter: blur(4px);
86
+ border-radius: 4px;
87
+ }
88
+ .loading-spinner {
89
+ width: 2rem;
90
+ height: 2rem;
91
+ border: 2px solid #e5e7eb;
92
+ border-top-color: #3b82f6;
93
+ border-radius: 50%;
94
+ animation: analytics-spin 0.7s linear infinite;
95
+ }
96
+ @keyframes analytics-spin {
97
+ to { transform: rotate(360deg); }
98
+ }
99
+ .empty {
100
+ padding: 0.5rem;
101
+ color: #6b7280;
102
+ }
103
+ td.empty-placeholder {
104
+ text-align: center;
105
+ padding: 2rem 0.75rem;
106
+ color: #9ca3af;
107
+ }
108
+ .widget-with-toolbar {
109
+ position: relative;
110
+ width: 100%;
111
+ height: 100%;
112
+ display: flex;
113
+ flex-direction: column;
114
+ min-height: 0;
115
+ }
116
+ .widget-with-toolbar:hover analytics-widget-toolbar {
117
+ opacity: 1;
118
+ pointer-events: auto;
119
+ }
120
+ .widget-header {
121
+ flex-shrink: 0;
122
+ padding: 0.5rem 0.75rem 0.25rem 0.75rem;
123
+ min-width: 0;
124
+ }
125
+ .widget-title {
126
+ margin: 0;
127
+ font-size: 0.9375rem;
128
+ font-weight: 600;
129
+ color: #374151;
130
+ line-height: 1.3;
131
+ display: -webkit-box;
132
+ -webkit-box-orient: vertical;
133
+ -webkit-line-clamp: 1;
134
+ overflow: hidden;
135
+ text-overflow: ellipsis;
136
+ }
137
+ .widget-body {
138
+ flex: 1;
139
+ min-height: 0;
140
+ position: relative;
141
+ overflow: auto;
142
+ box-sizing: border-box;
143
+ }
144
+ `; }
145
+ /** Initial delay (ms) before entering debounce; each new resize signal cancels and restarts. */
146
+ static { this._RESIZE_DELAY_MS = 300; }
147
+ /** Debounce (ms) after delay; resize runs only when no signal for this long. */
148
+ static { this._RESIZE_DEBOUNCE_MS = 300; }
149
+ constructor() {
150
+ super();
151
+ this._loadGeneration = 0;
152
+ this._loadDataDebounceId = null;
153
+ this._loadDataInFlight = false;
154
+ this._ignoreNextFilterChange = false;
155
+ this._dashboardEl = null;
156
+ this._hasLoadedOnce = false;
157
+ this._viewportUnobserve = null;
158
+ this._resizeObserver = null;
159
+ this._resizeDelayId = null;
160
+ this._resizeDebounceId = null;
161
+ this._boundOnDashboardFilterChange = (e) => {
162
+ const ce = e;
163
+ this._dashboardParams = { ...(ce.detail?.params ?? {}) };
164
+ this._loadData();
165
+ };
166
+ this._boundOnDashboardRefresh = () => {
167
+ if (this.lazy && !this._viewportVisible)
168
+ return;
169
+ this._loadData();
170
+ };
171
+ this.title = '';
172
+ this.dataUrl = '';
173
+ this.widgetId = '';
174
+ this.apiUrl = '';
175
+ this.dataParams = {};
176
+ this.dashboard = null;
177
+ this.data = null;
178
+ this.meta = { columns: {} };
179
+ this.lazy = false;
180
+ this.prefetchMargin = '200px';
181
+ this.initialDefinition = undefined;
182
+ this._widgetData = null;
183
+ this._loading = false;
184
+ this._error = null;
185
+ this._dashboardParams = {};
186
+ this._widgetFilters = [];
187
+ this._widgetFilterParams = {};
188
+ this._hideFilter = false;
189
+ this._viewportVisible = true;
190
+ this._definitionTitle = '';
191
+ }
192
+ connectedCallback() {
193
+ super.connectedCallback();
194
+ this._attachDashboard();
195
+ }
196
+ disconnectedCallback() {
197
+ this._viewportUnobserve?.();
198
+ this._viewportUnobserve = null;
199
+ if (this._loadDataDebounceId != null) {
200
+ clearTimeout(this._loadDataDebounceId);
201
+ this._loadDataDebounceId = null;
202
+ }
203
+ if (this._resizeDelayId != null) {
204
+ clearTimeout(this._resizeDelayId);
205
+ this._resizeDelayId = null;
206
+ }
207
+ if (this._resizeDebounceId != null) {
208
+ clearTimeout(this._resizeDebounceId);
209
+ this._resizeDebounceId = null;
210
+ }
211
+ this._resizeObserver?.disconnect();
212
+ this._resizeObserver = null;
213
+ this._detachDashboard();
214
+ super.disconnectedCallback();
215
+ }
216
+ updated(changed) {
217
+ if (changed.has('dashboard')) {
218
+ this._detachDashboard();
219
+ this._attachDashboard();
220
+ }
221
+ if (changed.has('widgetId') || changed.has('apiUrl')) {
222
+ this._widgetFilters = [];
223
+ this._widgetFilterParams = {};
224
+ this._definitionTitle = '';
225
+ this._ignoreNextFilterChange = true;
226
+ }
227
+ const onlyFilterState = (changed.has('_widgetFilterParams') || changed.has('_widgetFilters')) &&
228
+ !changed.has('dataUrl') &&
229
+ !changed.has('dataParams') &&
230
+ !changed.has('widgetId') &&
231
+ !changed.has('apiUrl') &&
232
+ !changed.has('_dashboardParams');
233
+ if (onlyFilterState && this._ignoreNextFilterChange) {
234
+ this._ignoreNextFilterChange = false;
235
+ }
236
+ else if (changed.has('dataUrl') ||
237
+ changed.has('dataParams') ||
238
+ changed.has('widgetId') ||
239
+ changed.has('apiUrl') ||
240
+ changed.has('_dashboardParams') ||
241
+ changed.has('_widgetFilterParams')) {
242
+ this._scheduleLoadData();
243
+ }
244
+ if (changed.has('lazy')) {
245
+ this._viewportVisible = !this.lazy;
246
+ }
247
+ }
248
+ _attachDashboard() {
249
+ const d = this.dashboard;
250
+ if (!d)
251
+ return;
252
+ this._dashboardEl = d;
253
+ this._dashboardParams = typeof d.getDashboardParams === 'function' ? d.getDashboardParams() : {};
254
+ d.addEventListener(EVENT_DASHBOARD_FILTER_CHANGE, this._boundOnDashboardFilterChange);
255
+ d.addEventListener(EVENT_DASHBOARD_REFRESH, this._boundOnDashboardRefresh);
256
+ }
257
+ _detachDashboard() {
258
+ const d = this._dashboardEl;
259
+ if (!d)
260
+ return;
261
+ d.removeEventListener(EVENT_DASHBOARD_FILTER_CHANGE, this._boundOnDashboardFilterChange);
262
+ d.removeEventListener(EVENT_DASHBOARD_REFRESH, this._boundOnDashboardRefresh);
263
+ this._dashboardEl = null;
264
+ this._dashboardParams = {};
265
+ }
266
+ firstUpdated() {
267
+ const body = this.renderRoot.querySelector('.widget-body');
268
+ if (body && typeof ResizeObserver !== 'undefined') {
269
+ this._resizeObserver = new ResizeObserver(() => {
270
+ if (this._resizeDelayId != null)
271
+ clearTimeout(this._resizeDelayId);
272
+ if (this._resizeDebounceId != null)
273
+ clearTimeout(this._resizeDebounceId);
274
+ this._resizeDelayId = setTimeout(() => {
275
+ this._resizeDelayId = null;
276
+ this._resizeDebounceId = setTimeout(() => {
277
+ this._resizeDebounceId = null;
278
+ this.requestUpdate();
279
+ }, TableWidget_1._RESIZE_DEBOUNCE_MS);
280
+ }, TableWidget_1._RESIZE_DELAY_MS);
281
+ });
282
+ this._resizeObserver.observe(body);
283
+ }
284
+ if (this.lazy) {
285
+ this._viewportVisible = false;
286
+ this._viewportUnobserve = observeViewport(this, {
287
+ rootMargin: this.prefetchMargin || '200px',
288
+ threshold: 0,
289
+ }, {
290
+ onEnter: () => {
291
+ this._viewportVisible = true;
292
+ if (!this._hasLoadedOnce) {
293
+ this._hasLoadedOnce = true;
294
+ this._loadData();
295
+ }
296
+ },
297
+ onLeave: () => {
298
+ this._viewportVisible = false;
299
+ },
300
+ });
301
+ }
302
+ }
303
+ /** Debounce (ms) so multiple updated() calls (e.g. dataUrl then dataParams in separate ticks) result in one fetch. */
304
+ static { this._LOAD_DATA_DEBOUNCE_MS = 50; }
305
+ /** Debounce data load so multiple updated() calls (e.g. dataUrl then dataParams) result in one fetch. */
306
+ _scheduleLoadData() {
307
+ if (this._loadDataDebounceId != null) {
308
+ clearTimeout(this._loadDataDebounceId);
309
+ this._loadDataDebounceId = null;
310
+ }
311
+ this._loadDataDebounceId = setTimeout(() => {
312
+ this._loadDataDebounceId = null;
313
+ this._loadData();
314
+ }, TableWidget_1._LOAD_DATA_DEBOUNCE_MS);
315
+ }
316
+ _getEffectiveDataUrl() {
317
+ if (this.apiUrl && this.widgetId) {
318
+ return buildWidgetDataUrl(this.apiUrl, this.widgetId);
319
+ }
320
+ return this.dataUrl;
321
+ }
322
+ _getEffectiveParams() {
323
+ const dashboardParams = this._dashboardParams;
324
+ const widgetParams = this.dataParams ?? {};
325
+ let widgetFilterParams = {};
326
+ if (this._hideFilter) {
327
+ for (const f of this._widgetFilters) {
328
+ if (f.default_value !== undefined && f.default_value !== null) {
329
+ widgetFilterParams[f.param_name] = f.default_value;
330
+ }
331
+ }
332
+ }
333
+ else if (this._widgetFilters.length > 0) {
334
+ for (const f of this._widgetFilters) {
335
+ const setByUser = f.param_name in this._widgetFilterParams;
336
+ widgetFilterParams[f.param_name] = setByUser
337
+ ? (this._widgetFilterParams[f.param_name] ?? '')
338
+ : (f.default_value ?? '');
339
+ }
340
+ }
341
+ else {
342
+ widgetFilterParams = { ...this._widgetFilterParams };
343
+ }
344
+ const out = { ...widgetFilterParams, ...widgetParams };
345
+ if (Object.keys(dashboardParams).length > 0) {
346
+ out.dashboard_params = dashboardParams;
347
+ }
348
+ return out;
349
+ }
350
+ /** Display title: optional host `title` overrides definition title from API. */
351
+ get _effectiveTitle() {
352
+ return (this.title != null && this.title !== '') ? this.title : (this._definitionTitle ?? '');
353
+ }
354
+ /** Run a callback after the current update cycle to avoid Lit "change-in-update" warning. */
355
+ _defer(fn) {
356
+ if (typeof requestAnimationFrame !== 'undefined') {
357
+ requestAnimationFrame(fn);
358
+ }
359
+ else {
360
+ setTimeout(fn, 0);
361
+ }
362
+ }
363
+ async _loadData() {
364
+ if (this.lazy && !this._viewportVisible)
365
+ return;
366
+ // Skip fetch only when using pure inline data (no dataUrl). When we have dataUrl we must refetch when params change (e.g. report filters).
367
+ if (this.data != null && !this.dataUrl) {
368
+ this._error = null;
369
+ this._loading = false;
370
+ return;
371
+ }
372
+ const dataUrl = this._getEffectiveDataUrl();
373
+ if (!dataUrl) {
374
+ this._error = null;
375
+ this._loading = false;
376
+ return;
377
+ }
378
+ if (this._loadDataInFlight) {
379
+ return;
380
+ }
381
+ const gen = ++this._loadGeneration;
382
+ this._loadDataInFlight = true;
383
+ this._loading = true;
384
+ this._error = null;
385
+ try {
386
+ const useWidgetApi = Boolean(this.apiUrl && this.widgetId);
387
+ if (useWidgetApi) {
388
+ let params;
389
+ if (this._widgetFilters.length === 0) {
390
+ const def = this.initialDefinition ?? (await fetchWidgetDefinition(this.apiUrl, this.widgetId));
391
+ this._definitionTitle = def.title ?? '';
392
+ this._widgetFilters = def.filters ?? [];
393
+ this._hideFilter = def.hide_filter ?? false;
394
+ const defaultParams = {};
395
+ for (const f of (def.filters ?? [])) {
396
+ if (f.default_value !== undefined && f.default_value !== null) {
397
+ defaultParams[f.param_name] = f.default_value;
398
+ }
399
+ }
400
+ this._widgetFilterParams = { ...defaultParams, ...this._widgetFilterParams };
401
+ this._ignoreNextFilterChange = true;
402
+ params = { ...defaultParams, ...(this.dataParams ?? {}) };
403
+ if (Object.keys(this._dashboardParams).length > 0) {
404
+ params.dashboard_params = this._dashboardParams;
405
+ }
406
+ }
407
+ else {
408
+ params = this._getEffectiveParams();
409
+ }
410
+ const res = await fetchWidgetData(dataUrl, params);
411
+ if (gen !== this._loadGeneration)
412
+ return;
413
+ const data = res.data ?? [];
414
+ const meta = res.meta ?? { columns: {} };
415
+ const wd = res.widgetData;
416
+ const widgetData = wd && wd.chartType === 'Table'
417
+ ? {
418
+ chartType: 'Table',
419
+ columns: wd.columns ?? [],
420
+ columnDefs: wd.columnDefs,
421
+ header_rows: wd.header_rows,
422
+ rows: wd.rows ?? res.data ?? [],
423
+ rowSpanRuns: wd.rowSpanRuns,
424
+ font_size: wd.font_size,
425
+ totals_sticky: wd.totals_sticky,
426
+ totals_position: wd.totals_position,
427
+ }
428
+ : null;
429
+ const filters = res.filters;
430
+ const hideFilter = res.hide_filter;
431
+ this._defer(() => {
432
+ if (gen !== this._loadGeneration)
433
+ return;
434
+ this._loadDataInFlight = false;
435
+ this.data = data;
436
+ this.meta = meta;
437
+ this._widgetData = widgetData;
438
+ if (filters?.length)
439
+ this._widgetFilters = filters;
440
+ else if (hideFilter !== true)
441
+ this._widgetFilters = [];
442
+ if (hideFilter !== undefined)
443
+ this._hideFilter = hideFilter;
444
+ this._loading = false;
445
+ });
446
+ }
447
+ else {
448
+ const params = this._getEffectiveParams();
449
+ const res = await fetchWidgetData(dataUrl, params);
450
+ if (gen !== this._loadGeneration)
451
+ return;
452
+ const data = res.data ?? [];
453
+ const meta = res.meta ?? { columns: {} };
454
+ const wd = res.widgetData;
455
+ const widgetData = wd && wd.chartType === 'Table'
456
+ ? {
457
+ chartType: 'Table',
458
+ columns: wd.columns ?? [],
459
+ columnDefs: wd.columnDefs,
460
+ header_rows: wd.header_rows,
461
+ rows: wd.rows ?? res.data ?? [],
462
+ rowSpanRuns: wd.rowSpanRuns,
463
+ font_size: wd.font_size,
464
+ totals_sticky: wd.totals_sticky,
465
+ totals_position: wd.totals_position,
466
+ }
467
+ : null;
468
+ const filters = res.filters;
469
+ const hideFilter = res.hide_filter;
470
+ this._defer(() => {
471
+ if (gen !== this._loadGeneration)
472
+ return;
473
+ this._loadDataInFlight = false;
474
+ this.data = data;
475
+ this.meta = meta;
476
+ this._widgetData = widgetData;
477
+ if (filters?.length)
478
+ this._widgetFilters = filters;
479
+ else if (hideFilter !== true)
480
+ this._widgetFilters = [];
481
+ if (hideFilter !== undefined)
482
+ this._hideFilter = hideFilter;
483
+ this._loading = false;
484
+ });
485
+ }
486
+ }
487
+ catch (e) {
488
+ const errMsg = e instanceof Error ? e.message : String(e);
489
+ this._defer(() => {
490
+ if (gen !== this._loadGeneration)
491
+ return;
492
+ this._loadDataInFlight = false;
493
+ this._error = errMsg;
494
+ this.data = [];
495
+ this._widgetData = null;
496
+ this._loading = false;
497
+ });
498
+ }
499
+ }
500
+ _onWidgetFilterChange(e) {
501
+ this._widgetFilterParams = { ...(e.detail?.params ?? {}) };
502
+ }
503
+ /** Column keys in display order. From widgetData when present, else derived from data/meta. */
504
+ _getColumns() {
505
+ const wd = this._widgetData;
506
+ if (wd?.columns?.length)
507
+ return wd.columns;
508
+ const rows = this.data ?? [];
509
+ const metaKeys = Object.keys(this.meta?.columns ?? {});
510
+ if (rows.length > 0)
511
+ return Object.keys(rows[0]);
512
+ return metaKeys.length > 0 ? metaKeys : [];
513
+ }
514
+ /** Body rows. From widgetData.rows when present, else from data. */
515
+ _getRows() {
516
+ const wd = this._widgetData;
517
+ if (wd?.rows?.length !== undefined && wd.rows.length >= 0)
518
+ return wd.rows;
519
+ return this.data ?? [];
520
+ }
521
+ /** Header label for column key. From widgetData.columnDefs or header_rows when present. */
522
+ _getColumnLabel(colKey) {
523
+ const wd = this._widgetData;
524
+ if (wd?.columnDefs?.length) {
525
+ const def = wd.columnDefs.find((c) => c.key === colKey);
526
+ if (def)
527
+ return def.label;
528
+ }
529
+ return colKey;
530
+ }
531
+ /** Text-align for column. From widgetData.columnDefs when present. */
532
+ _getColumnAlign(colKey) {
533
+ const wd = this._widgetData;
534
+ if (wd?.columnDefs?.length) {
535
+ const def = wd.columnDefs.find((c) => c.key === colKey);
536
+ if (def?.align)
537
+ return def.align;
538
+ }
539
+ return null;
540
+ }
541
+ /** Rowspan for this cell (column, rowIndex). 0 = merged (skip rendering cell). */
542
+ _getCellRowSpan(colKey, rowIndex) {
543
+ const wd = this._widgetData;
544
+ const runs = wd?.rowSpanRuns?.[colKey];
545
+ if (!runs || rowIndex >= runs.length)
546
+ return 1;
547
+ return runs[rowIndex] ?? 1;
548
+ }
549
+ _isTotalsRow(row) {
550
+ return Boolean(row['_is_totals_row']);
551
+ }
552
+ render() {
553
+ const hasWidgetFilters = this._widgetFilters.length > 0 && !this._hideFilter;
554
+ const widgetActiveCount = Object.keys(this._widgetFilterParams).filter((k) => this._widgetFilterParams[k] !== '' && this._widgetFilterParams[k] != null).length;
555
+ const rows = this._getRows();
556
+ const columns = this._getColumns();
557
+ const wd = this._widgetData;
558
+ const hasHeaderRows = Boolean(wd?.header_rows?.length);
559
+ const tableFontSizeStyle = wd?.font_size ? `font-size: ${wd.font_size}` : nothing;
560
+ const filterToolbar = hasWidgetFilters
561
+ ? html `
562
+ <analytics-widget-toolbar
563
+ .filters=${this._widgetFilters}
564
+ .values=${this._widgetFilterParams}
565
+ .activeCount=${widgetActiveCount}
566
+ @analytics-filter-change=${this._onWidgetFilterChange}
567
+ ></analytics-widget-toolbar>
568
+ `
569
+ : html ``;
570
+ const bodyContent = this._error
571
+ ? html `<div class="error">${this._error}</div>`
572
+ : columns.length === 0
573
+ ? html `<div class="empty">No columns</div>`
574
+ : rows.length === 0
575
+ ? html `
576
+ <table style=${tableFontSizeStyle}>
577
+ <thead>
578
+ ${this._renderThead(columns, hasHeaderRows)}
579
+ </thead>
580
+ <tbody>
581
+ <tr>
582
+ <td colspan=${columns.length} class="empty-placeholder">No data</td>
583
+ </tr>
584
+ </tbody>
585
+ </table>
586
+ `
587
+ : html `
588
+ <table style=${tableFontSizeStyle}>
589
+ <thead>
590
+ ${this._renderThead(columns, hasHeaderRows)}
591
+ </thead>
592
+ <tbody>
593
+ ${rows.map((row, rowIndex) => this._renderBodyRow(row, rowIndex, columns))}
594
+ </tbody>
595
+ </table>
596
+ `;
597
+ return html `
598
+ <div class="widget-with-toolbar">
599
+ ${this._effectiveTitle
600
+ ? html `<header class="widget-header"><h2 class="widget-title">${this._effectiveTitle}</h2></header>`
601
+ : ''}
602
+ <div class="widget-body">
603
+ ${bodyContent}
604
+ ${this._loading
605
+ ? html `<div class="loading-overlay" aria-busy="true" aria-live="polite">
606
+ <div class="loading-spinner" aria-hidden="true"></div>
607
+ </div>`
608
+ : nothing}
609
+ </div>
610
+ ${filterToolbar}
611
+ </div>
612
+ `;
613
+ }
614
+ _renderThead(columns, useHeaderRows) {
615
+ const wd = this._widgetData;
616
+ if (useHeaderRows && wd?.header_rows?.length) {
617
+ return html `${wd.header_rows.map((headerRow) => html `
618
+ <tr>
619
+ ${headerRow.map((cell) => {
620
+ if (cell.hide)
621
+ return null;
622
+ return html `<th
623
+ colspan=${cell.colspan != null && cell.colspan > 1 ? cell.colspan : nothing}
624
+ rowspan=${cell.rowspan != null && cell.rowspan > 1 ? cell.rowspan : nothing}
625
+ >${cell.label}</th>`;
626
+ })}
627
+ </tr>
628
+ `)}`;
629
+ }
630
+ return html `
631
+ <tr>
632
+ ${columns.map((col) => {
633
+ const align = this._getColumnAlign(col);
634
+ const style = align ? `text-align: ${align}` : undefined;
635
+ return html `<th style=${style ?? nothing}>${this._getColumnLabel(col)}</th>`;
636
+ })}
637
+ </tr>
638
+ `;
639
+ }
640
+ _renderBodyRow(row, rowIndex, columns) {
641
+ const isTotals = this._isTotalsRow(row);
642
+ const wd = this._widgetData;
643
+ const sticky = Boolean(isTotals && wd?.totals_sticky);
644
+ const stickyClass = sticky
645
+ ? wd?.totals_position === 'top'
646
+ ? 'totals-row-sticky totals-row-sticky-top'
647
+ : 'totals-row-sticky totals-row-sticky-bottom'
648
+ : '';
649
+ const totalsLabel = isTotals && row['_totals_label'] != null ? String(row['_totals_label']) : null;
650
+ return html `
651
+ <tr class=${isTotals ? `totals-row ${stickyClass}` : ''}>
652
+ ${columns.map((col, colIndex) => {
653
+ const rowSpan = this._getCellRowSpan(col, rowIndex);
654
+ if (rowSpan === 0)
655
+ return null;
656
+ const displayValue = colIndex === 0 && totalsLabel != null ? totalsLabel : row[col];
657
+ const align = this._getColumnAlign(col);
658
+ const style = align ? `text-align: ${align}` : undefined;
659
+ const cellContent = this._formatCell(displayValue, col);
660
+ return html `<td
661
+ style=${style ?? nothing}
662
+ rowspan=${rowSpan > 1 ? rowSpan : nothing}
663
+ class=${rowSpan === 0 ? 'rowspan-placeholder' : ''}
664
+ >${cellContent}</td>`;
665
+ })}
666
+ </tr>
667
+ `;
668
+ }
669
+ _formatCell(value, _col) {
670
+ if (value == null)
671
+ return '';
672
+ if (typeof value === 'object')
673
+ return JSON.stringify(value);
674
+ return value;
675
+ }
676
+ };
677
+ __decorate([
678
+ property({ type: String })
679
+ ], TableWidget.prototype, "title", void 0);
680
+ __decorate([
681
+ property({ type: String, attribute: 'data-url' })
682
+ ], TableWidget.prototype, "dataUrl", void 0);
683
+ __decorate([
684
+ property({ type: String, attribute: 'widget-id' })
685
+ ], TableWidget.prototype, "widgetId", void 0);
686
+ __decorate([
687
+ property({ type: String, attribute: 'api-url' })
688
+ ], TableWidget.prototype, "apiUrl", void 0);
689
+ __decorate([
690
+ property({ type: Object })
691
+ ], TableWidget.prototype, "dataParams", void 0);
692
+ __decorate([
693
+ property({ type: Object })
694
+ ], TableWidget.prototype, "dashboard", void 0);
695
+ __decorate([
696
+ property({ type: Object })
697
+ ], TableWidget.prototype, "data", void 0);
698
+ __decorate([
699
+ property({ type: Object })
700
+ ], TableWidget.prototype, "meta", void 0);
701
+ __decorate([
702
+ state()
703
+ ], TableWidget.prototype, "_widgetData", void 0);
704
+ __decorate([
705
+ property({ type: Boolean })
706
+ ], TableWidget.prototype, "lazy", void 0);
707
+ __decorate([
708
+ property({ type: String, attribute: 'prefetch-margin' })
709
+ ], TableWidget.prototype, "prefetchMargin", void 0);
710
+ __decorate([
711
+ property({ type: Object })
712
+ ], TableWidget.prototype, "initialDefinition", void 0);
713
+ __decorate([
714
+ state()
715
+ ], TableWidget.prototype, "_loading", void 0);
716
+ __decorate([
717
+ state()
718
+ ], TableWidget.prototype, "_error", void 0);
719
+ __decorate([
720
+ state()
721
+ ], TableWidget.prototype, "_dashboardParams", void 0);
722
+ __decorate([
723
+ state()
724
+ ], TableWidget.prototype, "_widgetFilters", void 0);
725
+ __decorate([
726
+ state()
727
+ ], TableWidget.prototype, "_widgetFilterParams", void 0);
728
+ __decorate([
729
+ state()
730
+ ], TableWidget.prototype, "_hideFilter", void 0);
731
+ __decorate([
732
+ state()
733
+ ], TableWidget.prototype, "_viewportVisible", void 0);
734
+ __decorate([
735
+ state()
736
+ ], TableWidget.prototype, "_definitionTitle", void 0);
737
+ TableWidget = TableWidget_1 = __decorate([
738
+ customElement('analytics-table')
739
+ ], TableWidget);
740
+ export { TableWidget };