@duckcodeailabs/dql-runtime 0.1.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 (50) hide show
  1. package/LICENSE +123 -0
  2. package/dist/breadcrumb.d.ts +2 -0
  3. package/dist/breadcrumb.d.ts.map +1 -0
  4. package/dist/breadcrumb.js +17 -0
  5. package/dist/breadcrumb.js.map +1 -0
  6. package/dist/dashboard-layout.d.ts +6 -0
  7. package/dist/dashboard-layout.d.ts.map +1 -0
  8. package/dist/dashboard-layout.js +14 -0
  9. package/dist/dashboard-layout.js.map +1 -0
  10. package/dist/data-fetcher.d.ts +16 -0
  11. package/dist/data-fetcher.d.ts.map +1 -0
  12. package/dist/data-fetcher.js +18 -0
  13. package/dist/data-fetcher.js.map +1 -0
  14. package/dist/dql-runtime.browser.js +571 -0
  15. package/dist/dql-runtime.browser.min.js +1 -0
  16. package/dist/dql-runtime.esm.js +549 -0
  17. package/dist/dql-runtime.esm.js.map +7 -0
  18. package/dist/filter-manager.d.ts +25 -0
  19. package/dist/filter-manager.d.ts.map +1 -0
  20. package/dist/filter-manager.js +140 -0
  21. package/dist/filter-manager.js.map +1 -0
  22. package/dist/hot-reload-client.d.ts +2 -0
  23. package/dist/hot-reload-client.d.ts.map +1 -0
  24. package/dist/hot-reload-client.js +27 -0
  25. package/dist/hot-reload-client.js.map +1 -0
  26. package/dist/index.d.ts +13 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +13 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/interactions.d.ts +49 -0
  31. package/dist/interactions.d.ts.map +1 -0
  32. package/dist/interactions.js +251 -0
  33. package/dist/interactions.js.map +1 -0
  34. package/dist/kpi-renderer.d.ts +4 -0
  35. package/dist/kpi-renderer.d.ts.map +1 -0
  36. package/dist/kpi-renderer.js +17 -0
  37. package/dist/kpi-renderer.js.map +1 -0
  38. package/dist/table-renderer.d.ts +4 -0
  39. package/dist/table-renderer.d.ts.map +1 -0
  40. package/dist/table-renderer.js +39 -0
  41. package/dist/table-renderer.js.map +1 -0
  42. package/dist/utils.d.ts +5 -0
  43. package/dist/utils.d.ts.map +1 -0
  44. package/dist/utils.js +29 -0
  45. package/dist/utils.js.map +1 -0
  46. package/dist/vega-renderer.d.ts +4 -0
  47. package/dist/vega-renderer.d.ts.map +1 -0
  48. package/dist/vega-renderer.js +15 -0
  49. package/dist/vega-renderer.js.map +1 -0
  50. package/package.json +45 -0
