@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/LICENSE +21 -0
- package/README.md +48 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +2711 -0
- package/dist/index.js.map +1 -0
- package/dist/theme.css +1172 -0
- package/dist/theme.css.map +1 -0
- package/dist/theme.d.ts +2 -0
- package/package.json +43 -0
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
|