@pythia-software/query-table-ui 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.
package/dist/index.js ADDED
@@ -0,0 +1,2711 @@
1
+ // src/renderers.ts
2
+ import { createElement } from "react";
3
+ var EMPTY = "\u2014";
4
+ var muted = (text) => createElement("span", { className: "qt-muted" }, text);
5
+ function isEmpty(v) {
6
+ return v == null || v === "" || Array.isArray(v) && v.length === 0;
7
+ }
8
+ var SAFE_LINK_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
9
+ function safeLinkHref(value) {
10
+ if (isEmpty(value)) return null;
11
+ const href = String(value).trim();
12
+ if (!href) return null;
13
+ try {
14
+ const parsed = new URL(href, "https://query-table.invalid/");
15
+ return SAFE_LINK_PROTOCOLS.has(parsed.protocol) ? href : null;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ var defaultRenderers = {
21
+ // String(value), em-dash for null/empty
22
+ text({ value }) {
23
+ if (isEmpty(value)) return muted(EMPTY);
24
+ return String(value);
25
+ },
26
+ // single-line ellipsis (CSS does the truncation; title carries the full text)
27
+ truncate({ value }) {
28
+ if (isEmpty(value)) return muted(EMPTY);
29
+ const s = String(value);
30
+ return createElement("span", { className: "qt-truncate", title: s }, s);
31
+ },
32
+ // locale-grouped number
33
+ number({ value }) {
34
+ if (value == null || value === "") return muted(EMPTY);
35
+ const n = typeof value === "number" ? value : Number(value);
36
+ if (!Number.isFinite(n)) return muted(EMPTY);
37
+ return n.toLocaleString();
38
+ },
39
+ // ✓ / —
40
+ bool_check({ value }) {
41
+ if (value == null) return muted(EMPTY);
42
+ return value ? "\u2713" : muted(EMPTY);
43
+ },
44
+ // ISO clamped to seconds
45
+ datetime({ value }) {
46
+ if (isEmpty(value)) return muted(EMPTY);
47
+ const s = String(value);
48
+ if (s.startsWith("0001-")) return muted(EMPTY);
49
+ return s.slice(0, 19).replace("T", " ");
50
+ },
51
+ // relative ("5m ago", "yesterday", "Mar 4")
52
+ datetime_rel({ value }) {
53
+ if (isEmpty(value)) return muted(EMPTY);
54
+ const s = String(value);
55
+ if (s.startsWith("0001-")) return muted(EMPTY);
56
+ return createElement("span", { title: s.slice(0, 19).replace("T", " ") }, formatRelative(s));
57
+ },
58
+ // human duration from a millisecond number
59
+ duration_ms({ value }) {
60
+ if (typeof value !== "number" || !Number.isFinite(value)) return muted(EMPTY);
61
+ return formatDuration(value);
62
+ },
63
+ // humanized bytes (B/KB/MB/GB)
64
+ byte_size({ value }) {
65
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return muted(EMPTY);
66
+ return formatBytes(value);
67
+ },
68
+ // string[] as pills
69
+ tags_pills({ value }) {
70
+ if (!Array.isArray(value) || value.length === 0) return muted(EMPTY);
71
+ return createElement(
72
+ "span",
73
+ { className: "qt-pills" },
74
+ value.map((t, i) => createElement("span", { key: `${String(t)}-${i}`, className: "qt-pill" }, String(t)))
75
+ );
76
+ },
77
+ // value as an <a href={value}> when its URL scheme is safe
78
+ link({ value }) {
79
+ if (isEmpty(value)) return muted(EMPTY);
80
+ const label = String(value);
81
+ const href = safeLinkHref(value);
82
+ if (!href) return createElement("span", { className: "qt-muted", title: "Unsafe link blocked" }, label);
83
+ return createElement(
84
+ "a",
85
+ { className: "qt-link", href, target: "_blank", rel: "noopener noreferrer", title: href },
86
+ label
87
+ );
88
+ }
89
+ };
90
+ function resolveRenderer(field, registry) {
91
+ const r = field.render;
92
+ if (typeof r === "function") {
93
+ return r;
94
+ }
95
+ if (typeof r === "string") {
96
+ const found = registry[r];
97
+ if (found) return found;
98
+ }
99
+ return registry["text"] ?? defaultRenderers["text"];
100
+ }
101
+ function formatDuration(ms) {
102
+ if (ms < 0) return String(ms);
103
+ if (ms < 1) return `${ms.toFixed(2)}ms`;
104
+ if (ms < 1e3) return `${ms.toFixed(0)}ms`;
105
+ const totalSec = ms / 1e3;
106
+ if (totalSec < 60) return `${totalSec.toFixed(1)}s`;
107
+ const h = Math.floor(totalSec / 3600);
108
+ const m = Math.floor(totalSec % 3600 / 60);
109
+ const s = Math.round(totalSec % 60);
110
+ if (h > 0) return `${h}h ${m}m`;
111
+ return `${m}m ${s}s`;
112
+ }
113
+ function formatBytes(n) {
114
+ if (n < 1024) return `${n} B`;
115
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
116
+ if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
117
+ return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
118
+ }
119
+ function formatRelative(iso) {
120
+ const ts = Date.parse(iso);
121
+ if (!Number.isFinite(ts)) return iso.slice(0, 19);
122
+ const now = Date.now();
123
+ const diffSec = Math.round((now - ts) / 1e3);
124
+ if (Math.abs(diffSec) < 30) return "just now";
125
+ const past = diffSec >= 0;
126
+ const abs = Math.abs(diffSec);
127
+ if (abs < 60) return past ? `${abs}s ago` : `in ${abs}s`;
128
+ if (abs < 3600) return past ? `${Math.round(abs / 60)}m ago` : `in ${Math.round(abs / 60)}m`;
129
+ if (abs < 86400) return past ? `${Math.round(abs / 3600)}h ago` : `in ${Math.round(abs / 3600)}h`;
130
+ const days = Math.round(abs / 86400);
131
+ if (past && days === 1) return "yesterday";
132
+ if (days < 7) return past ? `${days}d ago` : `in ${days}d`;
133
+ const d = new Date(ts);
134
+ const sameYear = d.getFullYear() === (/* @__PURE__ */ new Date()).getFullYear();
135
+ const opts = sameYear ? { month: "short", day: "numeric" } : { month: "short", day: "numeric", year: "2-digit" };
136
+ return d.toLocaleDateString(void 0, opts);
137
+ }
138
+
139
+ // src/DataTable.tsx
140
+ import { useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
141
+ import { isSortable, readFieldValue } from "@pythia-software/query-table-core";
142
+ import { useColumnDrag } from "@pythia-software/query-table-react";
143
+
144
+ // src/CellMenu.tsx
145
+ import { useEffect } from "react";
146
+ import { NULLARY_OPS, opsForField } from "@pythia-software/query-table-core";
147
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
148
+ var cx = (...parts) => parts.filter(Boolean).join(" ");
149
+ var MENU_WIDTH = 240;
150
+ var ROW_HEIGHT = 32;
151
+ var POINTER_OFFSET = 4;
152
+ function CellMenu({ field, value, x, y, onAddFilter, onClose, classNames }) {
153
+ useEffect(() => {
154
+ function onKey(e) {
155
+ if (e.key === "Escape") onClose();
156
+ }
157
+ function onDocClick() {
158
+ onClose();
159
+ }
160
+ window.addEventListener("keydown", onKey);
161
+ const t = setTimeout(() => window.addEventListener("click", onDocClick), 0);
162
+ return () => {
163
+ window.removeEventListener("keydown", onKey);
164
+ window.removeEventListener("click", onDocClick);
165
+ clearTimeout(t);
166
+ };
167
+ }, [onClose]);
168
+ const ops = opsForField(field);
169
+ const isArray = Array.isArray(value);
170
+ const isNullish = value == null || value === "" || isArray && value.length === 0;
171
+ function apply(op, raw) {
172
+ const v = raw != null ? raw : valueToString(value);
173
+ onAddFilter({ field: field.name, op, value: NULLARY_OPS.has(op) ? "" : v });
174
+ onClose();
175
+ }
176
+ const filterItems = [];
177
+ if (isArray && !isNullish) {
178
+ value.forEach((el, i) => {
179
+ const s = String(el);
180
+ const arrayOp = ops.includes("includes") ? "includes" : ops[0] ?? "=";
181
+ filterItems.push(
182
+ /* @__PURE__ */ jsxs(MenuItem, { classNames, onClick: () => apply(arrayOp, s), children: [
183
+ "filter: ",
184
+ opSymbol(arrayOp),
185
+ " ",
186
+ /* @__PURE__ */ jsx("code", { children: s })
187
+ ] }, `el-${i}`)
188
+ );
189
+ });
190
+ }
191
+ for (const op of ops) {
192
+ const nullary = NULLARY_OPS.has(op);
193
+ if (isNullish && !nullary) continue;
194
+ if (isArray && !nullary && !isNullish) continue;
195
+ filterItems.push(
196
+ /* @__PURE__ */ jsxs(MenuItem, { classNames, onClick: () => apply(op), children: [
197
+ "filter: ",
198
+ opSymbol(op),
199
+ !nullary && !isNullish ? /* @__PURE__ */ jsxs(Fragment, { children: [
200
+ " ",
201
+ /* @__PURE__ */ jsx("code", { children: previewValue(value) })
202
+ ] }) : null
203
+ ] }, op)
204
+ );
205
+ }
206
+ const copyItem = /* @__PURE__ */ jsx(
207
+ MenuItem,
208
+ {
209
+ classNames,
210
+ onClick: async () => {
211
+ try {
212
+ await navigator.clipboard.writeText(valueToString(value));
213
+ } catch {
214
+ }
215
+ onClose();
216
+ },
217
+ children: "copy value"
218
+ },
219
+ "copy"
220
+ );
221
+ const items = [
222
+ /* @__PURE__ */ jsxs("div", { className: "qt-cm-header", children: [
223
+ /* @__PURE__ */ jsx("span", { className: "qt-cm-field", children: field.label }),
224
+ /* @__PURE__ */ jsx("span", { className: "qt-cm-value", children: previewValue(value) })
225
+ ] }, "header"),
226
+ copyItem,
227
+ /* @__PURE__ */ jsx("div", { className: cx("qt-cm-sep", classNames?.separator) }, "sep"),
228
+ ...filterItems
229
+ ];
230
+ const vw = typeof window !== "undefined" ? window.innerWidth : 9999;
231
+ const vh = typeof window !== "undefined" ? window.innerHeight : 9999;
232
+ const left = Math.max(8, Math.min(x + POINTER_OFFSET, vw - MENU_WIDTH - 8));
233
+ const top = Math.max(8, Math.min(y + POINTER_OFFSET, vh - ROW_HEIGHT * items.length - 8));
234
+ return /* @__PURE__ */ jsx(
235
+ "div",
236
+ {
237
+ className: cx("qt-cell-menu", classNames?.popover),
238
+ style: { left, top, width: MENU_WIDTH },
239
+ onClick: (e) => e.stopPropagation(),
240
+ onContextMenu: (e) => e.preventDefault(),
241
+ role: "menu",
242
+ children: items
243
+ }
244
+ );
245
+ }
246
+ function MenuItem({
247
+ children,
248
+ onClick,
249
+ classNames
250
+ }) {
251
+ return /* @__PURE__ */ jsx("button", { type: "button", className: cx("qt-cm-item", classNames?.item), onClick, role: "menuitem", children });
252
+ }
253
+ function opSymbol(op) {
254
+ switch (op) {
255
+ case "!=":
256
+ return "\u2260";
257
+ case ">=":
258
+ return "\u2265";
259
+ case "<=":
260
+ return "\u2264";
261
+ case "is_null":
262
+ return "is null";
263
+ case "is_not_null":
264
+ return "is not null";
265
+ default:
266
+ return op;
267
+ }
268
+ }
269
+ function valueToString(v) {
270
+ if (v == null) return "";
271
+ if (Array.isArray(v)) return v.join(", ");
272
+ return String(v);
273
+ }
274
+ function previewValue(v) {
275
+ if (v == null || v === "") return "(empty)";
276
+ if (Array.isArray(v)) {
277
+ return v.length === 0 ? "(empty)" : `[${v.length}] ${v.slice(0, 2).join(", ")}${v.length > 2 ? "\u2026" : ""}`;
278
+ }
279
+ const s = String(v);
280
+ return s.length > 50 ? s.slice(0, 50) + "\u2026" : s;
281
+ }
282
+
283
+ // src/DataTable.tsx
284
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
285
+ var cx2 = (...parts) => parts.filter((p) => Boolean(p)).join(" ");
286
+ var MIN_WIDTH = 1;
287
+ var MIN_SELECTION_WIDTH = 1;
288
+ var MAX_WIDTH = 800;
289
+ var DEFAULT_WIDTH = 120;
290
+ var DEFAULT_SELECTION_WIDTH = 36;
291
+ var AUTOFIT_EXTRA_PX = 16;
292
+ var MENU_WIDTH2 = 240;
293
+ var MENU_ROW_HEIGHT = 32;
294
+ var COPY_FEEDBACK_MS = 900;
295
+ var SELECTION_COLUMN = "__qt_selection__";
296
+ function DataTable(props) {
297
+ const {
298
+ fields,
299
+ rows,
300
+ query,
301
+ onQueryChange,
302
+ renderers,
303
+ rowId,
304
+ selection,
305
+ trailing,
306
+ trailingLabel,
307
+ total = null,
308
+ loading,
309
+ emptyMessage,
310
+ classNames
311
+ } = props;
312
+ const [menu, setMenu] = useState2(null);
313
+ const [resizingField, setResizingField] = useState2(null);
314
+ const [draftWidths, setDraftWidths] = useState2({});
315
+ const [selectionColumnWidth, setSelectionColumnWidth] = useState2(DEFAULT_SELECTION_WIDTH);
316
+ const [tableColumnOrder, setTableColumnOrder] = useState2([]);
317
+ const [copiedCellKey, setCopiedCellKey] = useState2(null);
318
+ const tableWrapRef = useRef(null);
319
+ const resizeCommitRef = useRef(null);
320
+ const resizingFieldRef = useRef(null);
321
+ const copiedCellTimerRef = useRef(null);
322
+ const localColumnDrag = useColumnDrag();
323
+ const columnDrag = props.columnDrag ?? localColumnDrag;
324
+ useEffect2(() => {
325
+ return () => {
326
+ if (copiedCellTimerRef.current) {
327
+ clearTimeout(copiedCellTimerRef.current);
328
+ copiedCellTimerRef.current = null;
329
+ }
330
+ };
331
+ }, []);
332
+ function cellKey(rowIdValue, rowIndex, fieldName) {
333
+ return rowIdValue == null ? `row:${rowIndex}:${fieldName}` : `id:${rowIdValue}:${fieldName}`;
334
+ }
335
+ function isInteractiveTarget(target) {
336
+ if (!(target instanceof HTMLElement)) return false;
337
+ const tag = target.tagName;
338
+ return tag === "A" || tag === "BUTTON" || tag === "INPUT" || tag === "SELECT";
339
+ }
340
+ function showCopyFeedback(cellId) {
341
+ if (copiedCellTimerRef.current) {
342
+ clearTimeout(copiedCellTimerRef.current);
343
+ copiedCellTimerRef.current = null;
344
+ }
345
+ setCopiedCellKey(cellId);
346
+ copiedCellTimerRef.current = setTimeout(() => {
347
+ setCopiedCellKey((current) => current === cellId ? null : current);
348
+ copiedCellTimerRef.current = null;
349
+ }, COPY_FEEDBACK_MS);
350
+ }
351
+ async function copyCellToClipboard(e, f, rowIdValue, rowIndex, value) {
352
+ if (isInteractiveTarget(e.target)) return;
353
+ e.preventDefault();
354
+ e.stopPropagation();
355
+ setMenu(null);
356
+ try {
357
+ if (navigator.clipboard) {
358
+ await navigator.clipboard.writeText(valueToString2(value));
359
+ }
360
+ } catch {
361
+ }
362
+ showCopyFeedback(cellKey(rowIdValue, rowIndex, f.name));
363
+ }
364
+ const pageIds = rows.map(rowId).filter((id) => id != null);
365
+ const headerState = selection ? selection.pageState(pageIds) : "none";
366
+ const showSel = selection != null;
367
+ function sortKeyFor(f) {
368
+ return f.sort?.field ?? f.name;
369
+ }
370
+ function applyHeaderSort(f, dir, placement) {
371
+ if (!isSortable(f)) return;
372
+ onQueryChange({
373
+ ...query,
374
+ offset: 0,
375
+ orderBy: nextHeaderOrderBy(query.orderBy, sortKeyFor(f), dir, placement)
376
+ });
377
+ setMenu(null);
378
+ }
379
+ function removeColumn(f) {
380
+ const explicit = materialize(query.select, fields);
381
+ const nextSelect = explicit.filter((c) => c.field !== f.name);
382
+ const nextOrderBy = query.orderBy.filter((s) => s.field !== sortKeyFor(f));
383
+ if (nextSelect.length === 0) return;
384
+ onQueryChange({ ...query, select: nextSelect, orderBy: nextOrderBy });
385
+ setMenu(null);
386
+ }
387
+ function sortInfo(f) {
388
+ const key = sortKeyFor(f);
389
+ const idx = query.orderBy.findIndex((o) => o.field === key);
390
+ if (idx === -1) return null;
391
+ const term = query.orderBy[idx];
392
+ return { dir: term.dir, priority: query.orderBy.length > 1 ? idx + 1 : null };
393
+ }
394
+ function widthFor(name, f) {
395
+ const col = query.select.find((c) => c.field === name);
396
+ return col?.width ?? f.select?.width;
397
+ }
398
+ function resolvedWidthFor(name, f) {
399
+ return draftWidths[name] ?? widthFor(name, f);
400
+ }
401
+ function resolvedColumnWidthFor(name, f) {
402
+ if (name === SELECTION_COLUMN) return draftWidths[name] ?? selectionColumnWidth;
403
+ return f ? resolvedWidthFor(name, f) : draftWidths[name];
404
+ }
405
+ function displayColumnWidthFor(name, f) {
406
+ if (name === SELECTION_COLUMN) return resolvedColumnWidthFor(name) ?? DEFAULT_SELECTION_WIDTH;
407
+ return f ? resolvedWidthFor(name, f) ?? DEFAULT_WIDTH : DEFAULT_WIDTH;
408
+ }
409
+ function clampWidth(width) {
410
+ return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.round(width)));
411
+ }
412
+ function clampColumnWidth(name, width) {
413
+ const min = name === SELECTION_COLUMN ? MIN_SELECTION_WIDTH : MIN_WIDTH;
414
+ return Math.max(min, Math.min(MAX_WIDTH, Math.round(width)));
415
+ }
416
+ function commitColumnWidth(name, width) {
417
+ if (name === SELECTION_COLUMN) {
418
+ setSelectionColumnWidth(clampColumnWidth(name, width));
419
+ return;
420
+ }
421
+ setWidth(name, width);
422
+ }
423
+ function setWidth(name, width) {
424
+ const px = clampWidth(width);
425
+ onQueryChange((prev) => ({ ...prev, select: writeWidth(prev.select, fields, name, px) }));
426
+ }
427
+ function setDraftWidth(name, width) {
428
+ setDraftWidths((prev) => prev[name] === width ? prev : { ...prev, [name]: width });
429
+ }
430
+ function clearDraftWidth(name) {
431
+ setDraftWidths((prev) => {
432
+ if (!(name in prev)) return prev;
433
+ const next = { ...prev };
434
+ delete next[name];
435
+ return next;
436
+ });
437
+ }
438
+ function startResize(name, startWidth, e) {
439
+ e.preventDefault();
440
+ e.stopPropagation();
441
+ const startX = e.clientX;
442
+ const startW = startWidth ?? e.currentTarget.parentElement?.getBoundingClientRect().width ?? DEFAULT_WIDTH;
443
+ const previousCursor = document.body.style.cursor;
444
+ const previousUserSelect = document.body.style.userSelect;
445
+ resizeCommitRef.current = null;
446
+ resizingFieldRef.current = name;
447
+ setResizingField(name);
448
+ setDraftWidth(name, clampColumnWidth(name, startW));
449
+ document.body.style.cursor = "col-resize";
450
+ document.body.style.userSelect = "none";
451
+ const finish = () => {
452
+ window.removeEventListener("pointermove", onMove);
453
+ window.removeEventListener("pointerup", finish);
454
+ window.removeEventListener("pointercancel", finish);
455
+ document.body.style.cursor = previousCursor;
456
+ document.body.style.userSelect = previousUserSelect;
457
+ const commit = resizeCommitRef.current;
458
+ resizeCommitRef.current = null;
459
+ resizingFieldRef.current = null;
460
+ setResizingField(null);
461
+ clearDraftWidth(name);
462
+ if (commit) commitColumnWidth(commit.name, commit.width);
463
+ };
464
+ const onMove = (ev) => {
465
+ const next = clampColumnWidth(name, startW + (ev.clientX - startX));
466
+ resizeCommitRef.current = { name, width: next };
467
+ setDraftWidth(name, next);
468
+ };
469
+ window.addEventListener("pointermove", onMove);
470
+ window.addEventListener("pointerup", finish);
471
+ window.addEventListener("pointercancel", finish);
472
+ }
473
+ function autofitColumn(name) {
474
+ const wrap = tableWrapRef.current;
475
+ if (!wrap) return;
476
+ const selector = `[data-qt-field="${cssAttributeValue(name)}"]`;
477
+ let width = 0;
478
+ wrap.querySelectorAll(selector).forEach((el) => {
479
+ width = Math.max(width, el.scrollWidth);
480
+ });
481
+ if (width > 0) setWidth(name, width + AUTOFIT_EXTRA_PX);
482
+ }
483
+ function resizeWithKeyboard(name, startWidth, e) {
484
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight" && e.key !== "Enter") return;
485
+ e.preventDefault();
486
+ e.stopPropagation();
487
+ if (e.key === "Enter") {
488
+ autofitColumn(name);
489
+ return;
490
+ }
491
+ const step = e.shiftKey ? 25 : 10;
492
+ const direction = e.key === "ArrowRight" ? 1 : -1;
493
+ commitColumnWidth(name, (startWidth ?? DEFAULT_WIDTH) + step * direction);
494
+ }
495
+ function reorderByIndex(from, overIndex) {
496
+ const withoutDragged = tableColumnNames.filter((n) => n !== from);
497
+ if (withoutDragged.length === tableColumnNames.length) return;
498
+ const at = Math.max(0, Math.min(overIndex, withoutDragged.length));
499
+ withoutDragged.splice(at, 0, from);
500
+ setTableColumnOrder(withoutDragged);
501
+ const nextFieldOrder = withoutDragged.filter((name) => name !== SELECTION_COLUMN);
502
+ if (!sameOrder(nextFieldOrder, fieldNames)) {
503
+ onQueryChange({ ...query, select: reorderSelect(query.select, fields, nextFieldOrder) });
504
+ }
505
+ }
506
+ const fieldByName = useMemo(() => new Map(fields.map((f) => [f.name, f])), [fields]);
507
+ const fieldNames = useMemo(() => fields.map((f) => f.name), [fields]);
508
+ const tableColumnNames = useMemo(() => {
509
+ if (!showSel) return fieldNames;
510
+ const defaultOrder = [SELECTION_COLUMN, ...fieldNames];
511
+ const valid = new Set(defaultOrder);
512
+ const kept = tableColumnOrder.filter((name) => valid.has(name));
513
+ const missing = defaultOrder.filter((name) => !kept.includes(name));
514
+ return [...kept, ...missing];
515
+ }, [fieldNames, showSel, tableColumnOrder]);
516
+ const dragSource = columnDrag.source;
517
+ const renderedColumnNames = useMemo(() => columnDrag.preview(tableColumnNames), [columnDrag, tableColumnNames]);
518
+ function toggleHeader() {
519
+ if (!selection) return;
520
+ selection.setPage(pageIds, headerState !== "all");
521
+ }
522
+ function openMenu(e, f, row) {
523
+ if (isInteractiveTarget(e.target)) return;
524
+ e.preventDefault();
525
+ setMenu({ kind: "cell", field: f, value: readFieldValue(f, row), x: e.clientX, y: e.clientY });
526
+ }
527
+ function openHeaderMenu(e, f) {
528
+ if (isInteractiveTarget(e.target)) return;
529
+ e.preventDefault();
530
+ setMenu({ kind: "header", field: f, x: e.clientX, y: e.clientY });
531
+ }
532
+ function onTableWheel(e) {
533
+ const wrap = tableWrapRef.current;
534
+ if (!wrap) return;
535
+ const canScrollHorizontally = wrap.scrollWidth > wrap.clientWidth;
536
+ if (!canScrollHorizontally) return;
537
+ const dx = e.deltaX;
538
+ const dy = e.deltaY;
539
+ const absDx = Math.abs(dx);
540
+ const absDy = Math.abs(dy);
541
+ const prefersHorizontal = absDx > absDy * 1.25;
542
+ if (dx === 0 && !e.shiftKey) return;
543
+ if (dx === 0 && e.shiftKey) {
544
+ e.preventDefault();
545
+ e.stopPropagation();
546
+ wrap.scrollLeft += dy;
547
+ return;
548
+ }
549
+ if (e.shiftKey || prefersHorizontal) {
550
+ e.preventDefault();
551
+ e.stopPropagation();
552
+ wrap.scrollLeft += dx;
553
+ }
554
+ }
555
+ function resizeHandle(name, label, width) {
556
+ return /* @__PURE__ */ jsx2(
557
+ "span",
558
+ {
559
+ className: cx2("qt-resize-handle", classNames?.resizeHandle),
560
+ role: "separator",
561
+ "aria-orientation": "vertical",
562
+ "aria-label": `Resize ${label} column`,
563
+ title: "Drag to resize. Press Enter to auto-fit.",
564
+ tabIndex: 0,
565
+ onPointerDown: (e) => startResize(name, width, e),
566
+ onKeyDown: (e) => resizeWithKeyboard(name, width, e),
567
+ onClick: (e) => e.stopPropagation()
568
+ }
569
+ );
570
+ }
571
+ const totalCols = fields.length + (showSel ? 1 : 0) + (trailing ? 1 : 0);
572
+ const tableWidth = renderedColumnNames.reduce((sum, name) => {
573
+ const f = fieldByName.get(name);
574
+ return sum + displayColumnWidthFor(name, f);
575
+ }, trailing ? 40 : 0);
576
+ const start = query.offset;
577
+ const end = total != null ? Math.min(start + query.limit, total) : start + rows.length;
578
+ const canPrev = start > 0;
579
+ const canNext = total != null ? end < total : rows.length >= query.limit;
580
+ const prevPage = () => onQueryChange((prev) => ({ ...prev, offset: Math.max(0, prev.offset - prev.limit) }));
581
+ const nextPage = () => onQueryChange((prev) => ({ ...prev, offset: prev.offset + prev.limit }));
582
+ const summaryText = total == null || total === 0 ? `${rows.length} rows` : `${start + 1}\u2013${end} of ${total}`;
583
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
584
+ /* @__PURE__ */ jsxs2(
585
+ "div",
586
+ {
587
+ ref: tableWrapRef,
588
+ className: cx2(
589
+ "qt-table-wrap",
590
+ loading && "qt-table-wrap--loading",
591
+ !!resizingField && "qt-table-wrap--resizing",
592
+ classNames?.wrap
593
+ ),
594
+ onWheel: onTableWheel,
595
+ children: [
596
+ loading && /* @__PURE__ */ jsx2("div", { className: cx2("qt-loading-bar", classNames?.loadingBar), role: "progressbar", "aria-label": "loading" }),
597
+ /* @__PURE__ */ jsxs2("table", { className: cx2("qt-table", classNames?.table), style: { minWidth: tableWidth }, children: [
598
+ /* @__PURE__ */ jsxs2("colgroup", { children: [
599
+ renderedColumnNames.map((name) => {
600
+ const f = fieldByName.get(name);
601
+ const w = displayColumnWidthFor(name, f);
602
+ if (name === SELECTION_COLUMN) return /* @__PURE__ */ jsx2("col", { style: { width: w } }, name);
603
+ if (!f) return null;
604
+ return /* @__PURE__ */ jsx2("col", { style: { width: w } }, f.name);
605
+ }),
606
+ trailing && /* @__PURE__ */ jsx2("col", { style: { width: 40 } })
607
+ ] }),
608
+ /* @__PURE__ */ jsx2("thead", { className: classNames?.thead, children: /* @__PURE__ */ jsxs2("tr", { className: classNames?.headerRow, children: [
609
+ renderedColumnNames.map((name) => {
610
+ if (name === SELECTION_COLUMN) {
611
+ return /* @__PURE__ */ jsxs2(
612
+ "th",
613
+ {
614
+ "data-qt-field": name,
615
+ className: cx2(
616
+ "qt-th",
617
+ "qt-checkbox-cell",
618
+ dragSource === name && "qt-th--dragging",
619
+ resizingField === name && "qt-th--resizing",
620
+ classNames?.th,
621
+ classNames?.checkboxCell
622
+ ),
623
+ draggable: resizingField == null,
624
+ onDragStart: (e) => {
625
+ if (resizingFieldRef.current) {
626
+ e.preventDefault();
627
+ return;
628
+ }
629
+ columnDrag.start(name, tableColumnNames.indexOf(name));
630
+ e.dataTransfer.effectAllowed = "move";
631
+ e.dataTransfer.setData("text/plain", name);
632
+ },
633
+ onDragOver: (e) => {
634
+ if (!columnDrag.source) return;
635
+ e.preventDefault();
636
+ e.dataTransfer.dropEffect = "move";
637
+ if (columnDrag.source !== name) {
638
+ const withoutDragged = tableColumnNames.filter((n) => n !== columnDrag.source);
639
+ let next = withoutDragged.indexOf(name);
640
+ if (next >= 0) {
641
+ const rect = e.currentTarget.getBoundingClientRect();
642
+ if (e.clientX > rect.left + rect.width / 2) next += 1;
643
+ columnDrag.over(next);
644
+ }
645
+ }
646
+ },
647
+ onDrop: (e) => {
648
+ e.preventDefault();
649
+ e.dataTransfer.dropEffect = "move";
650
+ const from = columnDrag.source || e.dataTransfer.getData("text/plain");
651
+ if (from && columnDrag.overIndex != null) reorderByIndex(from, columnDrag.overIndex);
652
+ columnDrag.end();
653
+ },
654
+ onDragEnd: () => columnDrag.end(),
655
+ children: [
656
+ /* @__PURE__ */ jsx2(
657
+ "input",
658
+ {
659
+ type: "checkbox",
660
+ checked: headerState === "all",
661
+ ref: (el) => {
662
+ if (el) el.indeterminate = headerState === "some";
663
+ },
664
+ onChange: toggleHeader,
665
+ "aria-label": "select all on page"
666
+ }
667
+ ),
668
+ resizeHandle(name, "selection", resolvedColumnWidthFor(name))
669
+ ]
670
+ },
671
+ name
672
+ );
673
+ }
674
+ const f = fieldByName.get(name);
675
+ if (!f) return null;
676
+ const info = sortInfo(f);
677
+ const sortable = isSortable(f);
678
+ return /* @__PURE__ */ jsxs2(
679
+ "th",
680
+ {
681
+ "data-qt-field": f.name,
682
+ className: cx2(
683
+ "qt-th",
684
+ sortable && "qt-th--sortable",
685
+ dragSource === f.name && "qt-th--dragging",
686
+ resizingField === f.name && "qt-th--resizing",
687
+ classNames?.th
688
+ ),
689
+ style: f.select?.align ? { textAlign: f.select.align } : void 0,
690
+ draggable: resizingField == null,
691
+ onDragStart: (e) => {
692
+ if (resizingFieldRef.current) {
693
+ e.preventDefault();
694
+ return;
695
+ }
696
+ columnDrag.start(f.name, tableColumnNames.indexOf(f.name));
697
+ e.dataTransfer.effectAllowed = "move";
698
+ e.dataTransfer.setData("text/plain", f.name);
699
+ },
700
+ onDragOver: (e) => {
701
+ if (!columnDrag.source) return;
702
+ e.preventDefault();
703
+ e.dataTransfer.dropEffect = "move";
704
+ if (columnDrag.source !== f.name) {
705
+ const withoutDragged = tableColumnNames.filter((n) => n !== columnDrag.source);
706
+ let next = withoutDragged.indexOf(f.name);
707
+ if (next >= 0) {
708
+ const rect = e.currentTarget.getBoundingClientRect();
709
+ if (e.clientX > rect.left + rect.width / 2) next += 1;
710
+ columnDrag.over(next);
711
+ }
712
+ }
713
+ },
714
+ onDrop: (e) => {
715
+ e.preventDefault();
716
+ e.dataTransfer.dropEffect = "move";
717
+ const from = columnDrag.source || e.dataTransfer.getData("text/plain");
718
+ if (from && columnDrag.overIndex != null) reorderByIndex(from, columnDrag.overIndex);
719
+ columnDrag.end();
720
+ },
721
+ onDragEnd: () => columnDrag.end(),
722
+ children: [
723
+ /* @__PURE__ */ jsxs2(
724
+ "span",
725
+ {
726
+ className: "qt-th-label",
727
+ onClick: (e) => openHeaderMenu(e, f),
728
+ style: { cursor: "pointer" },
729
+ title: "Click to open column actions",
730
+ children: [
731
+ f.label,
732
+ info && /* @__PURE__ */ jsxs2("span", { className: "qt-sort-indicator", children: [
733
+ info.dir === "asc" ? " \u2191" : " \u2193",
734
+ info.priority != null && /* @__PURE__ */ jsx2("sup", { className: "qt-sort-priority", children: info.priority })
735
+ ] })
736
+ ]
737
+ }
738
+ ),
739
+ resizeHandle(f.name, f.label, resolvedColumnWidthFor(f.name, f))
740
+ ]
741
+ },
742
+ f.name
743
+ );
744
+ }),
745
+ trailing && /* @__PURE__ */ jsx2("th", { className: cx2("qt-th", classNames?.th), children: trailingLabel ?? "" })
746
+ ] }) }),
747
+ /* @__PURE__ */ jsxs2("tbody", { className: classNames?.tbody, children: [
748
+ rows.length === 0 && /* @__PURE__ */ jsx2("tr", { children: /* @__PURE__ */ jsx2("td", { colSpan: totalCols, className: cx2("qt-cell", "qt-empty", classNames?.empty), children: emptyMessage ?? "No rows." }) }),
749
+ rows.map((row, i) => {
750
+ const id = rowId(row);
751
+ const selected = id != null && selection ? selection.isSelected(id) : false;
752
+ return /* @__PURE__ */ jsxs2(
753
+ "tr",
754
+ {
755
+ className: cx2("qt-row", selected && "qt-row--selected", classNames?.row, selected && classNames?.rowSelected),
756
+ children: [
757
+ renderedColumnNames.map((name) => {
758
+ if (name === SELECTION_COLUMN) {
759
+ return /* @__PURE__ */ jsx2(
760
+ "td",
761
+ {
762
+ "data-qt-field": name,
763
+ className: cx2(
764
+ "qt-cell",
765
+ "qt-checkbox-cell",
766
+ dragSource === name && "qt-cell--dragging",
767
+ classNames?.cell,
768
+ classNames?.checkboxCell
769
+ ),
770
+ children: id != null && /* @__PURE__ */ jsx2(
771
+ "input",
772
+ {
773
+ type: "checkbox",
774
+ checked: selected,
775
+ onClick: (e) => selection.toggle(id, e.shiftKey),
776
+ onChange: () => {
777
+ },
778
+ "aria-label": `select row ${String(id)}`
779
+ }
780
+ )
781
+ },
782
+ name
783
+ );
784
+ }
785
+ const f = fieldByName.get(name);
786
+ if (!f) return null;
787
+ const render = resolveRenderer(f, renderers);
788
+ const value = readFieldValue(f, row);
789
+ const key = cellKey(id, i, f.name);
790
+ return /* @__PURE__ */ jsx2(
791
+ "td",
792
+ {
793
+ "data-qt-field": f.name,
794
+ className: cx2("qt-cell", dragSource === f.name && "qt-cell--dragging", classNames?.cell),
795
+ style: f.select?.align ? { textAlign: f.select.align } : void 0,
796
+ onClick: (e) => openMenu(e, f, row),
797
+ onContextMenu: (e) => openMenu(e, f, row),
798
+ onDoubleClick: (e) => copyCellToClipboard(e, f, id, i, value),
799
+ children: copiedCellKey === key ? /* @__PURE__ */ jsx2("span", { className: "qt-cell-copy-chip", children: "\u2713 copied" }) : render({ value, row, field: f, query })
800
+ },
801
+ f.name
802
+ );
803
+ }),
804
+ trailing && /* @__PURE__ */ jsx2("td", { className: cx2("qt-cell", "qt-trailing-cell", classNames?.cell), children: trailing(row) })
805
+ ]
806
+ },
807
+ id != null ? String(id) : i
808
+ );
809
+ })
810
+ ] }),
811
+ /* @__PURE__ */ jsx2("tfoot", { children: /* @__PURE__ */ jsx2("tr", { className: cx2("qt-table-summary-row", classNames?.summaryRow), children: /* @__PURE__ */ jsx2("td", { colSpan: totalCols, className: cx2("qt-cell", "qt-table-summary-cell", classNames?.summaryCell), children: /* @__PURE__ */ jsxs2("div", { className: cx2("qt-table-summary", classNames?.summaryControls), children: [
812
+ /* @__PURE__ */ jsx2(
813
+ "button",
814
+ {
815
+ type: "button",
816
+ className: cx2("qt-btn", classNames?.summaryButton),
817
+ disabled: loading || !canPrev,
818
+ onClick: prevPage,
819
+ children: "\u2190 prev"
820
+ }
821
+ ),
822
+ /* @__PURE__ */ jsx2("span", { className: cx2("qt-table-summary-hint", classNames?.summaryHint), children: summaryText }),
823
+ /* @__PURE__ */ jsx2(
824
+ "button",
825
+ {
826
+ type: "button",
827
+ className: cx2("qt-btn", classNames?.summaryButton),
828
+ disabled: loading || !canNext,
829
+ onClick: nextPage,
830
+ children: "next \u2192"
831
+ }
832
+ )
833
+ ] }) }) }) })
834
+ ] })
835
+ ]
836
+ }
837
+ ),
838
+ menu && (menu.kind === "cell" ? /* @__PURE__ */ jsx2(
839
+ CellMenu,
840
+ {
841
+ field: menu.field,
842
+ value: menu.value,
843
+ x: menu.x,
844
+ y: menu.y,
845
+ onAddFilter: (clause) => onQueryChange((prev) => ({ ...prev, offset: 0, where: [...prev.where, clause] })),
846
+ onClose: () => setMenu(null)
847
+ }
848
+ ) : /* @__PURE__ */ jsx2(
849
+ HeaderMenu,
850
+ {
851
+ field: menu.field,
852
+ x: menu.x,
853
+ y: menu.y,
854
+ sortable: isSortable(menu.field),
855
+ canRemoveColumn: fields.length > 1,
856
+ onSetSort: (placement, dir) => applyHeaderSort(menu.field, dir, placement),
857
+ onRemove: () => removeColumn(menu.field),
858
+ onClose: () => setMenu(null)
859
+ }
860
+ ))
861
+ ] });
862
+ }
863
+ function HeaderMenu({
864
+ field,
865
+ x,
866
+ y,
867
+ sortable,
868
+ canRemoveColumn,
869
+ onSetSort,
870
+ onRemove,
871
+ onClose
872
+ }) {
873
+ useEffect2(() => {
874
+ function onKey(e) {
875
+ if (e.key === "Escape") onClose();
876
+ }
877
+ function onDocClick() {
878
+ onClose();
879
+ }
880
+ window.addEventListener("keydown", onKey);
881
+ const t = setTimeout(() => window.addEventListener("click", onDocClick), 0);
882
+ return () => {
883
+ window.removeEventListener("keydown", onKey);
884
+ window.removeEventListener("click", onDocClick);
885
+ clearTimeout(t);
886
+ };
887
+ }, [onClose]);
888
+ const items = [];
889
+ const addSortItem = (placement, dir, label) => items.push(
890
+ /* @__PURE__ */ jsx2(MenuItem2, { disabled: !sortable, onClick: () => onSetSort(placement, dir), children: label }, `${placement}-${dir}`)
891
+ );
892
+ addSortItem("set", "asc", "Set Sort (Asc)");
893
+ addSortItem("set", "desc", "Set Sort (Desc)");
894
+ addSortItem("append", "asc", "Append Sort (Asc)");
895
+ addSortItem("append", "desc", "Append Sort (Desc)");
896
+ addSortItem("prepend", "asc", "Prepend Sort (Asc)");
897
+ addSortItem("prepend", "desc", "Prepend Sort (Desc)");
898
+ items.push(/* @__PURE__ */ jsx2("div", { className: "qt-cm-sep" }, "sep"));
899
+ items.push(
900
+ /* @__PURE__ */ jsx2(MenuItem2, { disabled: !canRemoveColumn, onClick: onRemove, children: "Remove Column" }, "remove")
901
+ );
902
+ const vw = typeof window !== "undefined" ? window.innerWidth : 9999;
903
+ const vh = typeof window !== "undefined" ? window.innerHeight : 9999;
904
+ const left = Math.max(8, Math.min(x, vw - MENU_WIDTH2 - 8));
905
+ const top = Math.max(8, Math.min(y, vh - MENU_ROW_HEIGHT * items.length - 8));
906
+ return /* @__PURE__ */ jsxs2(
907
+ "div",
908
+ {
909
+ className: "qt-cell-menu",
910
+ style: { left, top, width: MENU_WIDTH2 },
911
+ onClick: (e) => e.stopPropagation(),
912
+ onContextMenu: (e) => e.preventDefault(),
913
+ role: "menu",
914
+ children: [
915
+ /* @__PURE__ */ jsx2("div", { className: "qt-cm-header", children: /* @__PURE__ */ jsx2("span", { className: "qt-cm-field", children: field.label }) }),
916
+ items
917
+ ]
918
+ }
919
+ );
920
+ }
921
+ function MenuItem2({
922
+ children,
923
+ onClick,
924
+ disabled
925
+ }) {
926
+ return /* @__PURE__ */ jsx2(
927
+ "button",
928
+ {
929
+ type: "button",
930
+ className: "qt-cm-item",
931
+ onClick,
932
+ role: "menuitem",
933
+ disabled,
934
+ children
935
+ }
936
+ );
937
+ }
938
+ function nextHeaderOrderBy(existing, field, dir, placement) {
939
+ const next = existing.filter((s) => s.field !== field);
940
+ if (placement === "set") return [{ field, dir }];
941
+ if (placement === "append") return [...next, { field, dir }];
942
+ return [{ field, dir }, ...next];
943
+ }
944
+ function materialize(select, fields) {
945
+ if (select.length) return select.map((c) => ({ ...c }));
946
+ return fields.map((f) => ({ field: f.name }));
947
+ }
948
+ function writeWidth(select, fields, name, width) {
949
+ const cols = materialize(select, fields);
950
+ let found = false;
951
+ const next = cols.map((c) => {
952
+ if (c.field !== name) return c;
953
+ found = true;
954
+ return { ...c, width };
955
+ });
956
+ if (!found) next.push({ field: name, width });
957
+ return next;
958
+ }
959
+ function reorderSelect(select, fields, order) {
960
+ const cols = materialize(select, fields);
961
+ const byName = new Map(cols.map((c) => [c.field, c]));
962
+ return order.map((name) => byName.get(name) ?? { field: name });
963
+ }
964
+ function cssAttributeValue(value) {
965
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
966
+ }
967
+ function sameOrder(a, b) {
968
+ return a.length === b.length && a.every((value, index) => value === b[index]);
969
+ }
970
+ function valueToString2(value) {
971
+ if (value == null) return "";
972
+ if (Array.isArray(value)) return value.join(", ");
973
+ return String(value);
974
+ }
975
+
976
+ // src/QueryBuilder.tsx
977
+ import { useEffect as useEffect4, useId, useMemo as useMemo3, useRef as useRef2, useState as useState4 } from "react";
978
+ import {
979
+ NULLARY_OPS as NULLARY_OPS2,
980
+ aggOpNeedsField,
981
+ aggOpsForField,
982
+ filterValues as filterValuesFor,
983
+ isGroupable,
984
+ isMeasurable,
985
+ queriesEqual,
986
+ isFilterable,
987
+ isSelectable,
988
+ isSortable as isSortable2,
989
+ opsForField as opsForField2
990
+ } from "@pythia-software/query-table-core";
991
+
992
+ // src/FieldPicker.tsx
993
+ import { useMemo as useMemo2, useState as useState3 } from "react";
994
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
995
+ var UNGROUPED = "other";
996
+ function FieldPicker({
997
+ fields,
998
+ excluded,
999
+ stats,
1000
+ gateOnDistinct,
1001
+ onPick,
1002
+ onClose
1003
+ }) {
1004
+ const [search, setSearch] = useState3("");
1005
+ const needle = search.trim().toLowerCase();
1006
+ const excludedSet = useMemo2(() => new Set(excluded ?? []), [excluded]);
1007
+ const matched = useMemo2(
1008
+ () => fields.filter((f) => {
1009
+ if (excludedSet.has(f.name)) return false;
1010
+ if (!needle) return true;
1011
+ return f.label.toLowerCase().includes(needle) || f.name.toLowerCase().includes(needle) || fieldAliases(f).some((a) => a.toLowerCase().includes(needle));
1012
+ }),
1013
+ [fields, excludedSet, needle]
1014
+ );
1015
+ const grouped = useMemo2(() => groupByGroup(matched), [matched]);
1016
+ const firstEnabled = matched.find((f) => isEnabled(f, stats, gateOnDistinct));
1017
+ return /* @__PURE__ */ jsxs3("div", { className: "qt-picker", children: [
1018
+ /* @__PURE__ */ jsx3(
1019
+ "input",
1020
+ {
1021
+ autoFocus: true,
1022
+ type: "text",
1023
+ className: "qt-picker-input",
1024
+ placeholder: "search fields\u2026",
1025
+ value: search,
1026
+ onChange: (e) => setSearch(e.target.value),
1027
+ onKeyDown: (e) => {
1028
+ if (e.key === "Escape") onClose?.();
1029
+ if (e.key === "Enter" && firstEnabled) {
1030
+ onPick(firstEnabled);
1031
+ setSearch("");
1032
+ }
1033
+ },
1034
+ onBlur: () => setTimeout(() => onClose?.(), 150)
1035
+ }
1036
+ ),
1037
+ /* @__PURE__ */ jsx3("div", { className: "qt-picker-list", children: matched.length === 0 ? /* @__PURE__ */ jsx3("div", { className: "qt-picker-empty", children: "no matches" }) : grouped.map(([group, fs]) => /* @__PURE__ */ jsxs3("div", { className: "qt-picker-group", children: [
1038
+ /* @__PURE__ */ jsx3("div", { className: "qt-picker-group-label", children: group }),
1039
+ fs.map((f) => {
1040
+ const enabled = isEnabled(f, stats, gateOnDistinct);
1041
+ const s = stats?.[f.name];
1042
+ const aliasHit = aliasMatch(f, needle);
1043
+ return /* @__PURE__ */ jsxs3(
1044
+ "button",
1045
+ {
1046
+ type: "button",
1047
+ disabled: !enabled,
1048
+ className: enabled ? "qt-picker-item" : "qt-picker-item qt-picker-item--disabled",
1049
+ title: enabled ? f.name : "not useful as a filter (\u22641 distinct value)",
1050
+ onMouseDown: (e) => {
1051
+ if (!enabled) return;
1052
+ e.preventDefault();
1053
+ onPick(f);
1054
+ setSearch("");
1055
+ },
1056
+ children: [
1057
+ /* @__PURE__ */ jsxs3("span", { className: "qt-picker-row", children: [
1058
+ /* @__PURE__ */ jsx3("span", { className: "qt-picker-label", children: f.label }),
1059
+ /* @__PURE__ */ jsx3("span", { className: "qt-picker-name", children: f.name })
1060
+ ] }),
1061
+ aliasHit && /* @__PURE__ */ jsxs3("span", { className: "qt-picker-alias", children: [
1062
+ "matches: ",
1063
+ aliasHit
1064
+ ] }),
1065
+ s && /* @__PURE__ */ jsxs3("span", { className: "qt-picker-stats", children: [
1066
+ s.distinct != null && /* @__PURE__ */ jsxs3(Fragment3, { children: [
1067
+ s.distinct,
1068
+ " distinct"
1069
+ ] }),
1070
+ s.min != null && s.max != null && /* @__PURE__ */ jsxs3(Fragment3, { children: [
1071
+ " ",
1072
+ "\xB7 ",
1073
+ fmtStat(s.min),
1074
+ "\u2026",
1075
+ fmtStat(s.max)
1076
+ ] })
1077
+ ] })
1078
+ ]
1079
+ },
1080
+ f.name
1081
+ );
1082
+ })
1083
+ ] }, group)) })
1084
+ ] });
1085
+ }
1086
+ function isEnabled(f, stats, gate) {
1087
+ if (!gate) return true;
1088
+ const s = stats?.[f.name];
1089
+ if (!s || s.distinct == null) return true;
1090
+ return s.distinct > 1;
1091
+ }
1092
+ function aliasMatch(f, needle) {
1093
+ if (!needle) return void 0;
1094
+ if (f.label.toLowerCase().includes(needle) || f.name.toLowerCase().includes(needle)) return void 0;
1095
+ return fieldAliases(f).find((a) => a.toLowerCase().includes(needle));
1096
+ }
1097
+ function fieldAliases(f) {
1098
+ const aliases = /* @__PURE__ */ new Set();
1099
+ if (f.alias) aliases.add(f.alias);
1100
+ if (f.aliases) {
1101
+ for (const alias of f.aliases) aliases.add(alias);
1102
+ }
1103
+ return [...aliases];
1104
+ }
1105
+ function groupByGroup(fs) {
1106
+ const groups = /* @__PURE__ */ new Map();
1107
+ for (const f of fs) {
1108
+ const key = f.group ?? UNGROUPED;
1109
+ const arr = groups.get(key);
1110
+ if (arr) arr.push(f);
1111
+ else groups.set(key, [f]);
1112
+ }
1113
+ return [...groups.entries()];
1114
+ }
1115
+ function fmtStat(v) {
1116
+ if (typeof v === "number") return v.toLocaleString();
1117
+ const s = String(v);
1118
+ if (/^\d{4}-\d{2}-\d{2}T/.test(s)) return s.slice(0, 10);
1119
+ if (s.length > 18) return s.slice(0, 15) + "\u2026";
1120
+ return s;
1121
+ }
1122
+
1123
+ // src/SavedQueriesModal.tsx
1124
+ import { useEffect as useEffect3 } from "react";
1125
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1126
+ function SavedQueriesModal({ saved, onClose, onLoad }) {
1127
+ useEffect3(() => {
1128
+ function onKey(e) {
1129
+ if (e.key === "Escape") onClose();
1130
+ }
1131
+ window.addEventListener("keydown", onKey);
1132
+ return () => window.removeEventListener("keydown", onKey);
1133
+ }, [onClose]);
1134
+ return /* @__PURE__ */ jsx4("div", { className: "qt-modal-backdrop", onClick: onClose, children: /* @__PURE__ */ jsxs4("div", { className: "qt-modal", onClick: (e) => e.stopPropagation(), role: "dialog", "aria-label": "Saved queries", children: [
1135
+ /* @__PURE__ */ jsx4("h3", { className: "qt-modal-title", children: "Saved queries" }),
1136
+ saved.loading ? /* @__PURE__ */ jsx4("p", { className: "qt-muted", children: "Loading\u2026" }) : saved.items.length === 0 ? /* @__PURE__ */ jsx4("p", { className: "qt-muted", children: "None saved yet. Save the current query from the toolbar." }) : /* @__PURE__ */ jsx4("ul", { className: "qt-saved-list", children: saved.items.map((s) => {
1137
+ const filters = s.query.where.length;
1138
+ const isDefault = saved.defaultId === s.id;
1139
+ return /* @__PURE__ */ jsxs4("li", { className: "qt-saved-item", children: [
1140
+ /* @__PURE__ */ jsxs4(
1141
+ "button",
1142
+ {
1143
+ type: "button",
1144
+ className: "qt-saved-pick",
1145
+ onClick: () => {
1146
+ onLoad?.(s.id);
1147
+ saved.load(s.id);
1148
+ onClose();
1149
+ },
1150
+ children: [
1151
+ /* @__PURE__ */ jsx4("span", { className: "qt-saved-name", children: s.name }),
1152
+ /* @__PURE__ */ jsxs4("span", { className: "qt-saved-meta qt-muted", children: [
1153
+ isDefault ? "Default view \xB7 " : "",
1154
+ new Date(s.savedAt).toLocaleString(),
1155
+ " \xB7 ",
1156
+ filters,
1157
+ " filter",
1158
+ filters === 1 ? "" : "s"
1159
+ ] })
1160
+ ]
1161
+ }
1162
+ ),
1163
+ isDefault ? /* @__PURE__ */ jsx4(
1164
+ "button",
1165
+ {
1166
+ type: "button",
1167
+ className: "qt-btn qt-saved-default",
1168
+ title: "Clear default view",
1169
+ onClick: () => void saved.clearDefault(),
1170
+ children: "default"
1171
+ }
1172
+ ) : /* @__PURE__ */ jsx4(
1173
+ "button",
1174
+ {
1175
+ type: "button",
1176
+ className: "qt-btn qt-saved-default",
1177
+ title: "Use as default view",
1178
+ onClick: () => void saved.setDefault(s.id),
1179
+ children: "make default"
1180
+ }
1181
+ ),
1182
+ /* @__PURE__ */ jsx4(
1183
+ "button",
1184
+ {
1185
+ type: "button",
1186
+ className: "qt-btn qt-saved-delete",
1187
+ title: "Delete saved query",
1188
+ onClick: () => void saved.remove(s.id),
1189
+ children: "delete"
1190
+ }
1191
+ )
1192
+ ] }, s.id);
1193
+ }) }),
1194
+ /* @__PURE__ */ jsx4("div", { className: "qt-modal-footer", children: /* @__PURE__ */ jsx4("button", { type: "button", className: "qt-btn", onClick: onClose, children: "close" }) })
1195
+ ] }) });
1196
+ }
1197
+
1198
+ // src/QueryBuilder.tsx
1199
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1200
+ var cx3 = (...parts) => parts.filter((p) => Boolean(p)).join(" ");
1201
+ var MAX_AUTO_REFRESH_POLLS = 1e3;
1202
+ var DEFAULT_AUTO_REFRESH_FREQUENCY_MS = 2e4;
1203
+ var DEFAULT_AUTO_REFRESH_TURN_OFF_AFTER_MS = 5 * 6e4;
1204
+ var AUTO_REFRESH_FREQUENCIES = [
1205
+ { label: "Every 5s", value: 5e3 },
1206
+ { label: "Every 10s", value: 1e4 },
1207
+ { label: "Every 20s", value: DEFAULT_AUTO_REFRESH_FREQUENCY_MS },
1208
+ { label: "Every 30s", value: 3e4 },
1209
+ { label: "Every 1m", value: 6e4 },
1210
+ { label: "Every 2m", value: 2 * 6e4 },
1211
+ { label: "Every 5m", value: 5 * 6e4 },
1212
+ { label: "Every 10m", value: 10 * 6e4 }
1213
+ ];
1214
+ var AUTO_REFRESH_TURN_OFF_AFTER = [
1215
+ { label: "After 5m", value: DEFAULT_AUTO_REFRESH_TURN_OFF_AFTER_MS },
1216
+ { label: "After 15m", value: 15 * 6e4 },
1217
+ { label: "After 30m", value: 30 * 6e4 },
1218
+ { label: "After 1h", value: 60 * 6e4 },
1219
+ { label: "After 2h", value: 2 * 60 * 6e4 },
1220
+ { label: "After 4h", value: 4 * 60 * 6e4 },
1221
+ { label: "After 8h", value: 8 * 60 * 6e4 },
1222
+ { label: "After 12h", value: 12 * 60 * 6e4 },
1223
+ { label: "After 24h", value: 24 * 60 * 6e4 }
1224
+ ];
1225
+ function autoRefreshPolls(frequencyMs, turnOffAfterMs) {
1226
+ if (frequencyMs <= 0 || turnOffAfterMs <= 0) return Infinity;
1227
+ return Math.floor(turnOffAfterMs / frequencyMs);
1228
+ }
1229
+ function formatDuration2(ms) {
1230
+ const seconds = Math.round(ms / 1e3);
1231
+ if (seconds < 60) return `${seconds}s`;
1232
+ const minutes = Math.round(seconds / 60);
1233
+ if (minutes < 60) return `${minutes}m`;
1234
+ const hours = minutes / 60;
1235
+ return Number.isInteger(hours) ? `${hours}h` : `${minutes}m`;
1236
+ }
1237
+ function formatStatusTime(ms) {
1238
+ if (ms == null) return "never";
1239
+ return new Date(ms).toLocaleTimeString([], { hour: "numeric", minute: "2-digit", second: "2-digit" });
1240
+ }
1241
+ function useFieldHasNull(api, fieldName) {
1242
+ const [hasNull, setHasNull] = useState4(void 0);
1243
+ useEffect4(() => {
1244
+ if (!fieldName) return;
1245
+ let cancelled = false;
1246
+ void api.filterValues(fieldName, "").then((r) => {
1247
+ if (!cancelled && typeof r.hasNull === "boolean") {
1248
+ setHasNull(r.hasNull);
1249
+ }
1250
+ }).catch(() => {
1251
+ if (!cancelled) setHasNull(void 0);
1252
+ });
1253
+ return () => {
1254
+ cancelled = true;
1255
+ };
1256
+ }, [api, fieldName]);
1257
+ return hasNull;
1258
+ }
1259
+ function whereClauseAsText(clause, byName) {
1260
+ const field = byName.get(clause.field)?.label ?? clause.field;
1261
+ const op = clause.op.replace(/_/g, " ");
1262
+ if (NULLARY_OPS2.has(clause.op)) return `${field} ${op}`;
1263
+ const value = clause.value === "" ? "''" : clause.value;
1264
+ return `${field} ${op} ${value}`;
1265
+ }
1266
+ function orderByAsText(term, byName) {
1267
+ const field = byName.get(term.field)?.label ?? term.field;
1268
+ const nulls = term.nulls ? ` nulls ${term.nulls}` : "";
1269
+ return `${field} ${term.dir}${nulls}`;
1270
+ }
1271
+ function chipFieldWidthChars(text, fallback = 5, max = 30) {
1272
+ return Math.min(max, Math.max(fallback, (text || "").length + 1));
1273
+ }
1274
+ function chipFieldWidthForInput(text, fieldType, fallback = 5, padding = 0) {
1275
+ const cap = fieldType === "number" ? 10 : 30;
1276
+ return chipFieldWidthChars(text, fallback, cap) + padding;
1277
+ }
1278
+ function chipFieldWidthForSelect(text, fallback = 3, max = 30) {
1279
+ return chipFieldWidthChars(text, fallback, max) + 2;
1280
+ }
1281
+ function buildCollapsedSummary(where, orderBy, byName) {
1282
+ const whereText = where.length === 0 ? "all rows" : where.map((c) => whereClauseAsText(c, byName)).join(" and ");
1283
+ const orderText = orderBy.length === 0 ? "(default)" : orderBy.map((term) => orderByAsText(term, byName)).join(", ");
1284
+ return `WHERE ${whereText} ORDER BY ${orderText}`;
1285
+ }
1286
+ function whereClauseSummaryText(where, byName) {
1287
+ return where.length === 0 ? "all rows" : where.map((c) => whereClauseAsText(c, byName)).join(" and ");
1288
+ }
1289
+ function orderBySummaryText(orderBy, byName) {
1290
+ return orderBy.length === 0 ? "(default)" : orderBy.map((term) => orderByAsText(term, byName)).join(", ");
1291
+ }
1292
+ function QueryBuilder({
1293
+ api,
1294
+ fields,
1295
+ total,
1296
+ running,
1297
+ classNames,
1298
+ collapsed,
1299
+ onCollapsedChange,
1300
+ defaultCollapsed = false
1301
+ }) {
1302
+ const UNSAVED_QUERY_EDIT_ID = "__qt-unsaved-query__";
1303
+ const [showSaved, setShowSaved] = useState4(false);
1304
+ const [showAutoRefresh, setShowAutoRefresh] = useState4(false);
1305
+ const [autoRefreshFrequencyMs, setAutoRefreshFrequencyMs] = useState4(DEFAULT_AUTO_REFRESH_FREQUENCY_MS);
1306
+ const [autoRefreshTurnOffAfterMs, setAutoRefreshTurnOffAfterMs] = useState4(DEFAULT_AUTO_REFRESH_TURN_OFF_AFTER_MS);
1307
+ const [editingSavedId, setEditingSavedId] = useState4(null);
1308
+ const [editingSavedName, setEditingSavedName] = useState4("");
1309
+ const [lastSavedId, setLastSavedId] = useState4(null);
1310
+ const [collapsedFallback, setCollapsedFallback] = useState4(defaultCollapsed);
1311
+ const collapsedControlled = typeof collapsed !== "undefined";
1312
+ const isCollapsed = collapsedControlled ? collapsed : collapsedFallback;
1313
+ const setCollapsed = (next) => {
1314
+ if (collapsedControlled) {
1315
+ onCollapsedChange?.(next);
1316
+ return;
1317
+ }
1318
+ setCollapsedFallback(next);
1319
+ };
1320
+ const [lastUpdatedAt, setLastUpdatedAt] = useState4(null);
1321
+ const [lastFailureAt, setLastFailureAt] = useState4(null);
1322
+ const byName = useMemo3(() => new Map(fields.map((f) => [f.name, f])), [fields]);
1323
+ const activeSavedQuery = useMemo3(
1324
+ () => api.saved.items.find((item) => queriesEqual(item.query, api.query)),
1325
+ [api.query, api.saved.items]
1326
+ );
1327
+ const lastSavedQuery = useMemo3(
1328
+ () => api.saved.items.find((item) => item.id === lastSavedId) ?? null,
1329
+ [api.saved.items, lastSavedId]
1330
+ );
1331
+ const isSavedQuery = activeSavedQuery != null;
1332
+ const isEditingUnsaved = editingSavedId === UNSAVED_QUERY_EDIT_ID;
1333
+ const bodyId = useId();
1334
+ const isEditingSaved = editingSavedId != null && activeSavedQuery?.id === editingSavedId;
1335
+ const showUpdateButton = Boolean(lastSavedQuery) && !activeSavedQuery;
1336
+ const collapsedSummary = useMemo3(
1337
+ () => buildCollapsedSummary(api.query.where, api.query.orderBy, byName),
1338
+ [api.query.where, api.query.orderBy, byName]
1339
+ );
1340
+ const collapsedWhereText = useMemo3(
1341
+ () => whereClauseSummaryText(api.query.where, byName),
1342
+ [api.query.where, byName]
1343
+ );
1344
+ const collapsedOrderText = useMemo3(
1345
+ () => orderBySummaryText(api.query.orderBy, byName),
1346
+ [api.query.orderBy, byName]
1347
+ );
1348
+ const autoRefreshStatus = api.autoRefresh.status;
1349
+ const selectedAutoRefreshPolls = autoRefreshPolls(autoRefreshFrequencyMs, autoRefreshTurnOffAfterMs);
1350
+ const canSubmitAutoRefresh = selectedAutoRefreshPolls >= 1 && selectedAutoRefreshPolls <= MAX_AUTO_REFRESH_POLLS;
1351
+ const autoRefreshButtonText = autoRefreshStatus ? `\u27F3 Auto-Update ${formatDuration2(autoRefreshStatus.frequencyMs)}` : "\u27F3 Auto-Update";
1352
+ const rowSummary = `${api.rows.length} of ${total ?? "?"}`;
1353
+ const lastUpdatedText = formatStatusTime(lastUpdatedAt);
1354
+ const failureText = api.error ? `Error: ${api.error.message}` : lastFailureAt ? `Last failure at ${formatStatusTime(lastFailureAt)}` : null;
1355
+ const isBusy = Boolean(running) || api.loading;
1356
+ const lastRowsRef = useRef2(null);
1357
+ const lastTotalRef = useRef2(null);
1358
+ const previousLoadingRef = useRef2(api.loading);
1359
+ const autoRefreshRef = useRef2(null);
1360
+ const autoRefreshPopoverRef = useRef2(null);
1361
+ const [autoRefreshPopoverAlignRight, setAutoRefreshPopoverAlignRight] = useState4(false);
1362
+ useEffect4(() => {
1363
+ if (activeSavedQuery) {
1364
+ setLastSavedId(activeSavedQuery.id);
1365
+ if (!isEditingSaved) {
1366
+ setEditingSavedName(activeSavedQuery.name);
1367
+ setEditingSavedId(null);
1368
+ }
1369
+ return;
1370
+ }
1371
+ if (!isEditingUnsaved) {
1372
+ setEditingSavedId(null);
1373
+ setEditingSavedName("");
1374
+ }
1375
+ }, [activeSavedQuery, isEditingSaved, isEditingUnsaved]);
1376
+ useEffect4(() => {
1377
+ if (!lastSavedId || api.saved.items.some((item) => item.id === lastSavedId)) return;
1378
+ setLastSavedId(null);
1379
+ }, [api.saved.items, lastSavedId]);
1380
+ useEffect4(() => {
1381
+ const hasData = api.rows.length > 0 || total != null;
1382
+ const rowsChanged = lastRowsRef.current !== api.rows;
1383
+ const totalChanged = lastTotalRef.current !== total;
1384
+ const loadingDone = !api.loading;
1385
+ if (loadingDone && api.error) {
1386
+ setLastFailureAt(Date.now());
1387
+ lastRowsRef.current = api.rows;
1388
+ lastTotalRef.current = total;
1389
+ previousLoadingRef.current = api.loading;
1390
+ return;
1391
+ }
1392
+ if (loadingDone && (previousLoadingRef.current || rowsChanged || totalChanged || lastUpdatedAt == null) && hasData) {
1393
+ setLastUpdatedAt(Date.now());
1394
+ setLastFailureAt(null);
1395
+ lastRowsRef.current = api.rows;
1396
+ lastTotalRef.current = total;
1397
+ }
1398
+ previousLoadingRef.current = api.loading;
1399
+ }, [api.error, api.loading, api.rows, total, lastUpdatedAt]);
1400
+ function saveFailed(error) {
1401
+ const message = error instanceof Error ? error.message : "Failed to save query.";
1402
+ if (typeof window !== "undefined") window.alert(message);
1403
+ }
1404
+ function beginEditSavedName() {
1405
+ if (activeSavedQuery) {
1406
+ setEditingSavedId(activeSavedQuery.id);
1407
+ setEditingSavedName(activeSavedQuery.name);
1408
+ return;
1409
+ }
1410
+ if (isEditingUnsaved) return;
1411
+ setEditingSavedId(UNSAVED_QUERY_EDIT_ID);
1412
+ setEditingSavedName((next) => next ? next : "Custom Query");
1413
+ }
1414
+ function cancelNameEdit() {
1415
+ setEditingSavedId(null);
1416
+ setEditingSavedName("");
1417
+ }
1418
+ async function persistSavedQuery(name) {
1419
+ const trimmed = name.trim();
1420
+ if (!trimmed) return null;
1421
+ try {
1422
+ const saved = await api.saved.save(trimmed);
1423
+ setLastSavedId(saved.id);
1424
+ return saved.id;
1425
+ } catch (error) {
1426
+ saveFailed(error);
1427
+ return null;
1428
+ }
1429
+ }
1430
+ async function commitSavedNameEdit() {
1431
+ if (!isEditingSaved || !activeSavedQuery) return;
1432
+ const trimmed = editingSavedName.trim();
1433
+ if (!trimmed || trimmed === activeSavedQuery.name) {
1434
+ cancelNameEdit();
1435
+ return;
1436
+ }
1437
+ const savedId = await persistSavedQuery(trimmed);
1438
+ if (!savedId) return;
1439
+ await api.saved.remove(activeSavedQuery.id);
1440
+ cancelNameEdit();
1441
+ }
1442
+ async function commitUnsavedNameEdit() {
1443
+ if (!isEditingUnsaved) return;
1444
+ const savedId = await persistSavedQuery(editingSavedName);
1445
+ if (!savedId) {
1446
+ cancelNameEdit();
1447
+ return;
1448
+ }
1449
+ cancelNameEdit();
1450
+ }
1451
+ async function updateLastSavedQuery() {
1452
+ if (!lastSavedQuery) return;
1453
+ const wasDefault = api.saved.defaultId === lastSavedQuery.id;
1454
+ const trimmedName = lastSavedQuery.name.trim();
1455
+ if (!trimmedName) return;
1456
+ try {
1457
+ await api.saved.remove(lastSavedQuery.id);
1458
+ const saved = await api.saved.save(trimmedName);
1459
+ if (wasDefault) await api.saved.setDefault(saved.id);
1460
+ setLastSavedId(saved.id);
1461
+ } catch (error) {
1462
+ saveFailed(error);
1463
+ }
1464
+ }
1465
+ function commitNameEditor() {
1466
+ if (isEditingSaved) {
1467
+ void commitSavedNameEdit();
1468
+ return;
1469
+ }
1470
+ if (isEditingUnsaved) {
1471
+ void commitUnsavedNameEdit();
1472
+ return;
1473
+ }
1474
+ }
1475
+ useEffect4(() => {
1476
+ if (isCollapsed) setShowSaved(false);
1477
+ }, [isCollapsed]);
1478
+ useEffect4(() => {
1479
+ if (!showAutoRefresh || !autoRefreshStatus) return;
1480
+ setAutoRefreshFrequencyMs(autoRefreshStatus.frequencyMs);
1481
+ setAutoRefreshTurnOffAfterMs(autoRefreshStatus.turnOffAfterMs);
1482
+ }, [showAutoRefresh, autoRefreshStatus]);
1483
+ useEffect4(() => {
1484
+ if (!showAutoRefresh) return;
1485
+ function onDocumentClick(event) {
1486
+ const wrapper = autoRefreshRef.current;
1487
+ const target = event.target;
1488
+ if (!wrapper || !target) return;
1489
+ if (!wrapper.contains(target)) setShowAutoRefresh(false);
1490
+ }
1491
+ function onDocumentKeyDown(event) {
1492
+ if (event.key === "Escape") setShowAutoRefresh(false);
1493
+ }
1494
+ function calculateAutoRefreshPosition() {
1495
+ const popover = autoRefreshPopoverRef.current;
1496
+ const wrapper = autoRefreshRef.current;
1497
+ if (!popover || !wrapper) return;
1498
+ const wrapperRect = wrapper.getBoundingClientRect();
1499
+ const popWidth = popover.offsetWidth;
1500
+ const viewportRightInset = 8;
1501
+ setAutoRefreshPopoverAlignRight(wrapperRect.right + popWidth > window.innerWidth - viewportRightInset);
1502
+ }
1503
+ calculateAutoRefreshPosition();
1504
+ const t = setTimeout(() => window.addEventListener("click", onDocumentClick), 0);
1505
+ window.addEventListener("keydown", onDocumentKeyDown);
1506
+ window.addEventListener("resize", calculateAutoRefreshPosition);
1507
+ return () => {
1508
+ clearTimeout(t);
1509
+ window.removeEventListener("click", onDocumentClick);
1510
+ window.removeEventListener("keydown", onDocumentKeyDown);
1511
+ window.removeEventListener("resize", calculateAutoRefreshPosition);
1512
+ };
1513
+ }, [showAutoRefresh]);
1514
+ function submitAutoRefresh() {
1515
+ if (!canSubmitAutoRefresh) return;
1516
+ try {
1517
+ api.autoRefresh.start({
1518
+ frequencyMs: autoRefreshFrequencyMs,
1519
+ turnOffAfterMs: autoRefreshTurnOffAfterMs
1520
+ });
1521
+ setShowAutoRefresh(false);
1522
+ } catch (error) {
1523
+ saveFailed(error);
1524
+ }
1525
+ }
1526
+ return /* @__PURE__ */ jsxs5("div", { className: cx3("qt-qb", classNames?.root), children: [
1527
+ /* @__PURE__ */ jsxs5("div", { className: "qt-qb-bar qt-qb-state-bar qt-qb-row", children: [
1528
+ /* @__PURE__ */ jsxs5("span", { className: "qt-qb-state-main", children: [
1529
+ /* @__PURE__ */ jsxs5("span", { className: "qt-qb-saved", children: [
1530
+ /* @__PURE__ */ jsx5(
1531
+ "button",
1532
+ {
1533
+ type: "button",
1534
+ className: cx3("qt-qb-saved-star", isSavedQuery ? "qt-qb-saved-star--saved" : "qt-qb-saved-star--unsaved"),
1535
+ title: isSavedQuery ? "Rename saved query" : "Save query",
1536
+ disabled: isBusy,
1537
+ onClick: beginEditSavedName,
1538
+ children: isSavedQuery ? "\u2605" : "\u2606"
1539
+ }
1540
+ ),
1541
+ isEditingSaved || isEditingUnsaved ? /* @__PURE__ */ jsxs5("span", { className: "qt-qb-saved-input-wrap", children: [
1542
+ /* @__PURE__ */ jsx5(
1543
+ "input",
1544
+ {
1545
+ type: "text",
1546
+ className: "qt-qb-saved-input",
1547
+ value: editingSavedName,
1548
+ disabled: isBusy,
1549
+ autoFocus: true,
1550
+ onChange: (e) => setEditingSavedName(e.target.value),
1551
+ onBlur: commitNameEditor,
1552
+ onKeyDown: (e) => {
1553
+ if (e.key === "Enter") {
1554
+ e.preventDefault();
1555
+ commitNameEditor();
1556
+ }
1557
+ if (e.key === "Escape") {
1558
+ e.preventDefault();
1559
+ cancelNameEdit();
1560
+ }
1561
+ },
1562
+ onClick: (e) => e.stopPropagation()
1563
+ }
1564
+ ),
1565
+ /* @__PURE__ */ jsx5(
1566
+ "button",
1567
+ {
1568
+ type: "button",
1569
+ className: "qt-qb-saved-cancel",
1570
+ title: "Do not save",
1571
+ onMouseDown: (e) => {
1572
+ e.preventDefault();
1573
+ cancelNameEdit();
1574
+ },
1575
+ onClick: (e) => e.stopPropagation(),
1576
+ children: "\u2715"
1577
+ }
1578
+ )
1579
+ ] }) : /* @__PURE__ */ jsx5(
1580
+ "button",
1581
+ {
1582
+ type: "button",
1583
+ className: cx3("qt-qb-saved-name", !activeSavedQuery && "qt-qb-saved-name--custom"),
1584
+ onClick: beginEditSavedName,
1585
+ children: activeSavedQuery ? activeSavedQuery.name : "Custom Query"
1586
+ }
1587
+ )
1588
+ ] }),
1589
+ isCollapsed ? /* @__PURE__ */ jsxs5("span", { className: "qt-qb-summary qt-truncate", title: collapsedSummary, children: [
1590
+ /* @__PURE__ */ jsx5("span", { className: "qt-qb-summary-kw", children: "WHERE" }),
1591
+ " ",
1592
+ /* @__PURE__ */ jsx5("strong", { children: collapsedWhereText }),
1593
+ " ",
1594
+ /* @__PURE__ */ jsx5("span", { className: "qt-qb-summary-kw", children: "ORDER BY" }),
1595
+ " ",
1596
+ /* @__PURE__ */ jsx5("strong", { children: collapsedOrderText })
1597
+ ] }) : null
1598
+ ] }),
1599
+ showUpdateButton ? /* @__PURE__ */ jsx5(
1600
+ "button",
1601
+ {
1602
+ type: "button",
1603
+ className: cx3("qt-btn", classNames?.button),
1604
+ onClick: () => void updateLastSavedQuery(),
1605
+ disabled: isBusy,
1606
+ children: `Update Query '${lastSavedQuery?.name ?? "query"}'`
1607
+ }
1608
+ ) : null,
1609
+ /* @__PURE__ */ jsxs5("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: () => setShowSaved(true), children: [
1610
+ "Saved",
1611
+ api.saved.items.length > 0 ? ` (${api.saved.items.length})` : ""
1612
+ ] }),
1613
+ /* @__PURE__ */ jsxs5(
1614
+ "button",
1615
+ {
1616
+ type: "button",
1617
+ className: cx3("qt-btn", "qt-qb-collapse-btn", classNames?.button),
1618
+ onClick: () => setCollapsed(!isCollapsed),
1619
+ "aria-expanded": !isCollapsed,
1620
+ "aria-controls": bodyId,
1621
+ title: isCollapsed ? "Expand query builder" : "Collapse query builder",
1622
+ children: [
1623
+ /* @__PURE__ */ jsx5(
1624
+ "span",
1625
+ {
1626
+ className: cx3("qt-qb-collapse-caret", isCollapsed && "qt-qb-collapse-caret--collapsed"),
1627
+ "aria-hidden": "true",
1628
+ children: "\u25BE"
1629
+ }
1630
+ ),
1631
+ /* @__PURE__ */ jsx5("span", { className: "qt-sr-only", children: isCollapsed ? "Expand query builder" : "Collapse query builder" })
1632
+ ]
1633
+ }
1634
+ )
1635
+ ] }),
1636
+ !isCollapsed ? /* @__PURE__ */ jsxs5("div", { className: "qt-qb-bar qt-qb-editor-bar", children: [
1637
+ /* @__PURE__ */ jsxs5("div", { id: bodyId, className: "qt-qb-body", children: [
1638
+ /* @__PURE__ */ jsx5(SelectRow, { api, fields, classNames, disabled: isBusy }),
1639
+ /* @__PURE__ */ jsx5(WhereRow, { api, byName, fields, classNames, disabled: isBusy }),
1640
+ /* @__PURE__ */ jsx5(OrderRow, { api, fields, classNames, disabled: isBusy }),
1641
+ /* @__PURE__ */ jsx5(WindowRow, { api, classNames, disabled: isBusy }),
1642
+ /* @__PURE__ */ jsx5(MetricsRow, { api, fields, classNames, disabled: isBusy })
1643
+ ] }),
1644
+ /* @__PURE__ */ jsxs5("div", { className: "qt-qb-editor-stack", children: [
1645
+ /* @__PURE__ */ jsx5("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.undo, disabled: !api.canUndo || isBusy, children: "\u21B6 Undo" }),
1646
+ /* @__PURE__ */ jsx5("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.redo, disabled: !api.canRedo || isBusy, children: "\u21B7 Redo" }),
1647
+ /* @__PURE__ */ jsx5("button", { type: "button", className: cx3("qt-btn", classNames?.button), onClick: api.resetAll, disabled: isBusy, children: "Reset All" })
1648
+ ] })
1649
+ ] }) : null,
1650
+ /* @__PURE__ */ jsxs5("div", { className: "qt-qb-bar qt-qb-run-bar qt-qb-row", children: [
1651
+ /* @__PURE__ */ jsx5("span", { className: "qt-qb-run-metric qt-qb-run-metric--strong", children: api.loading ? "loading\u2026" : `${rowSummary} rows` }),
1652
+ /* @__PURE__ */ jsxs5("span", { className: "qt-qb-run-metric", children: [
1653
+ "Last updated ",
1654
+ lastUpdatedText
1655
+ ] }),
1656
+ failureText ? /* @__PURE__ */ jsx5("span", { className: "qt-qb-run-error", children: failureText }) : null,
1657
+ /* @__PURE__ */ jsxs5("span", { className: "qt-qb-run-actions", children: [
1658
+ autoRefreshStatus ? /* @__PURE__ */ jsx5("span", { className: "qt-qb-auto-status", children: `Every ${formatDuration2(autoRefreshStatus.frequencyMs)}; clear after ${formatDuration2(
1659
+ autoRefreshStatus.turnOffAfterMs
1660
+ )} (${autoRefreshStatus.pollCount} polls run)` }) : null,
1661
+ /* @__PURE__ */ jsxs5("span", { className: "qt-auto-refresh", ref: autoRefreshRef, children: [
1662
+ /* @__PURE__ */ jsx5(
1663
+ "button",
1664
+ {
1665
+ type: "button",
1666
+ className: cx3("qt-btn", Boolean(autoRefreshStatus) && "qt-btn--active", classNames?.button),
1667
+ onClick: () => setShowAutoRefresh((next) => !next),
1668
+ "aria-expanded": showAutoRefresh,
1669
+ title: autoRefreshStatus ? "View or clear auto-update" : "Configure auto-update",
1670
+ children: autoRefreshButtonText
1671
+ }
1672
+ ),
1673
+ showAutoRefresh ? /* @__PURE__ */ jsxs5(
1674
+ "span",
1675
+ {
1676
+ ref: autoRefreshPopoverRef,
1677
+ className: cx3("qt-auto-refresh-popover", autoRefreshPopoverAlignRight && "qt-auto-refresh-popover--align-right"),
1678
+ role: "dialog",
1679
+ "aria-label": "Auto-refresh settings",
1680
+ children: [
1681
+ /* @__PURE__ */ jsxs5("label", { className: "qt-auto-refresh-field", children: [
1682
+ /* @__PURE__ */ jsx5("span", { children: "Frequency" }),
1683
+ /* @__PURE__ */ jsx5(
1684
+ "select",
1685
+ {
1686
+ className: "qt-auto-refresh-select",
1687
+ value: autoRefreshFrequencyMs,
1688
+ onChange: (e) => setAutoRefreshFrequencyMs(Number(e.target.value)),
1689
+ children: AUTO_REFRESH_FREQUENCIES.map((option) => /* @__PURE__ */ jsx5("option", { value: option.value, children: option.label }, option.value))
1690
+ }
1691
+ )
1692
+ ] }),
1693
+ /* @__PURE__ */ jsxs5("label", { className: "qt-auto-refresh-field", children: [
1694
+ /* @__PURE__ */ jsx5("span", { children: "Turn off after" }),
1695
+ /* @__PURE__ */ jsx5(
1696
+ "select",
1697
+ {
1698
+ className: "qt-auto-refresh-select",
1699
+ value: autoRefreshTurnOffAfterMs,
1700
+ onChange: (e) => setAutoRefreshTurnOffAfterMs(Number(e.target.value)),
1701
+ children: AUTO_REFRESH_TURN_OFF_AFTER.map((option) => /* @__PURE__ */ jsx5(
1702
+ "option",
1703
+ {
1704
+ value: option.value,
1705
+ disabled: autoRefreshPolls(autoRefreshFrequencyMs, option.value) < 1 || autoRefreshPolls(autoRefreshFrequencyMs, option.value) > MAX_AUTO_REFRESH_POLLS,
1706
+ children: option.label
1707
+ },
1708
+ option.value
1709
+ ))
1710
+ }
1711
+ )
1712
+ ] }),
1713
+ /* @__PURE__ */ jsx5("span", { className: cx3("qt-auto-refresh-status", !canSubmitAutoRefresh && "qt-auto-refresh-status--error"), children: canSubmitAutoRefresh ? `${selectedAutoRefreshPolls} polls maximum` : `Choose between 1 and 1000 polls` }),
1714
+ autoRefreshStatus ? /* @__PURE__ */ jsxs5("span", { className: "qt-auto-refresh-status", children: [
1715
+ "Active: every ",
1716
+ formatDuration2(autoRefreshStatus.frequencyMs),
1717
+ ", clears after",
1718
+ " ",
1719
+ formatDuration2(autoRefreshStatus.turnOffAfterMs),
1720
+ ". ",
1721
+ autoRefreshStatus.pollCount,
1722
+ " polls run."
1723
+ ] }) : null,
1724
+ /* @__PURE__ */ jsxs5("span", { className: "qt-auto-refresh-actions", children: [
1725
+ /* @__PURE__ */ jsx5(
1726
+ "button",
1727
+ {
1728
+ type: "button",
1729
+ className: cx3("qt-btn", classNames?.button),
1730
+ onClick: submitAutoRefresh,
1731
+ disabled: !canSubmitAutoRefresh,
1732
+ children: autoRefreshStatus ? "Update" : "Start"
1733
+ }
1734
+ ),
1735
+ autoRefreshStatus ? /* @__PURE__ */ jsx5(
1736
+ "button",
1737
+ {
1738
+ type: "button",
1739
+ className: cx3("qt-btn", classNames?.button),
1740
+ onClick: () => {
1741
+ api.autoRefresh.stop();
1742
+ setShowAutoRefresh(false);
1743
+ },
1744
+ children: "Clear"
1745
+ }
1746
+ ) : null
1747
+ ] })
1748
+ ]
1749
+ }
1750
+ ) : null
1751
+ ] }),
1752
+ /* @__PURE__ */ jsx5(
1753
+ "button",
1754
+ {
1755
+ type: "button",
1756
+ className: cx3("qt-btn", "qt-btn--primary", classNames?.button),
1757
+ onClick: api.refresh,
1758
+ disabled: isBusy,
1759
+ children: "Run Now"
1760
+ }
1761
+ )
1762
+ ] })
1763
+ ] }),
1764
+ !isCollapsed && showSaved ? /* @__PURE__ */ jsx5(SavedQueriesModal, { saved: api.saved, onClose: () => setShowSaved(false), onLoad: setLastSavedId }) : null
1765
+ ] });
1766
+ }
1767
+ function SelectRow({
1768
+ api,
1769
+ fields,
1770
+ classNames,
1771
+ disabled
1772
+ }) {
1773
+ const { select, columnDrag } = api;
1774
+ const [adding, setAdding] = useState4(false);
1775
+ const fieldLabelByName = useMemo3(() => new Map(select.fields.map((f) => [f.name, f.label])), [select.fields]);
1776
+ function reorderByIndex(from, overIndex) {
1777
+ select.move(from, overIndex);
1778
+ }
1779
+ const fieldNames = select.visible.map((c) => c.field);
1780
+ const dragSource = columnDrag.source;
1781
+ const rendered = columnDrag.preview(fieldNames);
1782
+ return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row", children: [
1783
+ /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "select" }),
1784
+ rendered.map((name) => {
1785
+ const label = fieldLabelByName.get(name) ?? name;
1786
+ return /* @__PURE__ */ jsxs5(
1787
+ "span",
1788
+ {
1789
+ className: cx3(
1790
+ "qt-chip",
1791
+ "qt-chip--col",
1792
+ dragSource === name && "qt-chip--dragging",
1793
+ classNames?.columnChip ?? classNames?.chip
1794
+ ),
1795
+ draggable: !disabled,
1796
+ onDragStart: (e) => {
1797
+ columnDrag.start(name, fieldNames.indexOf(name));
1798
+ e.dataTransfer.effectAllowed = "move";
1799
+ e.dataTransfer.setData("text/plain", name);
1800
+ },
1801
+ onDragOver: (e) => {
1802
+ if (!columnDrag.source) return;
1803
+ e.preventDefault();
1804
+ e.dataTransfer.dropEffect = "move";
1805
+ if (columnDrag.source !== name) {
1806
+ const withoutDragged = fieldNames.filter((n) => n !== columnDrag.source);
1807
+ let next = withoutDragged.indexOf(name);
1808
+ if (next >= 0) {
1809
+ const rect = e.currentTarget.getBoundingClientRect();
1810
+ if (e.clientX > rect.left + rect.width / 2) next += 1;
1811
+ columnDrag.over(next);
1812
+ }
1813
+ }
1814
+ },
1815
+ onDrop: (e) => {
1816
+ e.preventDefault();
1817
+ e.dataTransfer.dropEffect = "move";
1818
+ const from = columnDrag.source || e.dataTransfer.getData("text/plain");
1819
+ if (from && columnDrag.overIndex != null) reorderByIndex(from, columnDrag.overIndex);
1820
+ columnDrag.end();
1821
+ },
1822
+ onDragEnd: () => columnDrag.end(),
1823
+ title: "drag to reorder",
1824
+ children: [
1825
+ /* @__PURE__ */ jsx5("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
1826
+ /* @__PURE__ */ jsx5("span", { className: "qt-chip-field", children: label }),
1827
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: () => select.hide(name), disabled, children: "\u2715" })
1828
+ ]
1829
+ },
1830
+ name
1831
+ );
1832
+ }),
1833
+ adding ? /* @__PURE__ */ jsx5(
1834
+ FieldPicker,
1835
+ {
1836
+ fields: select.hidden.filter(isSelectable),
1837
+ onPick: (f) => {
1838
+ select.show(f.name);
1839
+ setAdding(false);
1840
+ },
1841
+ onClose: () => setAdding(false)
1842
+ }
1843
+ ) : /* @__PURE__ */ jsx5(
1844
+ "button",
1845
+ {
1846
+ type: "button",
1847
+ className: "qt-add",
1848
+ onClick: () => setAdding(true),
1849
+ disabled: disabled || select.hidden.length === 0,
1850
+ children: "+ add column"
1851
+ }
1852
+ ),
1853
+ api.query.select.length > 0 && /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-link-btn", onClick: select.reset, disabled, title: "Reset to default columns", children: "reset" })
1854
+ ] });
1855
+ }
1856
+ function WhereRow({
1857
+ api,
1858
+ byName,
1859
+ fields,
1860
+ classNames,
1861
+ disabled
1862
+ }) {
1863
+ const [adding, setAdding] = useState4(false);
1864
+ function addClause(f) {
1865
+ const op = opsForField2(f)[0] ?? "=";
1866
+ api.addFilter({ field: f.name, op, value: "" });
1867
+ setAdding(false);
1868
+ }
1869
+ const groupedWhere = useMemo3(() => {
1870
+ const groups = /* @__PURE__ */ new Map();
1871
+ const order = [];
1872
+ api.query.where.forEach((c, i) => {
1873
+ if (!groups.has(c.field)) order.push(c.field);
1874
+ const group = groups.get(c.field) ?? { field: c.field, clauses: [] };
1875
+ group.clauses.push({ clause: c, index: i });
1876
+ groups.set(c.field, group);
1877
+ });
1878
+ return order.map((fieldName) => groups.get(fieldName));
1879
+ }, [api.query.where]);
1880
+ return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row", children: [
1881
+ /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "where" }),
1882
+ api.query.where.length === 0 && !adding && /* @__PURE__ */ jsx5("span", { className: "qt-qb-hint", children: "all rows" }),
1883
+ groupedWhere.map((g) => /* @__PURE__ */ jsx5(
1884
+ ClauseChip,
1885
+ {
1886
+ api,
1887
+ field: byName.get(g.field),
1888
+ clauses: g.clauses,
1889
+ classNames,
1890
+ disabled,
1891
+ onChange: (index, next) => api.updateFilter(index, next),
1892
+ onRemove: (index) => api.removeFilter(index)
1893
+ },
1894
+ g.field
1895
+ )),
1896
+ adding ? /* @__PURE__ */ jsx5(
1897
+ FieldPicker,
1898
+ {
1899
+ fields: fields.filter(isFilterable),
1900
+ gateOnDistinct: true,
1901
+ onPick: addClause,
1902
+ onClose: () => setAdding(false)
1903
+ }
1904
+ ) : /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-add", onClick: () => setAdding(true), disabled, children: "+ add filter" }),
1905
+ api.query.where.length > 0 && /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-link-btn", onClick: api.clearFilters, disabled, title: "Reset filters", children: "reset" })
1906
+ ] });
1907
+ }
1908
+ function ClauseChip({
1909
+ api,
1910
+ field,
1911
+ clauses,
1912
+ classNames,
1913
+ disabled,
1914
+ onChange,
1915
+ onRemove
1916
+ }) {
1917
+ const labelField = field?.label;
1918
+ const fieldName = clauses[0]?.clause.field;
1919
+ const fieldHasNull = useFieldHasNull(api, field?.name);
1920
+ return /* @__PURE__ */ jsxs5("span", { className: cx3("qt-chip", "qt-chip--where", classNames?.chip), children: [
1921
+ /* @__PURE__ */ jsx5("span", { className: "qt-chip-field", children: labelField ?? fieldName }),
1922
+ /* @__PURE__ */ jsx5("span", { className: "qt-chip-where-list", children: clauses.map((entry, idx) => {
1923
+ const clause = entry.clause;
1924
+ const baseOps = field ? opsForField2(field) : [clause.op];
1925
+ const filteredBaseOps = fieldHasNull === false ? baseOps.filter((op) => !NULLARY_OPS2.has(op)) : baseOps;
1926
+ const clauseOps = filteredBaseOps.includes(clause.op) ? filteredBaseOps : [...filteredBaseOps, clause.op];
1927
+ const needsValue = !NULLARY_OPS2.has(clause.op);
1928
+ return /* @__PURE__ */ jsxs5("span", { className: "qt-chip-where-clause", children: [
1929
+ idx > 0 && /* @__PURE__ */ jsx5("span", { className: "qt-chip-and", children: "and" }),
1930
+ /* @__PURE__ */ jsx5(
1931
+ "select",
1932
+ {
1933
+ className: cx3("qt-chip-op", classNames?.select),
1934
+ value: clause.op,
1935
+ disabled,
1936
+ style: { width: `${chipFieldWidthForSelect(clause.op, 3)}ch` },
1937
+ onChange: (e) => onChange(entry.index, { ...clause, op: e.target.value }),
1938
+ children: clauseOps.map((op) => /* @__PURE__ */ jsx5("option", { value: op, children: op }, op))
1939
+ }
1940
+ ),
1941
+ needsValue && /* @__PURE__ */ jsx5(
1942
+ ValueInput,
1943
+ {
1944
+ api,
1945
+ field,
1946
+ value: clause.value,
1947
+ classNames,
1948
+ onChange: (v) => onChange(entry.index, { ...clause, value: v })
1949
+ }
1950
+ ),
1951
+ /* @__PURE__ */ jsx5(
1952
+ "button",
1953
+ {
1954
+ type: "button",
1955
+ className: "qt-chip-x",
1956
+ onClick: () => onRemove(entry.index),
1957
+ disabled,
1958
+ children: "\u2715"
1959
+ }
1960
+ )
1961
+ ] }, entry.index);
1962
+ }) })
1963
+ ] });
1964
+ }
1965
+ function ValueInput({
1966
+ api,
1967
+ field,
1968
+ value,
1969
+ classNames,
1970
+ onChange
1971
+ }) {
1972
+ const strategy = field ? filterValuesFor(field) : { source: "freeform" };
1973
+ if (strategy.source === "static") {
1974
+ const selectedText = value || "\u2014";
1975
+ const widthChars = chipFieldWidthForSelect(selectedText, 4);
1976
+ return /* @__PURE__ */ jsxs5(
1977
+ "select",
1978
+ {
1979
+ className: cx3("qt-chip-val", classNames?.select),
1980
+ value,
1981
+ style: { width: `${widthChars}ch` },
1982
+ onChange: (e) => onChange(e.target.value),
1983
+ children: [
1984
+ /* @__PURE__ */ jsx5("option", { value: "", children: "\u2014" }),
1985
+ strategy.options.map((o) => /* @__PURE__ */ jsx5("option", { value: o, children: o }, o))
1986
+ ]
1987
+ }
1988
+ );
1989
+ }
1990
+ if (strategy.source === "freeform" || !field) {
1991
+ const widthChars = chipFieldWidthForInput(value || "value", field?.type, 5, 4);
1992
+ return /* @__PURE__ */ jsx5(
1993
+ "input",
1994
+ {
1995
+ className: cx3("qt-chip-val", classNames?.input),
1996
+ value,
1997
+ placeholder: "value",
1998
+ size: widthChars,
1999
+ style: { width: `${widthChars}ch` },
2000
+ onChange: (e) => onChange(e.target.value)
2001
+ }
2002
+ );
2003
+ }
2004
+ return /* @__PURE__ */ jsx5(AutocompleteInput, { api, field, value, classNames, onChange });
2005
+ }
2006
+ function AutocompleteInput({
2007
+ api,
2008
+ field,
2009
+ value,
2010
+ classNames,
2011
+ onChange
2012
+ }) {
2013
+ const [result, setResult] = useState4({ values: [], hasMore: false });
2014
+ const listId = useId();
2015
+ useEffect4(() => {
2016
+ let cancelled = false;
2017
+ const t = setTimeout(() => {
2018
+ void api.filterValues(field.name, value).then((r) => {
2019
+ if (!cancelled) setResult(r);
2020
+ }).catch(() => {
2021
+ if (!cancelled) setResult({ values: [], hasMore: false });
2022
+ });
2023
+ }, 200);
2024
+ return () => {
2025
+ cancelled = true;
2026
+ clearTimeout(t);
2027
+ };
2028
+ }, [api, field.name, value]);
2029
+ return /* @__PURE__ */ jsxs5("span", { className: "qt-autocomplete", children: [
2030
+ /* @__PURE__ */ jsx5(
2031
+ "input",
2032
+ {
2033
+ className: cx3("qt-chip-val", classNames?.input),
2034
+ value,
2035
+ placeholder: "value",
2036
+ list: listId,
2037
+ size: chipFieldWidthForInput(value || "value", field.type, 5, 4),
2038
+ style: { width: `${chipFieldWidthForInput(value || "value", field.type, 5, 4)}ch` },
2039
+ onChange: (e) => onChange(e.target.value)
2040
+ }
2041
+ ),
2042
+ /* @__PURE__ */ jsx5("datalist", { id: listId, children: result.values.map((v) => /* @__PURE__ */ jsx5("option", { value: v }, v)) }),
2043
+ result.hasMore && /* @__PURE__ */ jsx5("span", { className: "qt-autocomplete-hint", children: "keep typing to refine\u2026" })
2044
+ ] });
2045
+ }
2046
+ function OrderTermChip({
2047
+ api,
2048
+ field,
2049
+ term,
2050
+ dataIndex,
2051
+ priority,
2052
+ showPriority,
2053
+ isDragSource,
2054
+ onDragStart,
2055
+ onDragOver,
2056
+ onDrop,
2057
+ onDragEnd,
2058
+ disabled,
2059
+ updateTerm,
2060
+ removeTerm,
2061
+ classNames,
2062
+ label
2063
+ }) {
2064
+ const fieldHasNull = useFieldHasNull(api, field?.name);
2065
+ return /* @__PURE__ */ jsxs5(
2066
+ "span",
2067
+ {
2068
+ className: cx3("qt-chip", "qt-chip--col", "qt-chip--sort", isDragSource && "qt-chip--dragging", classNames?.chip),
2069
+ draggable: !disabled,
2070
+ onDragStart: (e) => {
2071
+ onDragStart();
2072
+ e.dataTransfer.effectAllowed = "move";
2073
+ e.dataTransfer.setData("text/plain", term.field);
2074
+ },
2075
+ onDragOver,
2076
+ onDrop: (e) => {
2077
+ e.preventDefault();
2078
+ onDrop();
2079
+ },
2080
+ onDragEnd,
2081
+ title: "drag to reorder sort priority",
2082
+ children: [
2083
+ /* @__PURE__ */ jsx5("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
2084
+ showPriority && /* @__PURE__ */ jsx5("span", { className: "qt-chip-priority", children: priority }),
2085
+ /* @__PURE__ */ jsx5("span", { className: "qt-chip-field", children: label }),
2086
+ /* @__PURE__ */ jsx5(
2087
+ "button",
2088
+ {
2089
+ type: "button",
2090
+ className: "qt-chip-op-btn",
2091
+ disabled,
2092
+ onClick: () => updateTerm(dataIndex, { dir: term.dir === "asc" ? "desc" : "asc" }),
2093
+ children: term.dir === "asc" ? "\u2191 asc" : "\u2193 desc"
2094
+ }
2095
+ ),
2096
+ fieldHasNull !== false && /* @__PURE__ */ jsxs5(
2097
+ "button",
2098
+ {
2099
+ type: "button",
2100
+ className: "qt-chip-op-btn",
2101
+ disabled,
2102
+ title: "where NULL values sort",
2103
+ onClick: () => updateTerm(dataIndex, { nulls: (term.nulls ?? "last") === "last" ? "first" : "last" }),
2104
+ children: [
2105
+ "nulls ",
2106
+ term.nulls ?? "last"
2107
+ ]
2108
+ }
2109
+ ),
2110
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: () => removeTerm(dataIndex), disabled, children: "\u2715" })
2111
+ ]
2112
+ }
2113
+ );
2114
+ }
2115
+ function OrderRow({
2116
+ api,
2117
+ fields,
2118
+ classNames,
2119
+ disabled
2120
+ }) {
2121
+ const orderBy = api.query.orderBy;
2122
+ const byName = useMemo3(() => new Map(fields.map((f) => [f.name, f])), [fields]);
2123
+ const [drag, setDrag] = useState4(null);
2124
+ const [adding, setAdding] = useState4(false);
2125
+ function setOrderBy(next) {
2126
+ api.setSort(next);
2127
+ }
2128
+ function updateTerm(i, patch) {
2129
+ setOrderBy(orderBy.map((o, k) => k === i ? { ...o, ...patch } : o));
2130
+ }
2131
+ function removeTerm(i) {
2132
+ setOrderBy(orderBy.filter((_, k) => k !== i));
2133
+ }
2134
+ function addTerm(f) {
2135
+ const field = f.sort?.field ?? f.name;
2136
+ const nextOrderBy = [...orderBy, { field, dir: "desc" }];
2137
+ if (orderBy.some((o) => o.field === field)) {
2138
+ setAdding(false);
2139
+ return;
2140
+ }
2141
+ const hasSelectColumn = api.select.visible.some((c) => c.field === field);
2142
+ if (hasSelectColumn) {
2143
+ setOrderBy(nextOrderBy);
2144
+ setAdding(false);
2145
+ return;
2146
+ }
2147
+ api.setQuery((q) => ({
2148
+ ...q,
2149
+ offset: 0,
2150
+ orderBy: nextOrderBy,
2151
+ select: q.select.length ? [...q.select, { field }] : [...api.select.visible, { field }]
2152
+ }));
2153
+ setAdding(false);
2154
+ }
2155
+ const termFields = orderBy.map((o) => o.field);
2156
+ const termByField = useMemo3(() => new Map(orderBy.map((o) => [o.field, o])), [orderBy]);
2157
+ function previewFields() {
2158
+ if (!drag) return termFields;
2159
+ const without = termFields.filter((f) => f !== drag.source);
2160
+ if (without.length === termFields.length) return termFields;
2161
+ const at = Math.max(0, Math.min(drag.overIndex, without.length));
2162
+ return [...without.slice(0, at), drag.source, ...without.slice(at)];
2163
+ }
2164
+ function handleSortOver(e, field) {
2165
+ if (!drag) return;
2166
+ e.preventDefault();
2167
+ e.dataTransfer.dropEffect = "move";
2168
+ if (drag.source !== field) {
2169
+ const without = termFields.filter((f) => f !== drag.source);
2170
+ let next = without.indexOf(field);
2171
+ if (next >= 0) {
2172
+ const rect = e.currentTarget.getBoundingClientRect();
2173
+ if (e.clientX > rect.left + rect.width / 2) next += 1;
2174
+ setDrag((d) => d && d.overIndex !== next ? { source: d.source, overIndex: next } : d);
2175
+ }
2176
+ }
2177
+ }
2178
+ function commitSortDrop() {
2179
+ if (drag) setOrderBy(previewFields().map((f) => termByField.get(f)));
2180
+ setDrag(null);
2181
+ }
2182
+ const rendered = previewFields();
2183
+ const sortable = fields.filter(isSortable2);
2184
+ const isDefaultOrderBy = orderBy.length === api.defaults.orderBy.length && orderBy.every((term, i) => {
2185
+ const next = api.defaults.orderBy[i];
2186
+ return term.field === next?.field && term.dir === next?.dir && (term.nulls ?? "last") === (next?.nulls ?? "last");
2187
+ });
2188
+ return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row", children: [
2189
+ /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "order by" }),
2190
+ orderBy.length === 0 && !adding && /* @__PURE__ */ jsx5("span", { className: "qt-qb-hint", children: "(default)" }),
2191
+ rendered.map((f, pos) => {
2192
+ const term = termByField.get(f);
2193
+ return /* @__PURE__ */ jsx5(
2194
+ OrderTermChip,
2195
+ {
2196
+ api,
2197
+ field: byName.get(f),
2198
+ term,
2199
+ dataIndex: orderBy.findIndex((o) => o.field === f),
2200
+ priority: pos + 1,
2201
+ showPriority: orderBy.length > 1,
2202
+ isDragSource: drag?.source === f,
2203
+ onDragStart: () => setDrag({ source: f, overIndex: termFields.indexOf(f) }),
2204
+ onDragOver: (e) => handleSortOver(e, f),
2205
+ onDrop: commitSortDrop,
2206
+ onDragEnd: () => setDrag(null),
2207
+ disabled,
2208
+ updateTerm,
2209
+ removeTerm,
2210
+ classNames,
2211
+ label: byName.get(f)?.label ?? f
2212
+ },
2213
+ f
2214
+ );
2215
+ }),
2216
+ adding ? /* @__PURE__ */ jsx5(FieldPicker, { fields: sortable, onPick: addTerm, onClose: () => setAdding(false) }) : /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-add", onClick: () => setAdding(true), disabled, children: "+ add sort" }),
2217
+ !isDefaultOrderBy && /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-link-btn", onClick: () => api.setSort(api.defaults.orderBy), disabled, children: "reset" })
2218
+ ] });
2219
+ }
2220
+ function WindowRow({
2221
+ api,
2222
+ classNames,
2223
+ disabled
2224
+ }) {
2225
+ const { query } = api;
2226
+ const [limitDraft, setLimitDraft] = useState4(String(query.limit));
2227
+ const [offsetDraft, setOffsetDraft] = useState4(String(query.offset));
2228
+ useEffect4(() => setLimitDraft(String(query.limit)), [query.limit]);
2229
+ useEffect4(() => setOffsetDraft(String(query.offset)), [query.offset]);
2230
+ const hasLimitDefault = query.limit === api.defaults.limit;
2231
+ const hasOffsetDefault = query.offset === api.defaults.offset;
2232
+ const hasWindowDefault = hasLimitDefault && hasOffsetDefault;
2233
+ function commitLimit() {
2234
+ api.setLimit(Math.max(1, Number(limitDraft) || query.limit));
2235
+ }
2236
+ function commitOffset() {
2237
+ api.setOffset(Math.max(0, Number(offsetDraft) || 0));
2238
+ }
2239
+ return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row qt-qb-pagination", children: [
2240
+ /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "limit" }),
2241
+ /* @__PURE__ */ jsx5(
2242
+ "input",
2243
+ {
2244
+ className: cx3("qt-qb-num", classNames?.input),
2245
+ type: "number",
2246
+ min: 1,
2247
+ value: limitDraft,
2248
+ disabled,
2249
+ onChange: (e) => setLimitDraft(e.target.value),
2250
+ onBlur: commitLimit,
2251
+ onKeyDown: (e) => e.key === "Enter" && commitLimit()
2252
+ }
2253
+ ),
2254
+ /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "offset" }),
2255
+ /* @__PURE__ */ jsx5(
2256
+ "input",
2257
+ {
2258
+ className: cx3("qt-qb-num", classNames?.input),
2259
+ type: "number",
2260
+ min: 0,
2261
+ value: offsetDraft,
2262
+ disabled,
2263
+ onChange: (e) => setOffsetDraft(e.target.value),
2264
+ onBlur: commitOffset,
2265
+ onKeyDown: (e) => e.key === "Enter" && commitOffset()
2266
+ }
2267
+ ),
2268
+ !hasWindowDefault && /* @__PURE__ */ jsx5(
2269
+ "button",
2270
+ {
2271
+ type: "button",
2272
+ className: "qt-link-btn",
2273
+ onClick: () => {
2274
+ api.setLimit(api.defaults.limit);
2275
+ api.setOffset(api.defaults.offset);
2276
+ },
2277
+ disabled,
2278
+ title: "Reset limit and offset",
2279
+ children: "reset"
2280
+ }
2281
+ )
2282
+ ] });
2283
+ }
2284
+ var AGG_OPS = ["count", "count_distinct", "sum", "avg", "min", "max"];
2285
+ var AGG_OP_LABELS = {
2286
+ count: "count",
2287
+ count_distinct: "count distinct",
2288
+ sum: "sum",
2289
+ avg: "avg",
2290
+ min: "min",
2291
+ max: "max"
2292
+ };
2293
+ function MetricsRow({
2294
+ api,
2295
+ fields,
2296
+ classNames,
2297
+ disabled
2298
+ }) {
2299
+ const { aggregations } = api;
2300
+ const clauses = aggregations.clauses;
2301
+ const measurable = useMemo3(() => fields.filter(isMeasurable), [fields]);
2302
+ const groupable = useMemo3(() => fields.filter(isGroupable), [fields]);
2303
+ const [drag, setDrag] = useState4(null);
2304
+ const ids = clauses.map((c) => c.id);
2305
+ const byId = useMemo3(() => new Map(clauses.map((c) => [c.id, c])), [clauses]);
2306
+ function previewIds() {
2307
+ if (!drag) return ids;
2308
+ const without = ids.filter((i) => i !== drag.source);
2309
+ if (without.length === ids.length) return ids;
2310
+ const at = Math.max(0, Math.min(drag.overIndex, without.length));
2311
+ return [...without.slice(0, at), drag.source, ...without.slice(at)];
2312
+ }
2313
+ function handleOver(e, id) {
2314
+ if (!drag) return;
2315
+ e.preventDefault();
2316
+ e.dataTransfer.dropEffect = "move";
2317
+ if (drag.source === id) return;
2318
+ const without = ids.filter((i) => i !== drag.source);
2319
+ let next = without.indexOf(id);
2320
+ if (next >= 0) {
2321
+ const rect = e.currentTarget.getBoundingClientRect();
2322
+ if (e.clientX > rect.left + rect.width / 2) next += 1;
2323
+ setDrag((d) => d && d.overIndex !== next ? { source: d.source, overIndex: next } : d);
2324
+ }
2325
+ }
2326
+ function commitDrop() {
2327
+ if (drag) aggregations.move(drag.source, drag.overIndex);
2328
+ setDrag(null);
2329
+ }
2330
+ const rendered = previewIds();
2331
+ return /* @__PURE__ */ jsxs5("div", { className: "qt-qb-row qt-qb-row--metrics", children: [
2332
+ /* @__PURE__ */ jsx5("span", { className: "qt-qb-kw", children: "metrics" }),
2333
+ clauses.length === 0 && /* @__PURE__ */ jsx5("span", { className: "qt-qb-hint", children: "none" }),
2334
+ rendered.map((id) => {
2335
+ const clause = byId.get(id);
2336
+ return /* @__PURE__ */ jsx5(
2337
+ MetricChip,
2338
+ {
2339
+ clause,
2340
+ measurable,
2341
+ groupable,
2342
+ classNames,
2343
+ disabled,
2344
+ isDragSource: drag?.source === id,
2345
+ onDragStart: () => setDrag({ source: id, overIndex: ids.indexOf(id) }),
2346
+ onDragOver: (e) => handleOver(e, id),
2347
+ onDrop: commitDrop,
2348
+ onDragEnd: () => setDrag(null),
2349
+ onChangeOp: (op) => changeMetricOp(aggregations, clause, measurable, op),
2350
+ onChangeField: (field) => aggregations.update(id, { field }),
2351
+ onAddGroup: (field) => aggregations.update(id, { groupBy: [...clause.groupBy, field] }),
2352
+ onRemoveGroup: (field) => aggregations.update(id, { groupBy: clause.groupBy.filter((g) => g !== field) }),
2353
+ onRemove: () => aggregations.remove(id)
2354
+ },
2355
+ id
2356
+ );
2357
+ }),
2358
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-add", onClick: () => aggregations.add(), disabled, children: "+ add metric" }),
2359
+ clauses.length > 0 && /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-link-btn", onClick: aggregations.clear, disabled, title: "Remove all metrics", children: "reset" })
2360
+ ] });
2361
+ }
2362
+ function changeMetricOp(aggregations, clause, measurable, op) {
2363
+ const field = clause.field;
2364
+ const stillValid = field != null && measurable.some((f) => f.name === field && aggOpsForField(f).includes(op));
2365
+ if (field != null && !stillValid) aggregations.update(clause.id, { op, field: void 0 });
2366
+ else aggregations.update(clause.id, { op });
2367
+ }
2368
+ function MetricChip({
2369
+ clause,
2370
+ measurable,
2371
+ groupable,
2372
+ classNames,
2373
+ disabled,
2374
+ isDragSource,
2375
+ onDragStart,
2376
+ onDragOver,
2377
+ onDrop,
2378
+ onDragEnd,
2379
+ onChangeOp,
2380
+ onChangeField,
2381
+ onAddGroup,
2382
+ onRemoveGroup,
2383
+ onRemove
2384
+ }) {
2385
+ const [addingGroup, setAddingGroup] = useState4(false);
2386
+ const byName = useMemo3(() => new Map(groupable.map((f) => [f.name, f])), [groupable]);
2387
+ const needsField = aggOpNeedsField(clause.op);
2388
+ const measureOptions = measurable.filter((f) => aggOpsForField(f).includes(clause.op));
2389
+ return /* @__PURE__ */ jsxs5(
2390
+ "span",
2391
+ {
2392
+ className: cx3("qt-chip", "qt-chip--agg", isDragSource && "qt-chip--dragging", classNames?.chip),
2393
+ draggable: !disabled,
2394
+ onDragStart: (e) => {
2395
+ onDragStart();
2396
+ e.dataTransfer.effectAllowed = "move";
2397
+ e.dataTransfer.setData("text/plain", clause.id);
2398
+ },
2399
+ onDragOver,
2400
+ onDrop: (e) => {
2401
+ e.preventDefault();
2402
+ onDrop();
2403
+ },
2404
+ onDragEnd,
2405
+ title: "drag to reorder metrics",
2406
+ children: [
2407
+ /* @__PURE__ */ jsx5("span", { "aria-hidden": true, className: "qt-chip-grip", children: "\u22EE\u22EE" }),
2408
+ /* @__PURE__ */ jsx5(
2409
+ "select",
2410
+ {
2411
+ className: cx3("qt-chip-op", classNames?.select),
2412
+ value: clause.op,
2413
+ disabled,
2414
+ onChange: (e) => onChangeOp(e.target.value),
2415
+ children: AGG_OPS.map((op) => /* @__PURE__ */ jsx5("option", { value: op, children: AGG_OP_LABELS[op] }, op))
2416
+ }
2417
+ ),
2418
+ /* @__PURE__ */ jsxs5(
2419
+ "select",
2420
+ {
2421
+ className: cx3("qt-chip-val", classNames?.select),
2422
+ value: clause.field ?? "",
2423
+ disabled,
2424
+ onChange: (e) => onChangeField(e.target.value || void 0),
2425
+ children: [
2426
+ !needsField && /* @__PURE__ */ jsx5("option", { value: "", children: "(all rows)" }),
2427
+ needsField && clause.field == null && /* @__PURE__ */ jsx5("option", { value: "", children: "measure\u2026" }),
2428
+ measureOptions.map((f) => /* @__PURE__ */ jsx5("option", { value: f.name, children: f.label }, f.name))
2429
+ ]
2430
+ }
2431
+ ),
2432
+ /* @__PURE__ */ jsxs5("span", { className: "qt-chip-agg-by", children: [
2433
+ clause.groupBy.map((g) => /* @__PURE__ */ jsxs5("span", { className: "qt-chip-agg-group", children: [
2434
+ /* @__PURE__ */ jsx5("span", { className: "qt-chip-agg-group-label", children: byName.get(g)?.label ?? g }),
2435
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: () => onRemoveGroup(g), disabled, children: "\u2715" })
2436
+ ] }, g)),
2437
+ addingGroup ? /* @__PURE__ */ jsx5(
2438
+ FieldPicker,
2439
+ {
2440
+ fields: groupable,
2441
+ excluded: clause.groupBy,
2442
+ onPick: (f) => {
2443
+ onAddGroup(f.name);
2444
+ setAddingGroup(false);
2445
+ },
2446
+ onClose: () => setAddingGroup(false)
2447
+ }
2448
+ ) : /* @__PURE__ */ jsx5(
2449
+ "button",
2450
+ {
2451
+ type: "button",
2452
+ className: "qt-add qt-chip-agg-add",
2453
+ onClick: () => setAddingGroup(true),
2454
+ disabled,
2455
+ title: "Group by a field",
2456
+ children: clause.groupBy.length === 0 ? "+ by" : "+"
2457
+ }
2458
+ )
2459
+ ] }),
2460
+ /* @__PURE__ */ jsx5("button", { type: "button", className: "qt-chip-x", onClick: onRemove, disabled, title: "Remove metric", children: "\u2715" })
2461
+ ]
2462
+ }
2463
+ );
2464
+ }
2465
+
2466
+ // src/MetricsPanel.tsx
2467
+ import { useMemo as useMemo4 } from "react";
2468
+ import { EMPTY_QUERY } from "@pythia-software/query-table-core";
2469
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2470
+ var cx4 = (...parts) => parts.filter((p) => Boolean(p)).join(" ");
2471
+ var OP_LABELS = {
2472
+ count: "Count",
2473
+ count_distinct: "Distinct",
2474
+ sum: "Sum",
2475
+ avg: "Avg",
2476
+ min: "Min",
2477
+ max: "Max"
2478
+ };
2479
+ var NULL_KEY = "\u2205";
2480
+ function MetricsPanel({
2481
+ aggregations,
2482
+ fields,
2483
+ renderers,
2484
+ emptyMessage,
2485
+ classNames
2486
+ }) {
2487
+ const byName = useMemo4(() => new Map(fields.map((f) => [f.name, f])), [fields]);
2488
+ const { clauses, results, loading, error } = aggregations;
2489
+ if (clauses.length === 0) {
2490
+ return emptyMessage ? /* @__PURE__ */ jsx6("div", { className: cx4("qt-metrics", classNames?.root), children: emptyMessage }) : null;
2491
+ }
2492
+ const resultById = new Map((results?.metrics ?? []).map((m) => [m.id, m.buckets]));
2493
+ return /* @__PURE__ */ jsxs6("div", { className: cx4("qt-metrics", classNames?.root), children: [
2494
+ error ? /* @__PURE__ */ jsxs6("div", { className: "qt-metrics-error", children: [
2495
+ "Metrics failed: ",
2496
+ error.message
2497
+ ] }) : null,
2498
+ clauses.map((clause) => /* @__PURE__ */ jsx6(
2499
+ MetricCard,
2500
+ {
2501
+ clause,
2502
+ buckets: resultById.get(clause.id) ?? [],
2503
+ loading: loading && !resultById.has(clause.id),
2504
+ byName,
2505
+ renderers,
2506
+ className: classNames?.metric
2507
+ },
2508
+ clause.id
2509
+ ))
2510
+ ] });
2511
+ }
2512
+ function MetricCard({
2513
+ clause,
2514
+ buckets,
2515
+ loading,
2516
+ byName,
2517
+ renderers,
2518
+ className
2519
+ }) {
2520
+ const measure = clause.field ? byName.get(clause.field) : void 0;
2521
+ const groupLabels = clause.groupBy.map((g) => byName.get(g)?.label ?? g);
2522
+ const decimals = useMemo4(() => cardDecimals(buckets), [buckets]);
2523
+ const fmt = (v) => formatMetricValue(v, clause.op, measure, renderers, decimals);
2524
+ return /* @__PURE__ */ jsxs6("div", { className: cx4("qt-metric", `qt-metric--gb${Math.min(clause.groupBy.length, 3)}`, className), children: [
2525
+ /* @__PURE__ */ jsxs6("div", { className: "qt-metric-head", children: [
2526
+ /* @__PURE__ */ jsx6("span", { className: "qt-metric-title", children: metricTitle(clause, measure) }),
2527
+ groupLabels.length > 0 && /* @__PURE__ */ jsxs6("span", { className: "qt-metric-by", children: [
2528
+ "by ",
2529
+ groupLabels.join(" \xD7 ")
2530
+ ] })
2531
+ ] }),
2532
+ loading && buckets.length === 0 ? /* @__PURE__ */ jsx6("div", { className: "qt-metric-loading", children: "\u2026" }) : buckets.length === 0 ? /* @__PURE__ */ jsx6("div", { className: "qt-metric-empty", children: "no data" }) : clause.groupBy.length === 0 ? /* @__PURE__ */ jsx6(BigNumber, { bucket: buckets[0], fmt }) : clause.groupBy.length === 1 ? /* @__PURE__ */ jsx6(BarList, { buckets, fmt }) : clause.groupBy.length === 2 ? /* @__PURE__ */ jsx6(Pivot, { buckets, fmt, rowLabel: groupLabels[0], colLabel: groupLabels[1] }) : /* @__PURE__ */ jsx6(FlatTable, { buckets, fmt, groupLabels, valueLabel: metricTitle(clause, measure) })
2533
+ ] });
2534
+ }
2535
+ function BigNumber({ bucket, fmt }) {
2536
+ return /* @__PURE__ */ jsxs6("div", { className: "qt-metric-big", children: [
2537
+ /* @__PURE__ */ jsx6("span", { className: "qt-metric-big-value", children: fmt(bucket.value) }),
2538
+ /* @__PURE__ */ jsxs6("span", { className: "qt-metric-big-sub", children: [
2539
+ bucket.count.toLocaleString(),
2540
+ " rows"
2541
+ ] })
2542
+ ] });
2543
+ }
2544
+ function BarList({ buckets, fmt }) {
2545
+ const max = Math.max(...buckets.map((b) => barMagnitude(b)), 0);
2546
+ return /* @__PURE__ */ jsx6("div", { className: "qt-metric-bars", children: buckets.map((b, i) => {
2547
+ const pct = max > 0 ? barMagnitude(b) / max * 100 : 0;
2548
+ return /* @__PURE__ */ jsxs6("div", { className: "qt-metric-bar-row", children: [
2549
+ /* @__PURE__ */ jsx6("span", { className: "qt-metric-bar-label", title: keyText(b.keys[0]), children: keyNode(b.keys[0]) }),
2550
+ /* @__PURE__ */ jsx6("span", { className: "qt-metric-bar-track", children: /* @__PURE__ */ jsx6("span", { className: "qt-metric-bar-fill", style: { width: `${pct}%` } }) }),
2551
+ /* @__PURE__ */ jsx6("span", { className: "qt-metric-bar-value", children: fmt(b.value) })
2552
+ ] }, `${keyText(b.keys[0])}-${i}`);
2553
+ }) });
2554
+ }
2555
+ function Pivot({
2556
+ buckets,
2557
+ fmt,
2558
+ rowLabel,
2559
+ colLabel
2560
+ }) {
2561
+ const rowKeys = distinctKeys(buckets, 0);
2562
+ const colKeys = distinctKeys(buckets, 1);
2563
+ const cell = /* @__PURE__ */ new Map();
2564
+ for (const b of buckets) cell.set(`${keyText(b.keys[0])}\0${keyText(b.keys[1])}`, b.value);
2565
+ return /* @__PURE__ */ jsx6("div", { className: "qt-metric-pivot-wrap", children: /* @__PURE__ */ jsxs6("table", { className: "qt-metric-pivot", children: [
2566
+ /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsxs6("tr", { children: [
2567
+ /* @__PURE__ */ jsx6("th", { className: "qt-metric-pivot-corner", children: /* @__PURE__ */ jsxs6("span", { className: "qt-metric-pivot-axes", children: [
2568
+ rowLabel,
2569
+ " ",
2570
+ /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: "\\" }),
2571
+ " ",
2572
+ colLabel
2573
+ ] }) }),
2574
+ colKeys.map((c) => /* @__PURE__ */ jsx6("th", { className: "qt-metric-pivot-col", children: keyNode(c) }, keyText(c)))
2575
+ ] }) }),
2576
+ /* @__PURE__ */ jsx6("tbody", { children: rowKeys.map((r) => /* @__PURE__ */ jsxs6("tr", { children: [
2577
+ /* @__PURE__ */ jsx6("th", { className: "qt-metric-pivot-row", children: keyNode(r) }),
2578
+ colKeys.map((c) => {
2579
+ const v = cell.get(`${keyText(r)}\0${keyText(c)}`);
2580
+ return /* @__PURE__ */ jsx6("td", { className: "qt-metric-pivot-cell", children: v === void 0 ? /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: "\xB7" }) : fmt(v) }, keyText(c));
2581
+ })
2582
+ ] }, keyText(r))) })
2583
+ ] }) });
2584
+ }
2585
+ function FlatTable({
2586
+ buckets,
2587
+ fmt,
2588
+ groupLabels,
2589
+ valueLabel
2590
+ }) {
2591
+ return /* @__PURE__ */ jsx6("div", { className: "qt-metric-pivot-wrap", children: /* @__PURE__ */ jsxs6("table", { className: "qt-metric-flat", children: [
2592
+ /* @__PURE__ */ jsx6("thead", { children: /* @__PURE__ */ jsxs6("tr", { children: [
2593
+ groupLabels.map((g) => /* @__PURE__ */ jsx6("th", { children: g }, g)),
2594
+ /* @__PURE__ */ jsx6("th", { className: "qt-metric-flat-value", children: valueLabel })
2595
+ ] }) }),
2596
+ /* @__PURE__ */ jsx6("tbody", { children: buckets.map((b, i) => /* @__PURE__ */ jsxs6("tr", { children: [
2597
+ groupLabels.map((_, j) => /* @__PURE__ */ jsx6("td", { children: keyNode(b.keys[j]) }, j)),
2598
+ /* @__PURE__ */ jsx6("td", { className: "qt-metric-flat-value", children: fmt(b.value) })
2599
+ ] }, i)) })
2600
+ ] }) });
2601
+ }
2602
+ function metricTitle(clause, measure) {
2603
+ if (clause.label) return clause.label;
2604
+ if (clause.op === "count" && !measure) return "Count";
2605
+ return `${OP_LABELS[clause.op]} ${measure?.label ?? clause.field ?? ""}`.trim();
2606
+ }
2607
+ var UNIT_RENDER_KEYS = /* @__PURE__ */ new Set(["duration_ms", "byte_size"]);
2608
+ function formatMetricValue(value, op, measure, renderers, decimals) {
2609
+ if (value == null) return /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: "\u2014" });
2610
+ if (op === "count" || op === "count_distinct") {
2611
+ return typeof value === "number" ? formatNumber(value, 0) : String(value);
2612
+ }
2613
+ if (typeof value === "number") {
2614
+ const key = typeof measure?.render === "string" ? measure.render : void 0;
2615
+ if (key && UNIT_RENDER_KEYS.has(key) && measure && renderers) {
2616
+ const node = tryRender(measure, renderers, value);
2617
+ if (node != null) return node;
2618
+ }
2619
+ return formatNumber(value, decimals);
2620
+ }
2621
+ if (measure && renderers) {
2622
+ const node = tryRender(measure, renderers, value);
2623
+ if (node != null) return node;
2624
+ }
2625
+ return String(value);
2626
+ }
2627
+ function tryRender(measure, renderers, value) {
2628
+ try {
2629
+ const node = resolveRenderer(measure, renderers)({
2630
+ value,
2631
+ row: {},
2632
+ field: measure,
2633
+ query: EMPTY_QUERY
2634
+ });
2635
+ return node != null && node !== "" ? node : null;
2636
+ } catch {
2637
+ return null;
2638
+ }
2639
+ }
2640
+ function formatNumber(value, decimals) {
2641
+ if (!Number.isFinite(value)) return String(value);
2642
+ return value.toLocaleString(void 0, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
2643
+ }
2644
+ function cardDecimals(buckets) {
2645
+ const nums = buckets.map((b) => b.value).filter((v) => typeof v === "number" && Number.isFinite(v));
2646
+ if (nums.length === 0) return 0;
2647
+ if (nums.every((v) => Number.isInteger(v))) return 0;
2648
+ const maxAbs = Math.max(...nums.map((v) => Math.abs(v)));
2649
+ const intDigits = maxAbs >= 1 ? Math.floor(Math.log10(maxAbs)) + 1 : 0;
2650
+ return Math.min(6, Math.max(0, 5 - intDigits));
2651
+ }
2652
+ function barMagnitude(b) {
2653
+ return typeof b.value === "number" ? Math.abs(b.value) : b.count;
2654
+ }
2655
+ function distinctKeys(buckets, axis) {
2656
+ const seen = /* @__PURE__ */ new Set();
2657
+ const out = [];
2658
+ for (const b of buckets) {
2659
+ const k = b.keys[axis] ?? null;
2660
+ const t = keyText(k);
2661
+ if (!seen.has(t)) {
2662
+ seen.add(t);
2663
+ out.push(k);
2664
+ }
2665
+ }
2666
+ return out;
2667
+ }
2668
+ function keyText(k) {
2669
+ return k == null ? NULL_KEY : String(k);
2670
+ }
2671
+ function keyNode(k) {
2672
+ return k == null ? /* @__PURE__ */ jsx6("span", { className: "qt-muted", children: NULL_KEY }) : String(k);
2673
+ }
2674
+
2675
+ // src/SelectionToolbar.tsx
2676
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2677
+ var cx5 = (...parts) => parts.filter(Boolean).join(" ");
2678
+ function SelectionToolbar({ selection, actions, alwaysShow, className }) {
2679
+ if (selection.count === 0 && !alwaysShow) return null;
2680
+ const selectedIds = [...selection.selected];
2681
+ return /* @__PURE__ */ jsxs7("div", { className: cx5("qt-selection-toolbar", className), role: "toolbar", children: [
2682
+ /* @__PURE__ */ jsxs7("span", { className: "qt-selection-count", children: [
2683
+ selection.count,
2684
+ " selected"
2685
+ ] }),
2686
+ /* @__PURE__ */ jsx7(
2687
+ "button",
2688
+ {
2689
+ type: "button",
2690
+ className: "qt-btn",
2691
+ onClick: selection.clear,
2692
+ disabled: selection.count === 0,
2693
+ children: "clear"
2694
+ }
2695
+ ),
2696
+ /* @__PURE__ */ jsx7("span", { className: "qt-selection-actions", children: actions(selectedIds) })
2697
+ ] });
2698
+ }
2699
+ export {
2700
+ CellMenu,
2701
+ DataTable,
2702
+ FieldPicker,
2703
+ MetricsPanel,
2704
+ QueryBuilder,
2705
+ SavedQueriesModal,
2706
+ SelectionToolbar,
2707
+ defaultRenderers,
2708
+ resolveRenderer,
2709
+ safeLinkHref
2710
+ };
2711
+ //# sourceMappingURL=index.js.map