@@ -0,0 +1,571 @@
1
+ "use strict";
2
+ var DQLRuntime = (() => {
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
22
+
23
+ // src/index.ts
24
+ var index_exports = {};
25
+ __export(index_exports, {
26
+ DashboardLayout: () => DashboardLayout,
27
+ DataFetcher: () => DataFetcher,
28
+ FilterManager: () => FilterManager,
29
+ InteractionManager: () => InteractionManager,
30
+ KPIRenderer: () => KPIRenderer,
31
+ TableRenderer: () => TableRenderer,
32
+ VegaRenderer: () => VegaRenderer,
33
+ cleanDatum: () => cleanDatum,
34
+ escapeHTML: () => escapeHTML,
35
+ getClickedLabel: () => getClickedLabel,
36
+ initBreadcrumb: () => initBreadcrumb,
37
+ initHotReload: () => initHotReload,
38
+ interpolateDatum: () => interpolateDatum
39
+ });
40
+
41
+ // src/data-fetcher.ts
42
+ var DataFetcher = class {
43
+ constructor(apiEndpoint = "/api/query") {
44
+ __publicField(this, "apiEndpoint");
45
+ this.apiEndpoint = apiEndpoint;
46
+ }
47
+ async fetch(_chartId, sql, sqlParams, variables, connectionId) {
48
+ const response = await globalThis.fetch(this.apiEndpoint, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify({ sql, sqlParams, variables, connectionId })
52
+ });
53
+ if (!response.ok) {
54
+ throw new Error(`Query failed: ${response.statusText}`);
55
+ }
56
+ return response.json();
57
+ }
58
+ };
59
+
60
+ // src/vega-renderer.ts
61
+ var VegaRenderer = class {
62
+ async render(containerId, spec, data) {
63
+ const fullSpec = { ...spec, data: { values: data } };
64
+ const vegaEmbed = globalThis.vegaEmbed;
65
+ if (!vegaEmbed) {
66
+ throw new Error("vega-embed not loaded. Include the Vega-Lite CDN scripts.");
67
+ }
68
+ await vegaEmbed(`#${containerId}`, fullSpec, {
69
+ actions: false,
70
+ renderer: "svg"
71
+ });
72
+ }
73
+ };
74
+
75
+ // src/dashboard-layout.ts
76
+ var DashboardLayout = class {
77
+ constructor() {
78
+ __publicField(this, "container", null);
79
+ }
80
+ mount(elementId) {
81
+ this.container = document.getElementById(elementId);
82
+ }
83
+ setGrid(columns, gap = "16px") {
84
+ if (!this.container) return;
85
+ this.container.style.display = "grid";
86
+ this.container.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
87
+ this.container.style.gap = gap;
88
+ }
89
+ };
90
+
91
+ // src/kpi-renderer.ts
92
+ var KPIRenderer = class {
93
+ render(containerId, data, metrics) {
94
+ const container = document.getElementById(containerId);
95
+ if (!container || data.length === 0) return;
96
+ const row = data[0];
97
+ const cards = metrics.map((metric) => {
98
+ const value = row[metric];
99
+ const label = metric.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
100
+ return `<div class="dql-kpi-card"><div class="dql-kpi-label">${label}</div><div class="dql-kpi-value">${value ?? "-"}</div></div>`;
101
+ }).join("");
102
+ container.innerHTML = `<div class="dql-kpi-grid">${cards}</div>`;
103
+ }
104
+ };
105
+
106
+ // src/utils.ts
107
+ function escapeHTML(str) {
108
+ if (str === null || str === void 0) return "";
109
+ return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
110
+ }
111
+ function interpolateDatum(template, datum) {
112
+ return template.replace(/\{(\w+)\}/g, (_match, key) => {
113
+ const val = datum[key];
114
+ return val === null || val === void 0 ? "" : String(val);
115
+ });
116
+ }
117
+ function cleanDatum(datum) {
118
+ const clean = {};
119
+ for (const [k, v] of Object.entries(datum)) {
120
+ if (k !== "_vgsid_" && !k.startsWith("__")) {
121
+ clean[k] = v;
122
+ }
123
+ }
124
+ return clean;
125
+ }
126
+ function getClickedLabel(datum) {
127
+ const keys = Object.keys(datum).filter(
128
+ (k) => k !== "_vgsid_" && !k.startsWith("__") && typeof datum[k] === "string"
129
+ );
130
+ return keys.length > 0 ? String(datum[keys[0]]) : null;
131
+ }
132
+
133
+ // src/table-renderer.ts
134
+ var TableRenderer = class {
135
+ render(containerId, data, pageSize = 25) {
136
+ const container = document.getElementById(containerId);
137
+ if (!container || !data || data.length === 0) {
138
+ if (container) container.innerHTML = '<div class="dql-loading">No data</div>';
139
+ return;
140
+ }
141
+ const colNames = Object.keys(data[0]);
142
+ let html = '<table class="dql-drill-table" style="width:100%"><thead><tr>';
143
+ for (const col of colNames) {
144
+ const label = col.replace(/_/g, " ");
145
+ html += `<th style="cursor:pointer;" data-col="${col}">${escapeHTML(label)}</th>`;
146
+ }
147
+ html += "</tr></thead><tbody>";
148
+ const maxRows = Math.min(data.length, pageSize);
149
+ for (let r = 0; r < maxRows; r++) {
150
+ html += "<tr>";
151
+ for (const col of colNames) {
152
+ let val = data[r][col];
153
+ if (val === null || val === void 0) val = "";
154
+ else if (typeof val === "number") val = val.toLocaleString();
155
+ else val = String(val);
156
+ html += `<td>${escapeHTML(String(val))}</td>`;
157
+ }
158
+ html += "</tr>";
159
+ }
160
+ html += "</tbody></table>";
161
+ if (data.length > pageSize) {
162
+ html += `<p style="margin-top:12px;font-size:13px;opacity:0.7;">Showing ${maxRows} of ${data.length} rows</p>`;
163
+ }
164
+ container.innerHTML = html;
165
+ }
166
+ };
167
+
168
+ // src/interactions.ts
169
+ var InteractionManager = class {
170
+ constructor(fetcher, options = {}) {
171
+ __publicField(this, "vegaViews", {});
172
+ __publicField(this, "fetcher");
173
+ __publicField(this, "crossFilterState", {});
174
+ __publicField(this, "drillStack", []);
175
+ __publicField(this, "onStateChange");
176
+ this.fetcher = fetcher;
177
+ this.onStateChange = options.onStateChange;
178
+ }
179
+ registerView(chartId, view) {
180
+ this.vegaViews[chartId] = view;
181
+ }
182
+ getView(chartId) {
183
+ return this.vegaViews[chartId];
184
+ }
185
+ getState() {
186
+ return {
187
+ crossFilters: this.getCrossFilters(),
188
+ drillStack: this.getDrillStack()
189
+ };
190
+ }
191
+ getCrossFilters() {
192
+ return JSON.parse(JSON.stringify(this.crossFilterState));
193
+ }
194
+ getDrillStack() {
195
+ return JSON.parse(JSON.stringify(this.drillStack));
196
+ }
197
+ setupChartInteractions(chartId, view, interaction, _data) {
198
+ view.addEventListener("click", (_event, item) => {
199
+ if (!item || !item.datum) return;
200
+ const datum = item.datum;
201
+ if (interaction.drillDown) {
202
+ this.handleDrillDown(chartId, interaction.drillDown, datum);
203
+ }
204
+ if (interaction.linkTo) {
205
+ this.handleLinkTo(interaction.linkTo, datum);
206
+ }
207
+ if (interaction.onClick) {
208
+ this.handleOnClick(interaction.onClick, datum, chartId);
209
+ }
210
+ });
211
+ if (interaction.filterBy) {
212
+ this.setupFilterByInteraction(chartId, view, interaction.filterBy);
213
+ }
214
+ }
215
+ async drillUp() {
216
+ if (this.drillStack.length === 0) return;
217
+ this.drillStack.pop();
218
+ this.emitStateChange();
219
+ const modal = document.getElementById("dql-drill-modal");
220
+ const bodyEl = document.getElementById("dql-drill-body");
221
+ if (!modal || !bodyEl) return;
222
+ if (this.drillStack.length === 0) {
223
+ modal.classList.remove("active");
224
+ return;
225
+ }
226
+ const top = this.drillStack[this.drillStack.length - 1];
227
+ bodyEl.innerHTML = '<div class="dql-loading">Loading details...</div>';
228
+ const result = await this.fetchDrillDown(top);
229
+ if (result.error) {
230
+ bodyEl.innerHTML = `<div class="dql-error">${escapeHTML(result.error)}</div>`;
231
+ return;
232
+ }
233
+ if (!result.rows || result.rows.length === 0) {
234
+ bodyEl.innerHTML = '<div class="dql-loading">No data found</div>';
235
+ return;
236
+ }
237
+ bodyEl.innerHTML = this.renderDrillTable(result.columns, result.rows);
238
+ }
239
+ async handleDrillDown(chartId, drillDownSQL, datum) {
240
+ const clean = cleanDatum(datum);
241
+ const frame = {
242
+ chartId,
243
+ label: String(getClickedLabel(clean) || chartId),
244
+ templateSQL: drillDownSQL,
245
+ datum: clean,
246
+ predicates: this.buildPredicates(clean),
247
+ connectionId: this.resolveChartConnection(chartId)
248
+ };
249
+ this.drillStack.push(frame);
250
+ this.emitStateChange();
251
+ const modal = document.getElementById("dql-drill-modal");
252
+ const titleEl = document.getElementById("dql-drill-title");
253
+ const stackEl = document.getElementById("dql-drill-stack");
254
+ const bodyEl = document.getElementById("dql-drill-body");
255
+ if (!modal || !bodyEl) return;
256
+ if (titleEl) titleEl.textContent = "Detail View - " + frame.label;
257
+ if (stackEl) {
258
+ stackEl.textContent = this.drillStack.map((entry) => entry.label).join(" > ");
259
+ }
260
+ bodyEl.innerHTML = '<div class="dql-loading">Loading details...</div>';
261
+ modal.classList.add("active");
262
+ const result = await this.fetchDrillDown(frame);
263
+ if (result.error) {
264
+ bodyEl.innerHTML = `<div class="dql-error">${escapeHTML(result.error)}</div>`;
265
+ return;
266
+ }
267
+ if (!result.rows || result.rows.length === 0) {
268
+ bodyEl.innerHTML = '<div class="dql-loading">No data found</div>';
269
+ return;
270
+ }
271
+ bodyEl.innerHTML = this.renderDrillTable(result.columns, result.rows);
272
+ }
273
+ async fetchDrillDown(frame) {
274
+ try {
275
+ const apiEndpoint = globalThis.DQL_CONFIG?.apiEndpoint ?? "/api/query";
276
+ const response = await globalThis.fetch(apiEndpoint.replace("/query", "/drill"), {
277
+ method: "POST",
278
+ headers: { "Content-Type": "application/json" },
279
+ body: JSON.stringify({
280
+ chartId: frame.chartId,
281
+ templateSQL: frame.templateSQL,
282
+ datum: frame.datum,
283
+ connectionId: frame.connectionId
284
+ })
285
+ });
286
+ if (!response.ok) throw new Error("Drill-down query failed: " + response.statusText);
287
+ return response.json();
288
+ } catch (error) {
289
+ return { columns: [], rows: [], error: error.message };
290
+ }
291
+ }
292
+ renderDrillTable(columns, rows) {
293
+ const colNames = columns ? columns.map((c) => c.name) : Object.keys(rows[0] || {});
294
+ let html = '<table class="dql-drill-table"><thead><tr>';
295
+ for (const col of colNames) {
296
+ html += `<th>${escapeHTML(col)}</th>`;
297
+ }
298
+ html += "</tr></thead><tbody>";
299
+ const maxRows = Math.min(rows.length, 100);
300
+ for (let r = 0; r < maxRows; r++) {
301
+ html += "<tr>";
302
+ for (const col of colNames) {
303
+ const val = rows[r][col];
304
+ html += `<td>${escapeHTML(val === null || val === void 0 ? "" : String(val))}</td>`;
305
+ }
306
+ html += "</tr>";
307
+ }
308
+ html += "</tbody></table>";
309
+ if (rows.length > 100) {
310
+ html += `<p style="margin-top:12px;color:#888;">Showing first 100 of ${rows.length} rows</p>`;
311
+ }
312
+ return html;
313
+ }
314
+ handleLinkTo(linkTo, datum) {
315
+ let url = linkTo.replace(/\{(\w+)\}/g, (_m, key) => {
316
+ const val = datum[key];
317
+ return val === null || val === void 0 ? "" : String(val);
318
+ });
319
+ const config = globalThis.DQL_CONFIG;
320
+ const sep = url.indexOf("?") >= 0 ? "&" : "?";
321
+ url += sep + "_from=" + encodeURIComponent(location.pathname) + "&_fromTitle=" + encodeURIComponent(config?.title ?? "");
322
+ window.location.href = url;
323
+ }
324
+ handleOnClick(action, datum, chartId) {
325
+ const event = new CustomEvent("dql:click", {
326
+ detail: { chartId, action, datum },
327
+ bubbles: true
328
+ });
329
+ document.dispatchEvent(event);
330
+ const config = globalThis.DQL_CONFIG;
331
+ if (action === "drill_down") {
332
+ const interaction = config?.interactions?.[chartId];
333
+ if (interaction?.drillDown) {
334
+ this.handleDrillDown(chartId, interaction.drillDown, datum);
335
+ }
336
+ } else if (action === "navigate" || action === "link") {
337
+ const interaction = config?.interactions?.[chartId];
338
+ if (interaction?.linkTo) {
339
+ this.handleLinkTo(interaction.linkTo, datum);
340
+ }
341
+ } else if (action.startsWith("set_param:") || action.startsWith("set_params:")) {
342
+ document.dispatchEvent(new CustomEvent("dql:param-action", {
343
+ detail: { chartId, action, datum },
344
+ bubbles: true
345
+ }));
346
+ }
347
+ }
348
+ setupFilterByInteraction(chartId, view, filterBy) {
349
+ const fields = Array.isArray(filterBy) ? filterBy : [filterBy];
350
+ view.addSignalListener("dql_filter", (_name, value) => {
351
+ const selectedValues = {};
352
+ if (value && value.vlPoint && value.vlPoint.or) {
353
+ for (const point of value.vlPoint.or) {
354
+ for (const field of fields) {
355
+ if (point[field] !== void 0 && point[field] !== null && point[field] !== "") {
356
+ if (!selectedValues[field]) selectedValues[field] = [];
357
+ if (!selectedValues[field].includes(point[field])) {
358
+ selectedValues[field].push(point[field]);
359
+ }
360
+ }
361
+ }
362
+ }
363
+ }
364
+ this.applyCrossChartFilter(chartId, selectedValues);
365
+ });
366
+ }
367
+ applyCrossChartFilter(sourceChartId, selectedValues) {
368
+ if (Object.keys(selectedValues).length === 0) {
369
+ delete this.crossFilterState[sourceChartId];
370
+ } else {
371
+ this.crossFilterState[sourceChartId] = selectedValues;
372
+ }
373
+ this.emitStateChange();
374
+ document.dispatchEvent(new CustomEvent("dql:cross-filter", {
375
+ detail: {
376
+ sourceChartId,
377
+ selectedValues,
378
+ state: this.getCrossFilters()
379
+ },
380
+ bubbles: true
381
+ }));
382
+ }
383
+ buildPredicates(datum) {
384
+ const predicates = {};
385
+ for (const [key, value] of Object.entries(datum)) {
386
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
387
+ if (value === null || value === void 0 || value === "") continue;
388
+ predicates[key] = [value];
389
+ }
390
+ return predicates;
391
+ }
392
+ resolveChartConnection(chartId) {
393
+ const config = globalThis.DQL_CONFIG;
394
+ const charts = Array.isArray(config?.charts) ? config.charts : [];
395
+ const chart = charts.find((entry) => entry.id === chartId);
396
+ return chart?.connection;
397
+ }
398
+ emitStateChange() {
399
+ if (this.onStateChange) {
400
+ this.onStateChange(this.getState());
401
+ }
402
+ }
403
+ };
404
+
405
+ // src/filter-manager.ts
406
+ var FilterManager = class {
407
+ constructor(fetcher, onApply) {
408
+ __publicField(this, "filterState", {});
409
+ __publicField(this, "fetcher");
410
+ __publicField(this, "onApply");
411
+ this.fetcher = fetcher;
412
+ this.onApply = onApply;
413
+ }
414
+ getState() {
415
+ return { ...this.filterState };
416
+ }
417
+ async init(filters) {
418
+ if (!filters || filters.length === 0) return;
419
+ for (const filter of filters) {
420
+ if (filter.sql) {
421
+ await this.loadFilterOptions(filter);
422
+ }
423
+ if (filter.defaultValue) {
424
+ this.filterState[filter.param] = filter.defaultValue;
425
+ this.setUIValue(filter.id, filter.defaultValue);
426
+ }
427
+ }
428
+ this.setupEventHandlers(filters);
429
+ }
430
+ async loadFilterOptions(filter) {
431
+ try {
432
+ const result = await this.fetcher.fetch(filter.id, filter.sql, []);
433
+ if (!result.rows || result.rows.length === 0) return;
434
+ const selectEl = document.getElementById(filter.id);
435
+ if (!selectEl || selectEl.tagName !== "SELECT") return;
436
+ const colName = result.columns && result.columns.length > 0 ? result.columns[0].name : Object.keys(result.rows[0] || {})[0];
437
+ if (!colName) return;
438
+ for (const row of result.rows) {
439
+ const val = row[colName];
440
+ if (val === null || val === void 0) continue;
441
+ const option = document.createElement("option");
442
+ option.value = String(val);
443
+ option.textContent = String(val);
444
+ selectEl.appendChild(option);
445
+ }
446
+ } catch (err) {
447
+ console.warn("[DQL] Failed to load filter options for " + filter.id, err);
448
+ }
449
+ }
450
+ setUIValue(filterId, value) {
451
+ const el = document.getElementById(filterId);
452
+ if (!el) return;
453
+ el.value = String(value);
454
+ }
455
+ setupEventHandlers(filters) {
456
+ const applyBtn = document.getElementById("dql-filter-apply");
457
+ const resetBtn = document.getElementById("dql-filter-reset");
458
+ if (applyBtn) {
459
+ applyBtn.addEventListener("click", () => {
460
+ this.collectState(filters);
461
+ this.onApply();
462
+ });
463
+ }
464
+ if (resetBtn) {
465
+ resetBtn.addEventListener("click", () => {
466
+ this.resetState(filters);
467
+ this.onApply();
468
+ });
469
+ }
470
+ const autoApplyEls = document.querySelectorAll(".dql-filter-select, .dql-filter-date");
471
+ autoApplyEls.forEach((el) => {
472
+ el.addEventListener("change", () => {
473
+ this.collectState(filters);
474
+ this.onApply();
475
+ });
476
+ });
477
+ const debounceTimers = {};
478
+ const textInputs = document.querySelectorAll('.dql-filter-input[type="text"]');
479
+ textInputs.forEach((el) => {
480
+ el.addEventListener("input", () => {
481
+ const id = el.id;
482
+ if (debounceTimers[id]) clearTimeout(debounceTimers[id]);
483
+ debounceTimers[id] = setTimeout(() => {
484
+ this.collectState(filters);
485
+ this.onApply();
486
+ }, 300);
487
+ });
488
+ });
489
+ }
490
+ collectState(filters) {
491
+ this.filterState = {};
492
+ for (const filter of filters) {
493
+ const el = document.getElementById(filter.id);
494
+ if (!el) continue;
495
+ const value = el.value;
496
+ if (value && value !== "") {
497
+ this.filterState[filter.param] = value;
498
+ }
499
+ }
500
+ }
501
+ resetState(filters) {
502
+ this.filterState = {};
503
+ for (const filter of filters) {
504
+ const el = document.getElementById(filter.id);
505
+ if (!el) continue;
506
+ if (filter.defaultValue) {
507
+ el.value = String(filter.defaultValue);
508
+ this.filterState[filter.param] = filter.defaultValue;
509
+ } else {
510
+ el.value = "";
511
+ }
512
+ }
513
+ }
514
+ applyToSQL(sql) {
515
+ const urlParams = new URLSearchParams(window.location.search);
516
+ const merged = { ...this.filterState };
517
+ urlParams.forEach((value, key) => {
518
+ if (key !== "_from" && key !== "_fromTitle" && !merged[key]) {
519
+ merged[key] = value;
520
+ }
521
+ });
522
+ let result = sql;
523
+ for (const [paramName, paramValue] of Object.entries(merged)) {
524
+ const placeholder = "{" + paramName + "}";
525
+ result = result.split(placeholder).join(String(paramValue).replace(/'/g, "''"));
526
+ }
527
+ return result;
528
+ }
529
+ };
530
+
531
+ // src/breadcrumb.ts
532
+ function initBreadcrumb() {
533
+ const breadcrumb = document.getElementById("dql-breadcrumb");
534
+ if (!breadcrumb) return;
535
+ const urlParams = new URLSearchParams(window.location.search);
536
+ const fromPath = urlParams.get("_from");
537
+ const fromTitle = urlParams.get("_fromTitle");
538
+ const config = globalThis.DQL_CONFIG;
539
+ if (fromPath && fromTitle) {
540
+ breadcrumb.innerHTML = `<a href="${escapeHTML(fromPath)}">${escapeHTML(fromTitle)}</a><span class="dql-breadcrumb-sep">/</span><span>${escapeHTML(config?.title ?? "")}</span>`;
541
+ }
542
+ }
543
+
544
+ // src/hot-reload-client.ts
545
+ function initHotReload(wsUrl) {
546
+ if (typeof WebSocket === "undefined") return;
547
+ const url = wsUrl ?? `${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}/__dql_hmr`;
548
+ let reconnectAttempts = 0;
549
+ function connect() {
550
+ const ws = new WebSocket(url);
551
+ ws.onopen = () => {
552
+ console.log("[DQL] Hot-reload connected");
553
+ reconnectAttempts = 0;
554
+ };
555
+ ws.onmessage = (event) => {
556
+ const msg = JSON.parse(event.data);
557
+ if (msg.type === "reload") {
558
+ location.reload();
559
+ }
560
+ };
561
+ ws.onclose = () => {
562
+ if (reconnectAttempts < 10) {
563
+ reconnectAttempts++;
564
+ setTimeout(connect, Math.min(1e3 * 2 ** reconnectAttempts, 1e4));
565
+ }
566
+ };
567
+ }
568
+ connect();
569
+ }
570
+ return __toCommonJS(index_exports);
571
+ })();
@@ -0,0 +1 @@
1
+ "use strict";var DQLRuntime=(()=>{var p=Object.defineProperty;var D=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var I=Object.prototype.hasOwnProperty;var R=(o,e,t)=>e in o?p(o,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):o[e]=t;var q=(o,e)=>{for(var t in e)p(o,t,{get:e[t],enumerable:!0})},F=(o,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of x(e))!I.call(o,r)&&r!==t&&p(o,r,{get:()=>e[r],enumerable:!(n=D(e,r))||n.enumerable});return o};var _=o=>F(p({},"__esModule",{value:!0}),o);var u=(o,e,t)=>R(o,typeof e!="symbol"?e+"":e,t);var O={};q(O,{DashboardLayout:()=>h,DataFetcher:()=>f,FilterManager:()=>b,InteractionManager:()=>S,KPIRenderer:()=>v,TableRenderer:()=>k,VegaRenderer:()=>m,cleanDatum:()=>y,escapeHTML:()=>g,getClickedLabel:()=>w,initBreadcrumb:()=>C,initHotReload:()=>E,interpolateDatum:()=>T});var f=class{constructor(e="/api/query"){u(this,"apiEndpoint");this.apiEndpoint=e}async fetch(e,t,n,r,s){let i=await globalThis.fetch(this.apiEndpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sql:t,sqlParams:n,variables:r,connectionId:s})});if(!i.ok)throw new Error(`Query failed: ${i.statusText}`);return i.json()}};var m=class{async render(e,t,n){let r={...t,data:{values:n}},s=globalThis.vegaEmbed;if(!s)throw new Error("vega-embed not loaded. Include the Vega-Lite CDN scripts.");await s(`#${e}`,r,{actions:!1,renderer:"svg"})}};var h=class{constructor(){u(this,"container",null)}mount(e){this.container=document.getElementById(e)}setGrid(e,t="16px"){this.container&&(this.container.style.display="grid",this.container.style.gridTemplateColumns=`repeat(${e}, 1fr)`,this.container.style.gap=t)}};var v=class{render(e,t,n){let r=document.getElementById(e);if(!r||t.length===0)return;let s=t[0],i=n.map(l=>{let a=s[l];return`<div class="dql-kpi-card"><div class="dql-kpi-label">${l.replace(/_/g," ").replace(/\b\w/g,d=>d.toUpperCase())}</div><div class="dql-kpi-value">${a??"-"}</div></div>`}).join("");r.innerHTML=`<div class="dql-kpi-grid">${i}</div>`}};function g(o){return o==null?"":String(o).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function T(o,e){return o.replace(/\{(\w+)\}/g,(t,n)=>{let r=e[n];return r==null?"":String(r)})}function y(o){let e={};for(let[t,n]of Object.entries(o))t!=="_vgsid_"&&!t.startsWith("__")&&(e[t]=n);return e}function w(o){let e=Object.keys(o).filter(t=>t!=="_vgsid_"&&!t.startsWith("__")&&typeof o[t]=="string");return e.length>0?String(o[e[0]]):null}var k=class{render(e,t,n=25){let r=document.getElementById(e);if(!r||!t||t.length===0){r&&(r.innerHTML='<div class="dql-loading">No data</div>');return}let s=Object.keys(t[0]),i='<table class="dql-drill-table" style="width:100%"><thead><tr>';for(let a of s){let c=a.replace(/_/g," ");i+=`<th style="cursor:pointer;" data-col="${a}">${g(c)}</th>`}i+="</tr></thead><tbody>";let l=Math.min(t.length,n);for(let a=0;a<l;a++){i+="<tr>";for(let c of s){let d=t[a][c];d==null?d="":typeof d=="number"?d=d.toLocaleString():d=String(d),i+=`<td>${g(String(d))}</td>`}i+="</tr>"}i+="</tbody></table>",t.length>n&&(i+=`<p style="margin-top:12px;font-size:13px;opacity:0.7;">Showing ${l} of ${t.length} rows</p>`),r.innerHTML=i}};var S=class{constructor(e,t={}){u(this,"vegaViews",{});u(this,"fetcher");u(this,"crossFilterState",{});u(this,"drillStack",[]);u(this,"onStateChange");this.fetcher=e,this.onStateChange=t.onStateChange}registerView(e,t){this.vegaViews[e]=t}getView(e){return this.vegaViews[e]}getState(){return{crossFilters:this.getCrossFilters(),drillStack:this.getDrillStack()}}getCrossFilters(){return JSON.parse(JSON.stringify(this.crossFilterState))}getDrillStack(){return JSON.parse(JSON.stringify(this.drillStack))}setupChartInteractions(e,t,n,r){t.addEventListener("click",(s,i)=>{if(!i||!i.datum)return;let l=i.datum;n.drillDown&&this.handleDrillDown(e,n.drillDown,l),n.linkTo&&this.handleLinkTo(n.linkTo,l),n.onClick&&this.handleOnClick(n.onClick,l,e)}),n.filterBy&&this.setupFilterByInteraction(e,t,n.filterBy)}async drillUp(){if(this.drillStack.length===0)return;this.drillStack.pop(),this.emitStateChange();let e=document.getElementById("dql-drill-modal"),t=document.getElementById("dql-drill-body");if(!e||!t)return;if(this.drillStack.length===0){e.classList.remove("active");return}let n=this.drillStack[this.drillStack.length-1];t.innerHTML='<div class="dql-loading">Loading details...</div>';let r=await this.fetchDrillDown(n);if(r.error){t.innerHTML=`<div class="dql-error">${g(r.error)}</div>`;return}if(!r.rows||r.rows.length===0){t.innerHTML='<div class="dql-loading">No data found</div>';return}t.innerHTML=this.renderDrillTable(r.columns,r.rows)}async handleDrillDown(e,t,n){let r=y(n),s={chartId:e,label:String(w(r)||e),templateSQL:t,datum:r,predicates:this.buildPredicates(r),connectionId:this.resolveChartConnection(e)};this.drillStack.push(s),this.emitStateChange();let i=document.getElementById("dql-drill-modal"),l=document.getElementById("dql-drill-title"),a=document.getElementById("dql-drill-stack"),c=document.getElementById("dql-drill-body");if(!i||!c)return;l&&(l.textContent="Detail View - "+s.label),a&&(a.textContent=this.drillStack.map(L=>L.label).join(" > ")),c.innerHTML='<div class="dql-loading">Loading details...</div>',i.classList.add("active");let d=await this.fetchDrillDown(s);if(d.error){c.innerHTML=`<div class="dql-error">${g(d.error)}</div>`;return}if(!d.rows||d.rows.length===0){c.innerHTML='<div class="dql-loading">No data found</div>';return}c.innerHTML=this.renderDrillTable(d.columns,d.rows)}async fetchDrillDown(e){try{let t=globalThis.DQL_CONFIG?.apiEndpoint??"/api/query",n=await globalThis.fetch(t.replace("/query","/drill"),{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({chartId:e.chartId,templateSQL:e.templateSQL,datum:e.datum,connectionId:e.connectionId})});if(!n.ok)throw new Error("Drill-down query failed: "+n.statusText);return n.json()}catch(t){return{columns:[],rows:[],error:t.message}}}renderDrillTable(e,t){let n=e?e.map(i=>i.name):Object.keys(t[0]||{}),r='<table class="dql-drill-table"><thead><tr>';for(let i of n)r+=`<th>${g(i)}</th>`;r+="</tr></thead><tbody>";let s=Math.min(t.length,100);for(let i=0;i<s;i++){r+="<tr>";for(let l of n){let a=t[i][l];r+=`<td>${g(a==null?"":String(a))}</td>`}r+="</tr>"}return r+="</tbody></table>",t.length>100&&(r+=`<p style="margin-top:12px;color:#888;">Showing first 100 of ${t.length} rows</p>`),r}handleLinkTo(e,t){let n=e.replace(/\{(\w+)\}/g,(i,l)=>{let a=t[l];return a==null?"":String(a)}),r=globalThis.DQL_CONFIG,s=n.indexOf("?")>=0?"&":"?";n+=s+"_from="+encodeURIComponent(location.pathname)+"&_fromTitle="+encodeURIComponent(r?.title??""),window.location.href=n}handleOnClick(e,t,n){let r=new CustomEvent("dql:click",{detail:{chartId:n,action:e,datum:t},bubbles:!0});document.dispatchEvent(r);let s=globalThis.DQL_CONFIG;if(e==="drill_down"){let i=s?.interactions?.[n];i?.drillDown&&this.handleDrillDown(n,i.drillDown,t)}else if(e==="navigate"||e==="link"){let i=s?.interactions?.[n];i?.linkTo&&this.handleLinkTo(i.linkTo,t)}else(e.startsWith("set_param:")||e.startsWith("set_params:"))&&document.dispatchEvent(new CustomEvent("dql:param-action",{detail:{chartId:n,action:e,datum:t},bubbles:!0}))}setupFilterByInteraction(e,t,n){let r=Array.isArray(n)?n:[n];t.addSignalListener("dql_filter",(s,i)=>{let l={};if(i&&i.vlPoint&&i.vlPoint.or)for(let a of i.vlPoint.or)for(let c of r)a[c]!==void 0&&a[c]!==null&&a[c]!==""&&(l[c]||(l[c]=[]),l[c].includes(a[c])||l[c].push(a[c]));this.applyCrossChartFilter(e,l)})}applyCrossChartFilter(e,t){Object.keys(t).length===0?delete this.crossFilterState[e]:this.crossFilterState[e]=t,this.emitStateChange(),document.dispatchEvent(new CustomEvent("dql:cross-filter",{detail:{sourceChartId:e,selectedValues:t,state:this.getCrossFilters()},bubbles:!0}))}buildPredicates(e){let t={};for(let[n,r]of Object.entries(e))/^[A-Za-z_][A-Za-z0-9_]*$/.test(n)&&(r==null||r===""||(t[n]=[r]));return t}resolveChartConnection(e){let t=globalThis.DQL_CONFIG;return(Array.isArray(t?.charts)?t.charts:[]).find(s=>s.id===e)?.connection}emitStateChange(){this.onStateChange&&this.onStateChange(this.getState())}};var b=class{constructor(e,t){u(this,"filterState",{});u(this,"fetcher");u(this,"onApply");this.fetcher=e,this.onApply=t}getState(){return{...this.filterState}}async init(e){if(!(!e||e.length===0)){for(let t of e)t.sql&&await this.loadFilterOptions(t),t.defaultValue&&(this.filterState[t.param]=t.defaultValue,this.setUIValue(t.id,t.defaultValue));this.setupEventHandlers(e)}}async loadFilterOptions(e){try{let t=await this.fetcher.fetch(e.id,e.sql,[]);if(!t.rows||t.rows.length===0)return;let n=document.getElementById(e.id);if(!n||n.tagName!=="SELECT")return;let r=t.columns&&t.columns.length>0?t.columns[0].name:Object.keys(t.rows[0]||{})[0];if(!r)return;for(let s of t.rows){let i=s[r];if(i==null)continue;let l=document.createElement("option");l.value=String(i),l.textContent=String(i),n.appendChild(l)}}catch(t){console.warn("[DQL] Failed to load filter options for "+e.id,t)}}setUIValue(e,t){let n=document.getElementById(e);n&&(n.value=String(t))}setupEventHandlers(e){let t=document.getElementById("dql-filter-apply"),n=document.getElementById("dql-filter-reset");t&&t.addEventListener("click",()=>{this.collectState(e),this.onApply()}),n&&n.addEventListener("click",()=>{this.resetState(e),this.onApply()}),document.querySelectorAll(".dql-filter-select, .dql-filter-date").forEach(l=>{l.addEventListener("change",()=>{this.collectState(e),this.onApply()})});let s={};document.querySelectorAll('.dql-filter-input[type="text"]').forEach(l=>{l.addEventListener("input",()=>{let a=l.id;s[a]&&clearTimeout(s[a]),s[a]=setTimeout(()=>{this.collectState(e),this.onApply()},300)})})}collectState(e){this.filterState={};for(let t of e){let n=document.getElementById(t.id);if(!n)continue;let r=n.value;r&&r!==""&&(this.filterState[t.param]=r)}}resetState(e){this.filterState={};for(let t of e){let n=document.getElementById(t.id);n&&(t.defaultValue?(n.value=String(t.defaultValue),this.filterState[t.param]=t.defaultValue):n.value="")}}applyToSQL(e){let t=new URLSearchParams(window.location.search),n={...this.filterState};t.forEach((s,i)=>{i!=="_from"&&i!=="_fromTitle"&&!n[i]&&(n[i]=s)});let r=e;for(let[s,i]of Object.entries(n)){let l="{"+s+"}";r=r.split(l).join(String(i).replace(/'/g,"''"))}return r}};function C(){let o=document.getElementById("dql-breadcrumb");if(!o)return;let e=new URLSearchParams(window.location.search),t=e.get("_from"),n=e.get("_fromTitle"),r=globalThis.DQL_CONFIG;t&&n&&(o.innerHTML=`<a href="${g(t)}">${g(n)}</a><span class="dql-breadcrumb-sep">/</span><span>${g(r?.title??"")}</span>`)}function E(o){if(typeof WebSocket>"u")return;let e=o??`${location.protocol==="https:"?"wss:":"ws:"}//${location.host}/__dql_hmr`,t=0;function n(){let r=new WebSocket(e);r.onopen=()=>{console.log("[DQL] Hot-reload connected"),t=0},r.onmessage=s=>{JSON.parse(s.data).type==="reload"&&location.reload()},r.onclose=()=>{t<10&&(t++,setTimeout(n,Math.min(1e3*2**t,1e4)))}}n()}return _(O);})();