@pythia-software/query-table-react 0.5.0 → 0.7.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/README.md +62 -1
- package/dist/index.d.ts +236 -20
- package/dist/index.js +2413 -416
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,16 +1,769 @@
|
|
|
1
|
+
// src/useQueryFilters.ts
|
|
2
|
+
import { useCallback, useMemo } from "react";
|
|
3
|
+
import {
|
|
4
|
+
isOrGroup,
|
|
5
|
+
predicatesOf,
|
|
6
|
+
negateClause,
|
|
7
|
+
opsForField
|
|
8
|
+
} from "@pythia-software/query-table-core";
|
|
9
|
+
function useQueryFilters(schema, setQuery) {
|
|
10
|
+
const byName = useMemo(
|
|
11
|
+
() => new Map(schema.fields.map((field) => [field.name, field])),
|
|
12
|
+
[schema]
|
|
13
|
+
);
|
|
14
|
+
const addFilter = useCallback(
|
|
15
|
+
(clause) => setQuery((q) => ({ ...q, offset: 0, where: [...q.where, clause] })),
|
|
16
|
+
[setQuery]
|
|
17
|
+
);
|
|
18
|
+
const updateFilter = useCallback(
|
|
19
|
+
(index, clause) => setQuery((q) => ({
|
|
20
|
+
...q,
|
|
21
|
+
offset: 0,
|
|
22
|
+
where: q.where.map((t, i) => i === index ? clause : t)
|
|
23
|
+
})),
|
|
24
|
+
[setQuery]
|
|
25
|
+
);
|
|
26
|
+
const removeFilter = useCallback(
|
|
27
|
+
(index) => setQuery((q) => ({
|
|
28
|
+
...q,
|
|
29
|
+
offset: 0,
|
|
30
|
+
where: q.where.filter((_, i) => i !== index)
|
|
31
|
+
})),
|
|
32
|
+
[setQuery]
|
|
33
|
+
);
|
|
34
|
+
const clearFilters = useCallback(
|
|
35
|
+
() => setQuery((q) => ({ ...q, offset: 0, where: [] })),
|
|
36
|
+
[setQuery]
|
|
37
|
+
);
|
|
38
|
+
const updatePredicate = useCallback(
|
|
39
|
+
(termIndex, predIndex, clause) => setQuery((q) => ({
|
|
40
|
+
...q,
|
|
41
|
+
offset: 0,
|
|
42
|
+
where: q.where.map((term, i) => {
|
|
43
|
+
if (i !== termIndex) return term;
|
|
44
|
+
if (isOrGroup(term))
|
|
45
|
+
return {
|
|
46
|
+
any: term.any.map((c, j) => j === predIndex ? clause : c)
|
|
47
|
+
};
|
|
48
|
+
return clause;
|
|
49
|
+
})
|
|
50
|
+
})),
|
|
51
|
+
[setQuery]
|
|
52
|
+
);
|
|
53
|
+
const removePredicate = useCallback(
|
|
54
|
+
(termIndex, predIndex) => setQuery((q) => {
|
|
55
|
+
const where = [];
|
|
56
|
+
q.where.forEach((term, i) => {
|
|
57
|
+
if (i !== termIndex) {
|
|
58
|
+
where.push(term);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (isOrGroup(term)) {
|
|
62
|
+
const members = term.any.filter((_, j) => j !== predIndex);
|
|
63
|
+
if (members.length === 1) where.push(members[0]);
|
|
64
|
+
else if (members.length > 1) where.push({ any: members });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return { ...q, offset: 0, where };
|
|
69
|
+
}),
|
|
70
|
+
[setQuery]
|
|
71
|
+
);
|
|
72
|
+
const negateOne = useCallback(
|
|
73
|
+
(clause) => {
|
|
74
|
+
const field = byName.get(clause.field);
|
|
75
|
+
return negateClause(clause, field ? opsForField(field) : void 0);
|
|
76
|
+
},
|
|
77
|
+
[byName]
|
|
78
|
+
);
|
|
79
|
+
const negatePredicate = useCallback(
|
|
80
|
+
(termIndex, predIndex) => setQuery((q) => ({
|
|
81
|
+
...q,
|
|
82
|
+
offset: 0,
|
|
83
|
+
where: q.where.map((term, i) => {
|
|
84
|
+
if (i !== termIndex) return term;
|
|
85
|
+
if (isOrGroup(term))
|
|
86
|
+
return {
|
|
87
|
+
any: term.any.map((c, j) => j === predIndex ? negateOne(c) : c)
|
|
88
|
+
};
|
|
89
|
+
return negateOne(term);
|
|
90
|
+
})
|
|
91
|
+
})),
|
|
92
|
+
[setQuery, negateOne]
|
|
93
|
+
);
|
|
94
|
+
const reorderFilters = useCallback(
|
|
95
|
+
(fromIndex, toIndex) => setQuery((q) => {
|
|
96
|
+
if (fromIndex < 0 || fromIndex >= q.where.length) return q;
|
|
97
|
+
const where = [...q.where];
|
|
98
|
+
const [moved] = where.splice(fromIndex, 1);
|
|
99
|
+
where.splice(Math.max(0, Math.min(toIndex, where.length)), 0, moved);
|
|
100
|
+
return { ...q, offset: 0, where };
|
|
101
|
+
}),
|
|
102
|
+
[setQuery]
|
|
103
|
+
);
|
|
104
|
+
const mergeFilters = useCallback(
|
|
105
|
+
(sourceIndex, targetIndex) => setQuery((q) => {
|
|
106
|
+
if (sourceIndex === targetIndex) return q;
|
|
107
|
+
const source = q.where[sourceIndex];
|
|
108
|
+
const target = q.where[targetIndex];
|
|
109
|
+
if (!source || !target) return q;
|
|
110
|
+
const merged = {
|
|
111
|
+
any: [...predicatesOf(target), ...predicatesOf(source)]
|
|
112
|
+
};
|
|
113
|
+
const where = [];
|
|
114
|
+
q.where.forEach((term, i) => {
|
|
115
|
+
if (i === sourceIndex) return;
|
|
116
|
+
where.push(i === targetIndex ? merged : term);
|
|
117
|
+
});
|
|
118
|
+
return { ...q, offset: 0, where };
|
|
119
|
+
}),
|
|
120
|
+
[setQuery]
|
|
121
|
+
);
|
|
122
|
+
return {
|
|
123
|
+
addFilter,
|
|
124
|
+
updateFilter,
|
|
125
|
+
removeFilter,
|
|
126
|
+
clearFilters,
|
|
127
|
+
updatePredicate,
|
|
128
|
+
removePredicate,
|
|
129
|
+
negatePredicate,
|
|
130
|
+
reorderFilters,
|
|
131
|
+
mergeFilters
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/useGrouping.ts
|
|
136
|
+
import { useEffect, useRef, useState } from "react";
|
|
137
|
+
import {
|
|
138
|
+
MAX_SUMMARY_COLUMNS,
|
|
139
|
+
normalizeQueryState,
|
|
140
|
+
predicatesOf as predicatesOf2,
|
|
141
|
+
withoutGrouping,
|
|
142
|
+
queriesEqual
|
|
143
|
+
} from "@pythia-software/query-table-core";
|
|
144
|
+
function useGroupingController(api, source, model, inputs, supported, viewRevision, execution = {}) {
|
|
145
|
+
const [notice, setNotice] = useState("");
|
|
146
|
+
useEffect(() => setNotice(""), [viewRevision]);
|
|
147
|
+
const previous = useRef(null);
|
|
148
|
+
const stage = api.query.grouping;
|
|
149
|
+
const view = {
|
|
150
|
+
version: 2,
|
|
151
|
+
mode: stage ? "groups" : "rows",
|
|
152
|
+
keys: stage?.keys ?? [],
|
|
153
|
+
summaries: stage?.summaries ?? [],
|
|
154
|
+
representative: stage?.representative ?? null,
|
|
155
|
+
input: source.query,
|
|
156
|
+
result: withoutGrouping(api.query)
|
|
157
|
+
};
|
|
158
|
+
const outputSchema = {
|
|
159
|
+
...api.schema,
|
|
160
|
+
fields: api.computed.catalogue
|
|
161
|
+
};
|
|
162
|
+
function changeKeys(keys) {
|
|
163
|
+
if (keys.length && !supported) return;
|
|
164
|
+
const cached = previous.current?.revision === viewRevision ? previous.current.query : null;
|
|
165
|
+
const canRestore = (current) => !current.grouping && cached?.grouping && queriesEqual(cached.grouping.source, current) && JSON.stringify(cached.grouping.keys) === JSON.stringify(keys);
|
|
166
|
+
if (keys.length)
|
|
167
|
+
model.validate(canRestore(api.query) ? cached.grouping : {
|
|
168
|
+
...stage ?? { summaries: [], representative: null },
|
|
169
|
+
keys
|
|
170
|
+
});
|
|
171
|
+
if (!keys.length && stage)
|
|
172
|
+
previous.current = { query: api.query, revision: viewRevision };
|
|
173
|
+
api.setQuery((current) => {
|
|
174
|
+
const before = current.grouping;
|
|
175
|
+
if (!keys.length) return before ? before.source : current;
|
|
176
|
+
if (!before) {
|
|
177
|
+
if (cached?.grouping && canRestore(current))
|
|
178
|
+
return {
|
|
179
|
+
...cached,
|
|
180
|
+
grouping: { ...cached.grouping, source: current }
|
|
181
|
+
};
|
|
182
|
+
const grouping2 = {
|
|
183
|
+
keys,
|
|
184
|
+
summaries: [],
|
|
185
|
+
representative: null,
|
|
186
|
+
source: current
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
select: keys.map((field) => ({ field })),
|
|
190
|
+
where: [],
|
|
191
|
+
orderBy: [],
|
|
192
|
+
limit: current.limit,
|
|
193
|
+
offset: 0,
|
|
194
|
+
grouping: grouping2
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const grouping = { ...before, keys };
|
|
198
|
+
const removed = new Set(before.keys.filter((key) => !keys.includes(key)));
|
|
199
|
+
const columns = current.select.length ? current.select : before.keys.map((field) => ({ field }));
|
|
200
|
+
return {
|
|
201
|
+
...current,
|
|
202
|
+
grouping,
|
|
203
|
+
offset: 0,
|
|
204
|
+
aggregations: [],
|
|
205
|
+
select: [
|
|
206
|
+
...keys.filter((key) => !before.keys.includes(key)).map((field) => ({ field })),
|
|
207
|
+
...columns.filter((c) => !removed.has(c.field))
|
|
208
|
+
],
|
|
209
|
+
where: current.where.filter(
|
|
210
|
+
(term) => !predicatesOf2(term).some((p) => removed.has(p.field))
|
|
211
|
+
),
|
|
212
|
+
orderBy: current.orderBy.filter((term) => !removed.has(term.field))
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
setNotice(
|
|
216
|
+
stage && api.query.aggregations?.length && keys.length ? "Grouping changed. Group metrics cleared; Undo restores them." : ""
|
|
217
|
+
);
|
|
218
|
+
api.selection.clear();
|
|
219
|
+
}
|
|
220
|
+
function applyColumns(summaries, columns) {
|
|
221
|
+
if (!stage) {
|
|
222
|
+
api.setQuery((q) => ({ ...q, select: columns }));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
model.validate({ ...stage, summaries });
|
|
226
|
+
const changed = new Set(
|
|
227
|
+
stage.summaries.filter((before) => {
|
|
228
|
+
const after = summaries.find((column) => column.id === before.id);
|
|
229
|
+
return !after || before.op !== after.op || before.field !== after.field || before.expression !== after.expression;
|
|
230
|
+
}).map((column) => column.id)
|
|
231
|
+
);
|
|
232
|
+
api.setQuery(
|
|
233
|
+
(q) => q.grouping ? {
|
|
234
|
+
...q,
|
|
235
|
+
grouping: { ...q.grouping, summaries },
|
|
236
|
+
select: columns,
|
|
237
|
+
...changed.size ? {
|
|
238
|
+
offset: 0,
|
|
239
|
+
aggregations: [],
|
|
240
|
+
where: q.where.filter(
|
|
241
|
+
(term) => !predicatesOf2(term).some((p) => changed.has(p.field))
|
|
242
|
+
),
|
|
243
|
+
orderBy: q.orderBy.filter((term) => !changed.has(term.field))
|
|
244
|
+
} : {}
|
|
245
|
+
} : q
|
|
246
|
+
);
|
|
247
|
+
setNotice(
|
|
248
|
+
changed.size && (api.query.where.some(
|
|
249
|
+
(term) => predicatesOf2(term).some((p) => changed.has(p.field))
|
|
250
|
+
) || api.query.orderBy.some((term) => changed.has(term.field)) || api.query.aggregations?.length) ? "Columns updated. Affected filters, sorts, and metrics cleared; Undo restores them." : ""
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
function addSummary(op, field) {
|
|
254
|
+
if (!stage || op === "row" && !stage.representative) return;
|
|
255
|
+
const existing = stage.summaries.find(
|
|
256
|
+
(column2) => column2.op === op && column2.field === field
|
|
257
|
+
);
|
|
258
|
+
if (!existing && stage.summaries.length >= MAX_SUMMARY_COLUMNS) {
|
|
259
|
+
setNotice(
|
|
260
|
+
`A grouped view supports up to ${MAX_SUMMARY_COLUMNS} aggregate columns.`
|
|
261
|
+
);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const column = existing ?? {
|
|
265
|
+
id: `summary_${crypto.randomUUID().replace(/-/g, "")}`,
|
|
266
|
+
op,
|
|
267
|
+
...field ? { field } : {}
|
|
268
|
+
};
|
|
269
|
+
applyColumns(
|
|
270
|
+
existing ? stage.summaries : [...stage.summaries, column],
|
|
271
|
+
api.select.visible.some((c) => c.field === column.id) ? api.select.visible : [...api.select.visible, { field: column.id }]
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
function changeSummary(id, op) {
|
|
275
|
+
if (!stage) return;
|
|
276
|
+
applyColumns(
|
|
277
|
+
stage.summaries.map(
|
|
278
|
+
(column) => column.id === id ? { ...column, op } : column
|
|
279
|
+
),
|
|
280
|
+
api.select.visible
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
function representativeImpact() {
|
|
284
|
+
if (!stage) return "";
|
|
285
|
+
const before = api.query;
|
|
286
|
+
const after = model.reconcileRepresentative(
|
|
287
|
+
before,
|
|
288
|
+
stage.summaries,
|
|
289
|
+
outputSchema,
|
|
290
|
+
false
|
|
291
|
+
);
|
|
292
|
+
return [
|
|
293
|
+
before.where.length !== after.where.length ? "row-value filters" : "",
|
|
294
|
+
before.orderBy.length !== after.orderBy.length ? "row-value sorts" : "",
|
|
295
|
+
(before.aggregations?.length ?? 0) !== (after.aggregations?.length ?? 0) ? "dependent metrics" : ""
|
|
296
|
+
].filter(Boolean).join(", ");
|
|
297
|
+
}
|
|
298
|
+
function changeRepresentative(representative) {
|
|
299
|
+
if (!stage || JSON.stringify(stage.representative) === JSON.stringify(representative))
|
|
300
|
+
return;
|
|
301
|
+
const summaries = representative ? stage.summaries : stage.summaries.filter((column) => column.op !== "row");
|
|
302
|
+
model.validate({ ...stage, summaries, representative });
|
|
303
|
+
const impact = representativeImpact();
|
|
304
|
+
api.setQuery(
|
|
305
|
+
(q) => q.grouping ? normalizeQueryState({
|
|
306
|
+
...model.reconcileRepresentative(
|
|
307
|
+
q,
|
|
308
|
+
stage.summaries,
|
|
309
|
+
outputSchema,
|
|
310
|
+
!representative
|
|
311
|
+
),
|
|
312
|
+
grouping: { ...q.grouping, summaries, representative }
|
|
313
|
+
}) : q
|
|
314
|
+
);
|
|
315
|
+
setNotice(
|
|
316
|
+
impact ? `Representative row changed. Cleared ${impact}; Undo restores them.` : ""
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
...execution,
|
|
321
|
+
supported,
|
|
322
|
+
model,
|
|
323
|
+
api,
|
|
324
|
+
source,
|
|
325
|
+
grouped: api,
|
|
326
|
+
schema: api.schema,
|
|
327
|
+
view,
|
|
328
|
+
inputs,
|
|
329
|
+
notice,
|
|
330
|
+
setNotice,
|
|
331
|
+
changeKeys,
|
|
332
|
+
addSummary,
|
|
333
|
+
changeSummary,
|
|
334
|
+
applyColumns,
|
|
335
|
+
changeRepresentative,
|
|
336
|
+
representativeImpact
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/useQueryTable.ts
|
|
341
|
+
import {
|
|
342
|
+
createGroupingModel,
|
|
343
|
+
prepareGrouping,
|
|
344
|
+
withoutGrouping as withoutGrouping2,
|
|
345
|
+
sourceExecutionSchema,
|
|
346
|
+
toQueryRequestV3,
|
|
347
|
+
validateQueryResultV3
|
|
348
|
+
} from "@pythia-software/query-table-core";
|
|
349
|
+
import { ITEM_DETAILS_COLUMN, itemPageFields } from "@pythia-software/query-table-core";
|
|
350
|
+
|
|
351
|
+
// src/useItemDetails.ts
|
|
352
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
353
|
+
|
|
354
|
+
// src/usePropertyView.ts
|
|
355
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
|
|
356
|
+
import {
|
|
357
|
+
createPropertyView,
|
|
358
|
+
memoryPropertyViewStorage,
|
|
359
|
+
normalizePropertyView
|
|
360
|
+
} from "@pythia-software/query-table-core";
|
|
361
|
+
var asError = (error) => error instanceof Error ? error : new Error(String(error));
|
|
362
|
+
function usePropertyView(options) {
|
|
363
|
+
const [memory] = useState2(memoryPropertyViewStorage);
|
|
364
|
+
const storage = options.storage ?? memory;
|
|
365
|
+
const key = options.key ?? options.schema.name;
|
|
366
|
+
const schemaDefault = useMemo2(() => createPropertyView(options.schema), [options.schema]);
|
|
367
|
+
const fallback = useMemo2(
|
|
368
|
+
() => options.initialView ? normalizePropertyView(options.initialView) : schemaDefault,
|
|
369
|
+
[options.initialView, schemaDefault]
|
|
370
|
+
);
|
|
371
|
+
const defaults = useRef2(schemaDefault);
|
|
372
|
+
defaults.current = schemaDefault;
|
|
373
|
+
const initial = useRef2(options.initialView);
|
|
374
|
+
initial.current = options.initialView;
|
|
375
|
+
const [state, setState] = useState2({
|
|
376
|
+
key,
|
|
377
|
+
storage,
|
|
378
|
+
history: [fallback],
|
|
379
|
+
index: 0,
|
|
380
|
+
loading: true,
|
|
381
|
+
error: null,
|
|
382
|
+
saved: [],
|
|
383
|
+
defaultId: null
|
|
384
|
+
});
|
|
385
|
+
const generation = useRef2(0);
|
|
386
|
+
const edits = useRef2(0);
|
|
387
|
+
const saveQueue = useRef2(Promise.resolve());
|
|
388
|
+
const current = state.key === key && state.storage === storage;
|
|
389
|
+
const view = current ? state.history[state.index] : fallback;
|
|
390
|
+
useEffect2(() => {
|
|
391
|
+
const token = ++generation.current;
|
|
392
|
+
const editToken = edits.current;
|
|
393
|
+
setState({
|
|
394
|
+
key,
|
|
395
|
+
storage,
|
|
396
|
+
history: [defaults.current],
|
|
397
|
+
index: 0,
|
|
398
|
+
loading: true,
|
|
399
|
+
error: null,
|
|
400
|
+
saved: [],
|
|
401
|
+
defaultId: null
|
|
402
|
+
});
|
|
403
|
+
void Promise.resolve().then(
|
|
404
|
+
() => Promise.all([
|
|
405
|
+
storage.loadLast(key),
|
|
406
|
+
storage.listSaved(key),
|
|
407
|
+
storage.loadDefaultSaved(key)
|
|
408
|
+
])
|
|
409
|
+
).then(([last, saved, defaultView]) => {
|
|
410
|
+
if (generation.current !== token) return;
|
|
411
|
+
const restored = normalizePropertyView(
|
|
412
|
+
initial.current ?? last ?? defaultView?.view ?? defaults.current
|
|
413
|
+
);
|
|
414
|
+
const items = saved.map((item) => ({
|
|
415
|
+
...item,
|
|
416
|
+
view: normalizePropertyView(item.view)
|
|
417
|
+
}));
|
|
418
|
+
setState((previous) => ({
|
|
419
|
+
...previous,
|
|
420
|
+
...edits.current === editToken ? { history: [restored], index: 0 } : {},
|
|
421
|
+
saved: items,
|
|
422
|
+
defaultId: defaultView?.id ?? null,
|
|
423
|
+
loading: false
|
|
424
|
+
}));
|
|
425
|
+
}).catch((error) => {
|
|
426
|
+
if (generation.current === token)
|
|
427
|
+
setState((previous) => ({
|
|
428
|
+
...previous,
|
|
429
|
+
loading: false,
|
|
430
|
+
error: asError(error)
|
|
431
|
+
}));
|
|
432
|
+
});
|
|
433
|
+
return () => {
|
|
434
|
+
generation.current++;
|
|
435
|
+
};
|
|
436
|
+
}, [key, storage]);
|
|
437
|
+
useEffect2(() => {
|
|
438
|
+
if (!current || state.loading || state.error) return;
|
|
439
|
+
const token = generation.current;
|
|
440
|
+
const snapshot = normalizePropertyView(view);
|
|
441
|
+
saveQueue.current = saveQueue.current.then(() => storage.saveLast(key, snapshot)).catch((error) => {
|
|
442
|
+
if (generation.current === token)
|
|
443
|
+
setState((previous) => ({ ...previous, error: asError(error) }));
|
|
444
|
+
});
|
|
445
|
+
}, [view, current, state.loading, state.error, key, storage]);
|
|
446
|
+
const setView = useCallback2(
|
|
447
|
+
(next) => {
|
|
448
|
+
const normalized = normalizePropertyView(next);
|
|
449
|
+
edits.current++;
|
|
450
|
+
setState((previous) => {
|
|
451
|
+
const history = previous.key === key && previous.storage === storage ? previous.history.slice(0, previous.index + 1) : [defaults.current];
|
|
452
|
+
if (JSON.stringify(history[history.length - 1]) === JSON.stringify(normalized))
|
|
453
|
+
return { ...previous, error: null };
|
|
454
|
+
const nextHistory = [...history, normalized].slice(-50);
|
|
455
|
+
return {
|
|
456
|
+
...previous,
|
|
457
|
+
key,
|
|
458
|
+
storage,
|
|
459
|
+
history: nextHistory,
|
|
460
|
+
index: nextHistory.length - 1,
|
|
461
|
+
error: null
|
|
462
|
+
};
|
|
463
|
+
});
|
|
464
|
+
},
|
|
465
|
+
[key, storage]
|
|
466
|
+
);
|
|
467
|
+
const refresh = useCallback2(
|
|
468
|
+
async (token) => {
|
|
469
|
+
const [saved, defaultView] = await Promise.all([
|
|
470
|
+
storage.listSaved(key),
|
|
471
|
+
storage.loadDefaultSaved(key)
|
|
472
|
+
]);
|
|
473
|
+
if (generation.current === token)
|
|
474
|
+
setState((previous) => ({
|
|
475
|
+
...previous,
|
|
476
|
+
saved: saved.map((item) => ({
|
|
477
|
+
...item,
|
|
478
|
+
view: normalizePropertyView(item.view)
|
|
479
|
+
})),
|
|
480
|
+
defaultId: defaultView?.id ?? null,
|
|
481
|
+
error: null
|
|
482
|
+
}));
|
|
483
|
+
},
|
|
484
|
+
[key, storage]
|
|
485
|
+
);
|
|
486
|
+
return {
|
|
487
|
+
view,
|
|
488
|
+
loading: !current || state.loading,
|
|
489
|
+
error: current ? state.error : null,
|
|
490
|
+
saved: current ? state.saved : [],
|
|
491
|
+
defaultId: current ? state.defaultId : null,
|
|
492
|
+
setView,
|
|
493
|
+
reset: () => setView(defaults.current),
|
|
494
|
+
canUndo: current && state.index > 0,
|
|
495
|
+
canRedo: current && state.index < state.history.length - 1,
|
|
496
|
+
undo() {
|
|
497
|
+
edits.current++;
|
|
498
|
+
setState((previous) => ({
|
|
499
|
+
...previous,
|
|
500
|
+
index: Math.max(0, previous.index - 1),
|
|
501
|
+
error: null
|
|
502
|
+
}));
|
|
503
|
+
},
|
|
504
|
+
redo() {
|
|
505
|
+
edits.current++;
|
|
506
|
+
setState((previous) => ({
|
|
507
|
+
...previous,
|
|
508
|
+
index: Math.min(previous.history.length - 1, previous.index + 1),
|
|
509
|
+
error: null
|
|
510
|
+
}));
|
|
511
|
+
},
|
|
512
|
+
load(id) {
|
|
513
|
+
const found = state.saved.find((item) => item.id === id);
|
|
514
|
+
if (found && current) setView(found.view);
|
|
515
|
+
},
|
|
516
|
+
async save(name) {
|
|
517
|
+
const token = generation.current;
|
|
518
|
+
const saved = await storage.saveNamed(key, name, view, Date.now());
|
|
519
|
+
await refresh(token);
|
|
520
|
+
return saved;
|
|
521
|
+
},
|
|
522
|
+
async remove(id) {
|
|
523
|
+
const token = generation.current;
|
|
524
|
+
await storage.deleteSaved(key, id);
|
|
525
|
+
await refresh(token);
|
|
526
|
+
},
|
|
527
|
+
async setDefault(id) {
|
|
528
|
+
const token = generation.current;
|
|
529
|
+
await storage.setDefaultSaved(key, id);
|
|
530
|
+
await refresh(token);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/useItemDetails.ts
|
|
536
|
+
var ALL_MODES = ["hover", "modal", "expand", "page"];
|
|
537
|
+
function initialDetailModes(config) {
|
|
538
|
+
const available = ALL_MODES.filter((mode) => !config?.modes || config.modes.includes(mode));
|
|
539
|
+
return config?.defaultModes ? available.filter((mode) => config.defaultModes.includes(mode)) : available.includes("modal") ? ["modal"] : available.slice(0, 1);
|
|
540
|
+
}
|
|
541
|
+
function useDetailProperties(mode, schema, config, query, setQuery, enabled) {
|
|
542
|
+
const snapshot = query.itemDetails?.views?.[mode] ?? query.itemDetails?.view;
|
|
543
|
+
const seed = useRef3(snapshot);
|
|
544
|
+
const properties = usePropertyView({
|
|
545
|
+
schema,
|
|
546
|
+
...config?.storage ? { storage: config.storage } : {},
|
|
547
|
+
key: `${config?.objectType ?? schema.name}:${mode}`,
|
|
548
|
+
...seed.current ? { initialView: seed.current } : {}
|
|
549
|
+
});
|
|
550
|
+
const queryViewKey = JSON.stringify(snapshot);
|
|
551
|
+
const localViewKey = JSON.stringify(properties.view);
|
|
552
|
+
const previousView = useRef3({ query: queryViewKey, local: localViewKey });
|
|
553
|
+
const baselineView = useRef3(properties.view);
|
|
554
|
+
const baselineReady = useRef3(false);
|
|
555
|
+
const pendingView = useRef3(null);
|
|
556
|
+
useEffect3(() => {
|
|
557
|
+
if (!enabled) return;
|
|
558
|
+
if (!properties.loading && !baselineReady.current) {
|
|
559
|
+
baselineView.current = properties.view;
|
|
560
|
+
baselineReady.current = true;
|
|
561
|
+
}
|
|
562
|
+
const previous = previousView.current;
|
|
563
|
+
previousView.current = { query: queryViewKey, local: localViewKey };
|
|
564
|
+
if (queryViewKey !== previous.query) {
|
|
565
|
+
const next = snapshot ?? baselineView.current;
|
|
566
|
+
const target = JSON.stringify(next);
|
|
567
|
+
pendingView.current = target !== localViewKey ? { query: queryViewKey, local: target } : null;
|
|
568
|
+
if (target !== localViewKey) properties.setView(next);
|
|
569
|
+
} else if (pendingView.current && pendingView.current.query === queryViewKey) {
|
|
570
|
+
if (pendingView.current.local === localViewKey) pendingView.current = null;
|
|
571
|
+
} else if (!properties.loading && localViewKey !== queryViewKey && (queryViewKey !== void 0 || localViewKey !== previous.local)) {
|
|
572
|
+
setQuery((current) => ({ ...current, itemDetails: {
|
|
573
|
+
modes: current.itemDetails?.modes ?? initialDetailModes(config),
|
|
574
|
+
...current.itemDetails,
|
|
575
|
+
views: { ...current.itemDetails?.views, [mode]: properties.view }
|
|
576
|
+
} }));
|
|
577
|
+
}
|
|
578
|
+
}, [enabled, queryViewKey, localViewKey, properties.loading, properties.setView, setQuery]);
|
|
579
|
+
return properties;
|
|
580
|
+
}
|
|
581
|
+
function useItemDetails({
|
|
582
|
+
schema,
|
|
583
|
+
query,
|
|
584
|
+
setQuery,
|
|
585
|
+
options,
|
|
586
|
+
rows,
|
|
587
|
+
clientRows,
|
|
588
|
+
rowId,
|
|
589
|
+
transport,
|
|
590
|
+
refreshKey,
|
|
591
|
+
links,
|
|
592
|
+
queryReady = true,
|
|
593
|
+
queryParameter: queryParameter2
|
|
594
|
+
}) {
|
|
595
|
+
const enabled = Boolean(options);
|
|
596
|
+
const config = typeof options === "object" ? options : void 0;
|
|
597
|
+
const detailSchema = config?.schema ?? schema;
|
|
598
|
+
const availableModes = ALL_MODES.filter((mode) => !config?.modes || config.modes.includes(mode));
|
|
599
|
+
const modes = availableModes.filter((mode) => (query.itemDetails?.modes ?? initialDetailModes(config)).includes(mode));
|
|
600
|
+
const page = query.itemDetails?.page ?? config?.page ?? { urlTemplate: "", target: "_blank" };
|
|
601
|
+
const requested = query.itemDetails?.active;
|
|
602
|
+
const sharedModal = requested?.mode === "modal" && query.itemDetails?.modes.includes("modal") && (availableModes.includes("hover") || availableModes.includes("page") && !page.urlTemplate.trim());
|
|
603
|
+
const active = requested && (modes.includes(requested.mode) || sharedModal) ? requested : null;
|
|
604
|
+
const syncProperties = enabled && queryReady;
|
|
605
|
+
const modalProperties = useDetailProperties("modal", detailSchema, config, query, setQuery, syncProperties);
|
|
606
|
+
const hoverProperties = useDetailProperties("hover", detailSchema, config, query, setQuery, syncProperties);
|
|
607
|
+
const expandProperties = useDetailProperties("expand", detailSchema, config, query, setQuery, syncProperties);
|
|
608
|
+
const propertiesByMode = { modal: modalProperties, hover: hoverProperties, expand: expandProperties };
|
|
609
|
+
const views = {
|
|
610
|
+
modal: modalProperties.view,
|
|
611
|
+
hover: hoverProperties.view,
|
|
612
|
+
expand: expandProperties.view
|
|
613
|
+
};
|
|
614
|
+
const customLoad = config?.loadRow;
|
|
615
|
+
const customLoadRef = useRef3(customLoad);
|
|
616
|
+
customLoadRef.current = customLoad;
|
|
617
|
+
const hasCustomLoad = Boolean(customLoad);
|
|
618
|
+
const sourceType = config?.objectType ?? detailSchema.name;
|
|
619
|
+
const fallbackRows = !customLoad && !clientRows && !transport?.fetchRow ? rows : void 0;
|
|
620
|
+
const loadRow = useCallback3(async (id, signal) => {
|
|
621
|
+
if (customLoadRef.current) return customLoadRef.current(id, signal);
|
|
622
|
+
if (clientRows) {
|
|
623
|
+
const row2 = clientRows.find((row3) => rowId(row3) === id);
|
|
624
|
+
return row2 ? { row: row2 } : null;
|
|
625
|
+
}
|
|
626
|
+
if (transport?.fetchRow) {
|
|
627
|
+
const row2 = await transport.fetchRow(id, signal);
|
|
628
|
+
return row2 ? { row: row2 } : null;
|
|
629
|
+
}
|
|
630
|
+
const row = rows.find((row2) => rowId(row2) === id);
|
|
631
|
+
return row ? { row, loadedFields: Object.keys(row) } : null;
|
|
632
|
+
}, [hasCustomLoad, sourceType, clientRows, transport?.fetchRow, fallbackRows, rowId]);
|
|
633
|
+
const [retryKey, setRetryKey] = useState3(0);
|
|
634
|
+
const [result, setResult] = useState3({ source: void 0, epoch: "", id: void 0, record: null, loading: false, error: null });
|
|
635
|
+
const activeId = enabled ? active?.id : void 0;
|
|
636
|
+
const epoch = `${refreshKey}:${retryKey}`;
|
|
637
|
+
const currentResult = result.id === activeId && result.source === loadRow && result.epoch === epoch;
|
|
638
|
+
useEffect3(() => {
|
|
639
|
+
const controller = new AbortController();
|
|
640
|
+
if (activeId === void 0) {
|
|
641
|
+
setResult((previous) => previous.id === void 0 && !previous.loading && !previous.error && !previous.record ? previous : { source: void 0, epoch: "", id: void 0, record: null, loading: false, error: null });
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
setResult({ source: loadRow, epoch, id: activeId, record: null, loading: true, error: null });
|
|
645
|
+
void Promise.resolve().then(() => loadRow(activeId, controller.signal)).then((record) => {
|
|
646
|
+
if (!controller.signal.aborted) setResult({ source: loadRow, epoch, id: activeId, record, loading: false, error: null });
|
|
647
|
+
}).catch((error) => {
|
|
648
|
+
if (!controller.signal.aborted) setResult({
|
|
649
|
+
source: loadRow,
|
|
650
|
+
epoch,
|
|
651
|
+
id: activeId,
|
|
652
|
+
record: null,
|
|
653
|
+
loading: false,
|
|
654
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
return () => controller.abort();
|
|
658
|
+
}, [activeId, loadRow, refreshKey, retryKey]);
|
|
659
|
+
if (!enabled) return;
|
|
660
|
+
return {
|
|
661
|
+
...links ? { links } : {},
|
|
662
|
+
schema: detailSchema,
|
|
663
|
+
queryParameter: queryParameter2 ?? config?.queryParameter ?? "q",
|
|
664
|
+
properties: modalProperties,
|
|
665
|
+
propertiesByMode,
|
|
666
|
+
availableModes,
|
|
667
|
+
modes,
|
|
668
|
+
active,
|
|
669
|
+
page,
|
|
670
|
+
pageSchema: schema,
|
|
671
|
+
record: currentResult ? result.record : null,
|
|
672
|
+
loading: activeId !== void 0 && (!currentResult || result.loading),
|
|
673
|
+
error: currentResult ? result.error : null,
|
|
674
|
+
...config?.relationships ? { relationships: config.relationships } : {},
|
|
675
|
+
loadRow,
|
|
676
|
+
retry: () => setRetryKey((value) => value + 1),
|
|
677
|
+
setModes(next) {
|
|
678
|
+
setQuery((current) => {
|
|
679
|
+
const allowed = availableModes.filter((mode) => next.includes(mode));
|
|
680
|
+
const state = { ...current.itemDetails, modes: allowed, views };
|
|
681
|
+
if (state.active && !allowed.includes(state.active.mode)) delete state.active;
|
|
682
|
+
return { ...current, itemDetails: state };
|
|
683
|
+
});
|
|
684
|
+
},
|
|
685
|
+
open(id, mode) {
|
|
686
|
+
if (!modes.includes(mode)) return;
|
|
687
|
+
setQuery((current) => ({ ...current, itemDetails: {
|
|
688
|
+
...current.itemDetails,
|
|
689
|
+
modes: current.itemDetails?.modes ?? modes,
|
|
690
|
+
views,
|
|
691
|
+
active: { id, mode }
|
|
692
|
+
} }));
|
|
693
|
+
},
|
|
694
|
+
close() {
|
|
695
|
+
setQuery((current) => {
|
|
696
|
+
if (!current.itemDetails?.active) return current;
|
|
697
|
+
const { active: _active, ...state } = current.itemDetails;
|
|
698
|
+
return { ...current, itemDetails: state };
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/computedExecution.ts
|
|
705
|
+
import {
|
|
706
|
+
isComputedField,
|
|
707
|
+
readFieldValue
|
|
708
|
+
} from "@pythia-software/query-table-core";
|
|
709
|
+
function executionRevision(execution, id) {
|
|
710
|
+
return execution?.resolvedRevisions[id] ?? execution?.resolvedRevisions[`@computed/${id}`];
|
|
711
|
+
}
|
|
712
|
+
function validateComputedResponse(request, result, schema) {
|
|
713
|
+
const execution = result.execution;
|
|
714
|
+
if (result.version !== 2 || !execution || execution.profile !== request.profile || request.planToken && execution.planToken !== request.planToken || request.snapshot !== void 0 && execution.snapshot !== request.snapshot)
|
|
715
|
+
throw Error(
|
|
716
|
+
"Server computed execution identity changed. Refresh the handshake."
|
|
717
|
+
);
|
|
718
|
+
const executed = /* @__PURE__ */ new Set([
|
|
719
|
+
...request.select.filter(isComputedField),
|
|
720
|
+
...request.orderBy.map((term) => term.field).filter(isComputedField)
|
|
721
|
+
]);
|
|
722
|
+
for (const [name, revision] of Object.entries(request.expectedRevisions)) {
|
|
723
|
+
const id = name.replace(/^@computed\//, "");
|
|
724
|
+
if (!executed.has(`@computed/${id}`)) continue;
|
|
725
|
+
if (executionRevision(execution, id) !== revision)
|
|
726
|
+
throw Error(
|
|
727
|
+
"Server computed definition revision changed. Refresh the handshake."
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
const idField = schema.fields.find((f) => f.name === schema.idField);
|
|
731
|
+
if (!idField)
|
|
732
|
+
throw Error("Stable row identity is required for computed values.");
|
|
733
|
+
const ids = new Set(result.rows.map((row) => readFieldValue(idField, row)));
|
|
734
|
+
if (ids.has(null) || ids.has(void 0) || ids.size !== result.rows.length)
|
|
735
|
+
throw Error(
|
|
736
|
+
"Computed response contains missing or duplicate row identities."
|
|
737
|
+
);
|
|
738
|
+
const sidecars = new Map(
|
|
739
|
+
result.computed.map((item) => [item.id, item.values])
|
|
740
|
+
);
|
|
741
|
+
if (sidecars.size !== result.computed.length || [...sidecars.keys()].some((id) => !ids.has(id)))
|
|
742
|
+
throw Error("Computed sidecars do not match returned row identities.");
|
|
743
|
+
for (const field of request.select.filter(isComputedField)) {
|
|
744
|
+
const id = field.slice("@computed/".length);
|
|
745
|
+
for (const rowId of ids) {
|
|
746
|
+
const value = sidecars.get(rowId)?.[id];
|
|
747
|
+
if (!value || value.value !== null && !["string", "number", "boolean"].includes(typeof value.value) || typeof value.value === "number" && !Number.isFinite(value.value))
|
|
748
|
+
throw Error(`Missing or invalid server computed result: ${field}.`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
1
753
|
// src/useComputedColumns.ts
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
754
|
+
import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
|
|
3
755
|
import {
|
|
4
756
|
applyQuery,
|
|
5
757
|
compileFormula,
|
|
6
758
|
computedFieldName,
|
|
7
759
|
groupPreview,
|
|
8
|
-
isComputedField,
|
|
760
|
+
isComputedField as isComputedField2,
|
|
9
761
|
memoryComputedColumnStore,
|
|
10
|
-
readFieldValue,
|
|
762
|
+
readFieldValue as readFieldValue2,
|
|
11
763
|
toServerQuery,
|
|
12
764
|
validateComputedColumn,
|
|
13
|
-
isSelectable
|
|
765
|
+
isSelectable,
|
|
766
|
+
hasRegex
|
|
14
767
|
} from "@pythia-software/query-table-core";
|
|
15
768
|
|
|
16
769
|
// src/formulaWorker.ts
|
|
@@ -51,9 +804,9 @@ function evaluateFormulaRows(ast, inputs, signal, factory = createFormulaWorker)
|
|
|
51
804
|
const cleanup = () => {
|
|
52
805
|
clearTimeout(timer);
|
|
53
806
|
worker.terminate();
|
|
54
|
-
signal?.removeEventListener("abort",
|
|
807
|
+
signal?.removeEventListener("abort", abort2);
|
|
55
808
|
};
|
|
56
|
-
const
|
|
809
|
+
const abort2 = () => {
|
|
57
810
|
cleanup();
|
|
58
811
|
reject(new DOMException("Aborted", "AbortError"));
|
|
59
812
|
};
|
|
@@ -82,7 +835,7 @@ function evaluateFormulaRows(ast, inputs, signal, factory = createFormulaWorker)
|
|
|
82
835
|
reject(e);
|
|
83
836
|
}
|
|
84
837
|
};
|
|
85
|
-
signal?.addEventListener("abort",
|
|
838
|
+
signal?.addEventListener("abort", abort2, { once: true });
|
|
86
839
|
worker.onerror = () => {
|
|
87
840
|
cleanup();
|
|
88
841
|
reject(new Error("Formula worker failed."));
|
|
@@ -103,18 +856,18 @@ function evaluateFormulaRows(ast, inputs, signal, factory = createFormulaWorker)
|
|
|
103
856
|
// src/useComputedColumns.ts
|
|
104
857
|
function useComputedColumns(options) {
|
|
105
858
|
const { schema, query, rows, transport, clientRows, workerFactory } = options;
|
|
106
|
-
const store =
|
|
859
|
+
const store = useMemo3(
|
|
107
860
|
() => options.store ?? memoryComputedColumnStore(),
|
|
108
861
|
[options.store]
|
|
109
862
|
);
|
|
110
|
-
const [catalogue, setCatalogue] =
|
|
111
|
-
const definitions =
|
|
863
|
+
const [catalogue, setCatalogue] = useState4(null);
|
|
864
|
+
const definitions = useMemo3(
|
|
112
865
|
() => catalogue?.dataset === schema.name && catalogue.store === store ? catalogue.items : [],
|
|
113
866
|
[catalogue, schema.name, store]
|
|
114
867
|
);
|
|
115
|
-
const [loading, setLoading] =
|
|
116
|
-
const catalogueRequest =
|
|
117
|
-
const reload =
|
|
868
|
+
const [loading, setLoading] = useState4(false), [error, setError] = useState4(null);
|
|
869
|
+
const catalogueRequest = useRef4(null);
|
|
870
|
+
const reload = useCallback4(async () => {
|
|
118
871
|
catalogueRequest.current?.abort();
|
|
119
872
|
const ac = new AbortController();
|
|
120
873
|
catalogueRequest.current = ac;
|
|
@@ -136,35 +889,35 @@ function useComputedColumns(options) {
|
|
|
136
889
|
if (!ac.signal.aborted) setLoading(false);
|
|
137
890
|
}
|
|
138
891
|
}, [store, schema.name]);
|
|
139
|
-
|
|
892
|
+
useEffect4(() => {
|
|
140
893
|
void reload();
|
|
141
894
|
return () => catalogueRequest.current?.abort();
|
|
142
895
|
}, [reload]);
|
|
143
|
-
|
|
896
|
+
useEffect4(
|
|
144
897
|
() => store.subscribe?.(schema.name, () => {
|
|
145
898
|
void reload();
|
|
146
899
|
}),
|
|
147
900
|
[store, schema.name, reload]
|
|
148
901
|
);
|
|
149
|
-
|
|
902
|
+
useEffect4(() => {
|
|
150
903
|
const focus = () => {
|
|
151
904
|
void reload();
|
|
152
905
|
};
|
|
153
906
|
window.addEventListener("focus", focus);
|
|
154
907
|
return () => window.removeEventListener("focus", focus);
|
|
155
908
|
}, [reload]);
|
|
156
|
-
const inputFields =
|
|
909
|
+
const inputFields = useMemo3(
|
|
157
910
|
() => schema.fields.filter(isSelectable).filter(
|
|
158
911
|
(f) => f.source.kind === "backend" || f.source.accessor && f.source.dependencies
|
|
159
912
|
),
|
|
160
913
|
[schema]
|
|
161
914
|
);
|
|
162
|
-
const compile =
|
|
915
|
+
const compile = useCallback4(
|
|
163
916
|
(source, editingId) => {
|
|
164
917
|
const stack = new Set(editingId ? [editingId] : []);
|
|
165
918
|
let expanded = 0;
|
|
166
919
|
const resolve = (name) => {
|
|
167
|
-
if (!
|
|
920
|
+
if (!isComputedField2(name)) return void 0;
|
|
168
921
|
const id = name.slice("@computed/".length), def = definitions.find((d) => d.id === id);
|
|
169
922
|
if (!def) throw new Error(`Computed column unavailable: ${id}`);
|
|
170
923
|
if (stack.has(id))
|
|
@@ -182,7 +935,7 @@ function useComputedColumns(options) {
|
|
|
182
935
|
},
|
|
183
936
|
[inputFields, definitions]
|
|
184
937
|
);
|
|
185
|
-
const plans =
|
|
938
|
+
const plans = useMemo3(
|
|
186
939
|
() => new Map(
|
|
187
940
|
definitions.map((def) => {
|
|
188
941
|
try {
|
|
@@ -200,7 +953,7 @@ function useComputedColumns(options) {
|
|
|
200
953
|
),
|
|
201
954
|
[definitions, compile]
|
|
202
955
|
);
|
|
203
|
-
const backendDependencies =
|
|
956
|
+
const backendDependencies = useCallback4(
|
|
204
957
|
(plan) => [
|
|
205
958
|
...new Set(
|
|
206
959
|
plan.dependencies.flatMap((name) => {
|
|
@@ -215,14 +968,14 @@ function useComputedColumns(options) {
|
|
|
215
968
|
).sort(),
|
|
216
969
|
[schema, inputFields]
|
|
217
970
|
);
|
|
218
|
-
const inputsFor =
|
|
971
|
+
const inputsFor = useCallback4(
|
|
219
972
|
(plan, batch) => {
|
|
220
973
|
let size = 0;
|
|
221
974
|
return batch.map((row) => {
|
|
222
975
|
const values = /* @__PURE__ */ Object.create(null);
|
|
223
976
|
for (const name of plan.dependencies) {
|
|
224
977
|
const field = inputFields.find((f) => f.name === name);
|
|
225
|
-
const raw =
|
|
978
|
+
const raw = readFieldValue2(field, row);
|
|
226
979
|
values[name] = raw instanceof Date ? raw.toISOString() : raw;
|
|
227
980
|
}
|
|
228
981
|
size += JSON.stringify(values).length;
|
|
@@ -236,13 +989,15 @@ function useComputedColumns(options) {
|
|
|
236
989
|
[inputFields]
|
|
237
990
|
);
|
|
238
991
|
const activeSelect = query.select.length ? query.select : schema.defaultSelect ?? [];
|
|
239
|
-
const activeIds = activeSelect.filter((c) =>
|
|
240
|
-
const [evaluated, setEvaluated] =
|
|
241
|
-
|
|
992
|
+
const activeIds = activeSelect.filter((c) => isComputedField2(c.field)).map((c) => c.field.slice("@computed/".length)).sort().join(",");
|
|
993
|
+
const [evaluated, setEvaluated] = useState4(null);
|
|
994
|
+
useEffect4(() => {
|
|
242
995
|
const ac = new AbortController();
|
|
243
996
|
const values = /* @__PURE__ */ new Map();
|
|
244
997
|
void (async () => {
|
|
245
998
|
for (const id of activeIds.split(",").filter(Boolean)) {
|
|
999
|
+
if (options.execution?.fields[computedFieldName(id)]?.select && (transport?.fetchRowsV2 || transport?.fetchQueryV3))
|
|
1000
|
+
continue;
|
|
246
1001
|
const entry = plans.get(id);
|
|
247
1002
|
if (!entry?.plan) continue;
|
|
248
1003
|
try {
|
|
@@ -269,8 +1024,16 @@ function useComputedColumns(options) {
|
|
|
269
1024
|
if (!ac.signal.aborted) setEvaluated({ rows, plans, values });
|
|
270
1025
|
})();
|
|
271
1026
|
return () => ac.abort();
|
|
272
|
-
}, [
|
|
273
|
-
|
|
1027
|
+
}, [
|
|
1028
|
+
activeIds,
|
|
1029
|
+
rows,
|
|
1030
|
+
plans,
|
|
1031
|
+
inputsFor,
|
|
1032
|
+
workerFactory,
|
|
1033
|
+
options.execution,
|
|
1034
|
+
transport
|
|
1035
|
+
]);
|
|
1036
|
+
const displaySchema = useMemo3(() => {
|
|
274
1037
|
const indices = new Map(rows.map((row, i) => [row, i]));
|
|
275
1038
|
const fields = definitions.map((def) => {
|
|
276
1039
|
const entry = plans.get(def.id);
|
|
@@ -285,17 +1048,34 @@ function useComputedColumns(options) {
|
|
|
285
1048
|
dependencies: entry?.plan ? backendDependencies(entry.plan) : [],
|
|
286
1049
|
accessor: (row) => {
|
|
287
1050
|
if (entry?.error) return { computedError: entry.error };
|
|
1051
|
+
if (options.execution?.fields[computedFieldName(def.id)]?.select && (transport?.fetchRowsV2 || transport?.fetchQueryV3)) {
|
|
1052
|
+
if (options.serverExecution?.profile !== options.execution.profile || options.serverExecution?.planToken !== options.execution.planToken || options.serverExecution?.snapshot !== options.execution.snapshot || executionRevision(options.execution, def.id) !== def.revision || executionRevision(options.serverExecution, def.id) !== def.revision)
|
|
1053
|
+
return {
|
|
1054
|
+
computedError: "Definition changed or values are pending. Refresh the server handshake."
|
|
1055
|
+
};
|
|
1056
|
+
const idField = schema.fields.find(
|
|
1057
|
+
(f) => f.name === schema.idField
|
|
1058
|
+
);
|
|
1059
|
+
const id = idField ? readFieldValue2(idField, row) : null;
|
|
1060
|
+
const server = id === null ? void 0 : options.serverComputed?.get(id)?.[def.id];
|
|
1061
|
+
return server ? server.error ? { computedError: server.error.code } : server.value : { computedError: "Server computed result unavailable." };
|
|
1062
|
+
}
|
|
288
1063
|
const result = evaluated?.rows === rows && evaluated.plans === plans ? evaluated.values.get(def.id)?.[indices.get(row) ?? -1] : void 0;
|
|
289
1064
|
return result?.error ? { computedError: result.error } : result ? result.value : { computedError: "Calculating\u2026" };
|
|
290
1065
|
}
|
|
291
1066
|
},
|
|
292
|
-
filter: { enabled:
|
|
293
|
-
sort: {
|
|
294
|
-
|
|
1067
|
+
filter: { enabled: options.execution?.fields[computedFieldName(def.id)]?.filter === true && executionRevision(options.execution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan) },
|
|
1068
|
+
sort: {
|
|
1069
|
+
enabled: !!(transport?.fetchRowsV2 || transport?.fetchQueryV3) && !!options.execution?.fields[computedFieldName(def.id)]?.sort && executionRevision(options.execution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan)
|
|
1070
|
+
},
|
|
1071
|
+
aggregate: {
|
|
1072
|
+
measure: !!entry?.plan,
|
|
1073
|
+
groupable: (!!transport?.fetchMetrics && transport.metricCapabilities?.version === 2 || !!transport?.fetchQueryV3) && !!options.execution?.fields[computedFieldName(def.id)]?.group && executionRevision(options.execution, def.id) === def.revision && !!entry?.plan && !hasRegex(entry.plan)
|
|
1074
|
+
}
|
|
295
1075
|
};
|
|
296
1076
|
});
|
|
297
1077
|
for (const col of activeSelect)
|
|
298
|
-
if (
|
|
1078
|
+
if (isComputedField2(col.field) && !fields.some((f) => f.name === col.field))
|
|
299
1079
|
fields.push({
|
|
300
1080
|
name: col.field,
|
|
301
1081
|
label: col.field.slice(10),
|
|
@@ -321,9 +1101,13 @@ function useComputedColumns(options) {
|
|
|
321
1101
|
rows,
|
|
322
1102
|
query.select,
|
|
323
1103
|
schema.defaultSelect,
|
|
324
|
-
loading
|
|
1104
|
+
loading,
|
|
1105
|
+
transport,
|
|
1106
|
+
options.execution,
|
|
1107
|
+
options.serverComputed,
|
|
1108
|
+
options.serverExecution
|
|
325
1109
|
]);
|
|
326
|
-
const save =
|
|
1110
|
+
const save = useCallback4(
|
|
327
1111
|
async (column, expectedRevision) => {
|
|
328
1112
|
compile(column.expression.source, column.id);
|
|
329
1113
|
const saved = validateComputedColumn(
|
|
@@ -341,7 +1125,7 @@ function useComputedColumns(options) {
|
|
|
341
1125
|
},
|
|
342
1126
|
[compile, store, schema.name]
|
|
343
1127
|
);
|
|
344
|
-
const preview =
|
|
1128
|
+
const preview = useCallback4(
|
|
345
1129
|
async (source, count, signal, editingId, includeInputs = true) => {
|
|
346
1130
|
if (!Number.isInteger(count) || count < 1 || count > 1e4)
|
|
347
1131
|
throw new Error("Choose between 1 and 10,000 rows.");
|
|
@@ -422,137 +1206,602 @@ function useComputedColumns(options) {
|
|
|
422
1206
|
}
|
|
423
1207
|
|
|
424
1208
|
// src/useQueryTable.ts
|
|
425
|
-
import { useCallback as
|
|
1209
|
+
import { useCallback as useCallback12, useEffect as useEffect10, useMemo as useMemo9, useRef as useRef9, useState as useState11 } from "react";
|
|
426
1210
|
import {
|
|
427
1211
|
EMPTY_QUERY,
|
|
428
1212
|
applyQuery as applyQuery2,
|
|
429
|
-
decodeQuery,
|
|
430
|
-
encodeQuery,
|
|
431
1213
|
memoryStorageAdapter,
|
|
432
|
-
normalizeQueryState,
|
|
433
|
-
readFieldValue as
|
|
1214
|
+
normalizeQueryState as normalizeQueryState3,
|
|
1215
|
+
readFieldValue as readFieldValue4,
|
|
434
1216
|
selectedFields as selectedFields2,
|
|
435
1217
|
toServerQuery as toServerQuery2,
|
|
436
|
-
queriesEqual,
|
|
437
|
-
isOrGroup,
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
1218
|
+
queriesEqual as queriesEqual2,
|
|
1219
|
+
isOrGroup as isOrGroup2,
|
|
1220
|
+
metricPlans as metricPlans3,
|
|
1221
|
+
validateMetric as validateMetric2,
|
|
1222
|
+
metricDependencies,
|
|
1223
|
+
isComputedField as isComputedField4,
|
|
1224
|
+
computedFieldName as computedFieldName2,
|
|
1225
|
+
toServerQueryV2
|
|
441
1226
|
} from "@pythia-software/query-table-core";
|
|
442
1227
|
|
|
1228
|
+
// src/useRequestActivity.ts
|
|
1229
|
+
import { useCallback as useCallback5, useEffect as useEffect5, useMemo as useMemo4, useRef as useRef5, useState as useState5 } from "react";
|
|
1230
|
+
var methods = /* @__PURE__ */ new Set([
|
|
1231
|
+
"fetchRows",
|
|
1232
|
+
"fetchRowsV2",
|
|
1233
|
+
"fetchQueryV3",
|
|
1234
|
+
"fetchMetrics",
|
|
1235
|
+
"fetchAggregations",
|
|
1236
|
+
"fetchRow",
|
|
1237
|
+
"fetchDistinctValues",
|
|
1238
|
+
"fetchFieldStats"
|
|
1239
|
+
]);
|
|
1240
|
+
function observeTransport(transport, publish, nextId) {
|
|
1241
|
+
const wrappers = /* @__PURE__ */ new Map();
|
|
1242
|
+
return new Proxy(Object.create(transport), {
|
|
1243
|
+
get(_target, key) {
|
|
1244
|
+
const method = Reflect.get(transport, key, transport);
|
|
1245
|
+
if (!methods.has(String(key)) || typeof method !== "function")
|
|
1246
|
+
return method;
|
|
1247
|
+
if (!wrappers.has(key))
|
|
1248
|
+
wrappers.set(key, async (...args) => {
|
|
1249
|
+
const signal = args[1];
|
|
1250
|
+
const start = performance.now();
|
|
1251
|
+
const entry = {
|
|
1252
|
+
id: nextId(),
|
|
1253
|
+
kind: String(key).replace(/^fetch/, ""),
|
|
1254
|
+
startedAt: Date.now(),
|
|
1255
|
+
status: "pending"
|
|
1256
|
+
};
|
|
1257
|
+
publish(entry);
|
|
1258
|
+
let done = false;
|
|
1259
|
+
const finish = (status, error) => {
|
|
1260
|
+
if (done) return;
|
|
1261
|
+
done = true;
|
|
1262
|
+
publish({
|
|
1263
|
+
...entry,
|
|
1264
|
+
status,
|
|
1265
|
+
durationMs: Math.max(0, performance.now() - start),
|
|
1266
|
+
...error !== void 0 ? {
|
|
1267
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1268
|
+
} : {}
|
|
1269
|
+
});
|
|
1270
|
+
};
|
|
1271
|
+
const abort2 = () => finish("aborted");
|
|
1272
|
+
signal?.addEventListener("abort", abort2, { once: true });
|
|
1273
|
+
if (signal?.aborted) abort2();
|
|
1274
|
+
try {
|
|
1275
|
+
const result = await method.apply(transport, args);
|
|
1276
|
+
finish(signal?.aborted ? "aborted" : "success");
|
|
1277
|
+
return result;
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
finish(
|
|
1280
|
+
signal?.aborted || error instanceof Error && error.name === "AbortError" ? "aborted" : "error",
|
|
1281
|
+
error
|
|
1282
|
+
);
|
|
1283
|
+
throw error;
|
|
1284
|
+
} finally {
|
|
1285
|
+
signal?.removeEventListener("abort", abort2);
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
return wrappers.get(key);
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
function useRequestActivity(source) {
|
|
1293
|
+
const [entries, setEntries] = useState5([]);
|
|
1294
|
+
const sequence = useRef5(0), mounted = useRef5(true);
|
|
1295
|
+
useEffect5(() => {
|
|
1296
|
+
mounted.current = true;
|
|
1297
|
+
return () => {
|
|
1298
|
+
mounted.current = false;
|
|
1299
|
+
};
|
|
1300
|
+
}, []);
|
|
1301
|
+
const publish = useCallback5((entry) => {
|
|
1302
|
+
if (mounted.current)
|
|
1303
|
+
setEntries(
|
|
1304
|
+
(previous) => [entry, ...previous.filter((e) => e.id !== entry.id)].sort((a, b) => b.id - a.id).slice(0, 100)
|
|
1305
|
+
);
|
|
1306
|
+
}, []);
|
|
1307
|
+
const transport = useMemo4(
|
|
1308
|
+
() => source ? observeTransport(source, publish, () => ++sequence.current) : void 0,
|
|
1309
|
+
[source, publish]
|
|
1310
|
+
);
|
|
1311
|
+
const clear = useCallback5(
|
|
1312
|
+
() => setEntries((previous) => previous.filter((e) => e.status === "pending")),
|
|
1313
|
+
[]
|
|
1314
|
+
);
|
|
1315
|
+
const activity = useMemo4(
|
|
1316
|
+
() => ({ entries, clear }),
|
|
1317
|
+
[entries, clear]
|
|
1318
|
+
);
|
|
1319
|
+
return { transport, activity };
|
|
1320
|
+
}
|
|
1321
|
+
|
|
443
1322
|
// src/useAggregations.ts
|
|
444
|
-
import { useEffect as
|
|
445
|
-
import {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
1323
|
+
import { useCallback as useCallback6, useEffect as useEffect6, useMemo as useMemo5, useRef as useRef6, useState as useState6 } from "react";
|
|
1324
|
+
import {
|
|
1325
|
+
metricComputationKey,
|
|
1326
|
+
metricPlans as metricPlans2,
|
|
1327
|
+
hasRegex as hasRegex3
|
|
1328
|
+
} from "@pythia-software/query-table-core";
|
|
1329
|
+
|
|
1330
|
+
// src/metricExecution.ts
|
|
1331
|
+
import {
|
|
1332
|
+
applyAggregations,
|
|
1333
|
+
normalizeQueryState as normalizeQueryState2,
|
|
1334
|
+
evaluateMetrics,
|
|
1335
|
+
metricPlans,
|
|
1336
|
+
hasRegex as hasRegex2,
|
|
1337
|
+
toAggregationQuery,
|
|
1338
|
+
toMetricQuery,
|
|
1339
|
+
validateMetric,
|
|
1340
|
+
selectMetricShownRows,
|
|
1341
|
+
readFieldValue as readFieldValue3,
|
|
1342
|
+
resolveFieldName,
|
|
1343
|
+
isComputedField as isComputedField3
|
|
1344
|
+
} from "@pythia-software/query-table-core";
|
|
1345
|
+
var abort = (signal) => {
|
|
1346
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
1347
|
+
};
|
|
1348
|
+
async function localMetrics(rows, query, context, signal) {
|
|
1349
|
+
const originalClauses = query.aggregations ?? [];
|
|
1350
|
+
const advanced = (query.aggregations ?? []).filter(
|
|
1351
|
+
(c) => context.strictLocalMetrics || c.dateGrouping || isComputedField3(c.field ?? "") || c.groupBy.some(isComputedField3) || c.diagnostics?.length || c.expression !== void 0 || c.expressionY !== void 0 || c.scope !== void 0 || c.distribution || c.sort || c.groupLimit !== void 0
|
|
1352
|
+
);
|
|
1353
|
+
if (advanced.length && (originalClauses.length > 20 || normalizeQueryState2(query).aggregations?.length !== originalClauses.length))
|
|
1354
|
+
return evaluateMetrics(rows, query, context.schema, {
|
|
1355
|
+
...context.now !== void 0 ? { now: context.now } : {},
|
|
1356
|
+
...signal ? { signal } : {},
|
|
1357
|
+
...context.resolveComputed ? { resolveComputed: context.resolveComputed } : {}
|
|
1358
|
+
});
|
|
1359
|
+
const legacy = (query.aggregations ?? []).filter(
|
|
1360
|
+
(c) => !advanced.includes(c)
|
|
1361
|
+
);
|
|
1362
|
+
const compatible = applyAggregations(
|
|
1363
|
+
rows,
|
|
1364
|
+
{ ...query, aggregations: legacy },
|
|
1365
|
+
context.schema,
|
|
1366
|
+
{
|
|
1367
|
+
...context.now !== void 0 ? { now: context.now } : {},
|
|
1368
|
+
...signal ? { signal } : {}
|
|
1369
|
+
}
|
|
1370
|
+
);
|
|
1371
|
+
if (!advanced.length) return compatible;
|
|
1372
|
+
query = { ...query, aggregations: advanced };
|
|
1373
|
+
const options = {
|
|
1374
|
+
...context.now !== void 0 ? { now: context.now } : {},
|
|
1375
|
+
...context.resolveComputed ? { resolveComputed: context.resolveComputed } : {},
|
|
1376
|
+
...signal ? { signal } : {}
|
|
1377
|
+
};
|
|
1378
|
+
for (const clause of query.aggregations ?? []) {
|
|
1379
|
+
const diagnostics = validateMetric(
|
|
1380
|
+
clause,
|
|
1381
|
+
context.schema,
|
|
1382
|
+
context.resolveComputed
|
|
1383
|
+
);
|
|
1384
|
+
if (diagnostics.length)
|
|
1385
|
+
throw Error(diagnostics.map((d) => d.message).join(" "));
|
|
1386
|
+
}
|
|
1387
|
+
const requiresPage = (query.aggregations ?? []).some(
|
|
1388
|
+
(clause) => clause.scope === "shownRows"
|
|
1389
|
+
);
|
|
1390
|
+
const page = requiresPage ? selectMetricShownRows(rows, query, context.schema, {
|
|
1391
|
+
...options,
|
|
1392
|
+
...context.rowsReady && context.shownRows ? { shownRows: context.shownRows } : {}
|
|
1393
|
+
}) : [];
|
|
1394
|
+
const caches = /* @__PURE__ */ new Map();
|
|
1395
|
+
for (const clause of query.aggregations ?? []) {
|
|
1396
|
+
for (const plan of metricPlans(
|
|
1397
|
+
clause,
|
|
1398
|
+
context.schema,
|
|
1399
|
+
context.resolveComputed
|
|
1400
|
+
)) {
|
|
1401
|
+
if (!hasRegex2(plan)) continue;
|
|
1402
|
+
const nodes = "aggregates" in plan ? plan.aggregates.flatMap((a) => a.args) : [plan.ast];
|
|
1403
|
+
for (const node of nodes) {
|
|
1404
|
+
if (!hasRegex2({ ...plan, ast: node })) continue;
|
|
1405
|
+
const key = JSON.stringify(node);
|
|
1406
|
+
if (caches.has(key)) continue;
|
|
1407
|
+
let bytes = 0;
|
|
1408
|
+
const inputs = page.map((row) => {
|
|
1409
|
+
const values2 = /* @__PURE__ */ Object.create(null);
|
|
1410
|
+
for (const name of plan.dependencies) {
|
|
1411
|
+
const field = context.schema.fields.find((f) => f.name === name);
|
|
1412
|
+
if (!field) throw Error(`Unavailable metric input: ${name}`);
|
|
1413
|
+
const raw = readFieldValue3(field, row);
|
|
1414
|
+
values2[name] = raw instanceof Date ? raw.toISOString() : raw ?? null;
|
|
1415
|
+
}
|
|
1416
|
+
bytes += JSON.stringify(values2).length;
|
|
1417
|
+
if (bytes > 16e6)
|
|
1418
|
+
throw Error(
|
|
1419
|
+
"Metric worker input exceeds 16 MB. Reduce the shown rows."
|
|
1420
|
+
);
|
|
1421
|
+
return values2;
|
|
1422
|
+
});
|
|
1423
|
+
const values = await evaluateFormulaRows(
|
|
1424
|
+
node,
|
|
1425
|
+
inputs,
|
|
1426
|
+
signal,
|
|
1427
|
+
context.workerFactory
|
|
1428
|
+
);
|
|
1429
|
+
caches.set(
|
|
1430
|
+
key,
|
|
1431
|
+
new Map(page.map((row, i) => [row, values[i]]))
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
abort(signal);
|
|
1437
|
+
const typed = evaluateMetrics(rows, query, context.schema, {
|
|
1438
|
+
...options,
|
|
1439
|
+
shownRows: page,
|
|
1440
|
+
...caches.size ? {
|
|
1441
|
+
evaluateInput: (node, row, plan) => {
|
|
1442
|
+
const cached = caches.get(JSON.stringify(node));
|
|
1443
|
+
if (cached)
|
|
1444
|
+
return cached.get(row) ?? {
|
|
1445
|
+
value: null,
|
|
1446
|
+
error: "Metric worker result missing."
|
|
1447
|
+
};
|
|
1448
|
+
throw Error("Metric regex input was not evaluated in a worker.");
|
|
1449
|
+
}
|
|
1450
|
+
} : {}
|
|
1451
|
+
});
|
|
1452
|
+
const byId = new Map(
|
|
1453
|
+
[...compatible.metrics, ...typed.metrics].map((m) => [m.id, m])
|
|
1454
|
+
);
|
|
1455
|
+
return { metrics: originalClauses.flatMap((c) => byId.get(c.id) ?? []) };
|
|
1456
|
+
}
|
|
1457
|
+
async function executeMetrics(query, context, signal) {
|
|
1458
|
+
abort(signal);
|
|
1459
|
+
const clauses = query.aggregations ?? [];
|
|
1460
|
+
if (!clauses.length) return { metrics: [] };
|
|
1461
|
+
if (context.executeV3) return context.executeV3(query, signal);
|
|
1462
|
+
const supportsV2 = !!context.transport?.fetchMetrics && context.transport.metricCapabilities?.version === 2;
|
|
1463
|
+
if (context.clientRows && !supportsV2 && !context.transport?.fetchAggregations)
|
|
1464
|
+
return localMetrics(context.clientRows, query, context, signal);
|
|
1465
|
+
const needsWorker = (c) => metricPlans(c, context.schema, context.resolveComputed).some(hasRegex2);
|
|
1466
|
+
const shown = clauses.filter(
|
|
1467
|
+
(c) => c.scope === "shownRows" && (!supportsV2 || needsWorker(c) || !!c.dateGrouping)
|
|
1468
|
+
);
|
|
1469
|
+
const matching = clauses.filter(
|
|
1470
|
+
(c) => c.scope !== "shownRows" || supportsV2 && !needsWorker(c) && !c.dateGrouping
|
|
1471
|
+
);
|
|
1472
|
+
const results = [];
|
|
1473
|
+
if (shown.length) {
|
|
1474
|
+
if (!context.rowsReady || !context.shownRows)
|
|
1475
|
+
throw Error(
|
|
1476
|
+
"Wait for the current table page before previewing shown-row metrics."
|
|
1477
|
+
);
|
|
1478
|
+
const names = new Set(
|
|
1479
|
+
shown.flatMap((clause) => [
|
|
1480
|
+
...clause.groupBy,
|
|
1481
|
+
...metricPlans(clause, context.schema, context.resolveComputed).flatMap(
|
|
1482
|
+
(p) => p.dependencies
|
|
1483
|
+
)
|
|
1484
|
+
])
|
|
1485
|
+
);
|
|
1486
|
+
for (const name of names) {
|
|
1487
|
+
const canonical = resolveFieldName(context.schema, name) ?? name;
|
|
1488
|
+
const field = context.schema.fields.find((f) => f.name === canonical);
|
|
1489
|
+
const dependencies = field?.source.kind === "derived" ? field.source.dependencies ?? [] : [canonical];
|
|
1490
|
+
const loaded = context.shownFields ? dependencies.every((dep) => context.shownFields.includes(dep)) : field && context.shownRows.every(
|
|
1491
|
+
(row) => field.source.kind === "derived" || loadedPath(
|
|
1492
|
+
row,
|
|
1493
|
+
field.source.kind === "backend" ? field.source.path ?? field.name : field.name
|
|
1494
|
+
)
|
|
1495
|
+
);
|
|
1496
|
+
if (!field || !loaded)
|
|
1497
|
+
throw Error(
|
|
1498
|
+
`Shown-page input ${name} is not loaded. Add it to the selected columns and refresh, or use a snapshot-capable metric transport.`
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
const pageQuery = {
|
|
1502
|
+
...query,
|
|
1503
|
+
orderBy: [],
|
|
1504
|
+
limit: Math.max(1, context.shownRows.length),
|
|
1505
|
+
offset: 0,
|
|
1506
|
+
aggregations: shown
|
|
1507
|
+
};
|
|
1508
|
+
results.push(
|
|
1509
|
+
...(await localMetrics(context.shownRows, pageQuery, context, signal)).metrics
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
if (matching.length) {
|
|
1513
|
+
if (matching.some((c) => c.dateGrouping))
|
|
1514
|
+
throw Error(
|
|
1515
|
+
"This server does not support calendar grouping. Enable the v3 query executor."
|
|
1516
|
+
);
|
|
1517
|
+
const transport = context.transport;
|
|
1518
|
+
const scoped = { ...query, aggregations: matching };
|
|
1519
|
+
if (transport?.fetchMetrics && transport.metricCapabilities?.version === 2) {
|
|
1520
|
+
const request = toMetricQuery(scoped, context.schema, context.execution);
|
|
1521
|
+
if (request.diagnostics.length)
|
|
1522
|
+
throw Error(request.diagnostics.map((d) => d.message).join(" "));
|
|
1523
|
+
if (matching.some((c) => c.scope === "shownRows") && !transport.metricCapabilities.shownRows)
|
|
1524
|
+
throw Error("This server does not support shown-row metrics.");
|
|
1525
|
+
if (request.profile && transport.metricCapabilities.profile && request.profile !== transport.metricCapabilities.profile)
|
|
1526
|
+
throw Error(
|
|
1527
|
+
"Metric transport and computed execution profiles do not match. Refresh the handshake."
|
|
1528
|
+
);
|
|
1529
|
+
const remoteFields = new Map(
|
|
1530
|
+
context.schema.fields.map((field) => [field.name, field])
|
|
1531
|
+
);
|
|
1532
|
+
for (const [name, capability] of Object.entries(
|
|
1533
|
+
context.execution?.fields ?? {}
|
|
1534
|
+
))
|
|
1535
|
+
if (isComputedField3(name))
|
|
1536
|
+
remoteFields.set(name, {
|
|
1537
|
+
name,
|
|
1538
|
+
label: name,
|
|
1539
|
+
type: capability.type,
|
|
1540
|
+
source: { kind: "backend" }
|
|
1541
|
+
});
|
|
1542
|
+
const remoteSchema = {
|
|
1543
|
+
...context.schema,
|
|
1544
|
+
fields: [...remoteFields.values()]
|
|
1545
|
+
};
|
|
1546
|
+
if (matching.some(
|
|
1547
|
+
(clause) => [
|
|
1548
|
+
...clause.groupBy,
|
|
1549
|
+
...metricPlans(clause, remoteSchema).flatMap(
|
|
1550
|
+
(plan) => plan.dependencies
|
|
1551
|
+
)
|
|
1552
|
+
].some(isComputedField3)
|
|
1553
|
+
) && !transport.metricCapabilities.computedFields)
|
|
1554
|
+
throw Error("This metric server does not support computed fields.");
|
|
1555
|
+
if (request.metrics.some(
|
|
1556
|
+
(c) => !c.distribution && (c.expression !== void 0 || c.expressionY !== void 0)
|
|
1557
|
+
) && !transport.metricCapabilities.expressions)
|
|
1558
|
+
throw Error(
|
|
1559
|
+
"This server does not support composed metric expressions."
|
|
1560
|
+
);
|
|
1561
|
+
if (matching.some(
|
|
1562
|
+
(c) => c.distribution && (!c.display || ["box", "histogram"].includes(c.display.kind))
|
|
1563
|
+
) && !transport.metricCapabilities.distributions)
|
|
1564
|
+
throw Error("This server does not support distributions.");
|
|
1565
|
+
results.push(...(await transport.fetchMetrics(request, signal)).metrics);
|
|
1566
|
+
} else if (transport?.fetchAggregations) {
|
|
1567
|
+
const request = toAggregationQuery(scoped, context.schema);
|
|
1568
|
+
if (request.diagnostics?.length)
|
|
1569
|
+
throw Error(request.diagnostics.map((d) => d.message).join(" "));
|
|
1570
|
+
results.push(
|
|
1571
|
+
...(await transport.fetchAggregations(request, signal)).metrics
|
|
1572
|
+
);
|
|
1573
|
+
} else
|
|
1574
|
+
throw Error(
|
|
1575
|
+
"Remote all-matching metrics require a metric-capable server transport."
|
|
1576
|
+
);
|
|
1577
|
+
}
|
|
1578
|
+
abort(signal);
|
|
1579
|
+
const byId = new Map(results.map((r) => [r.id, r]));
|
|
1580
|
+
return {
|
|
1581
|
+
metrics: clauses.map(
|
|
1582
|
+
(c) => byId.get(c.id) ?? {
|
|
1583
|
+
id: c.id,
|
|
1584
|
+
buckets: [],
|
|
1585
|
+
error: "Server omitted this metric."
|
|
1586
|
+
}
|
|
1587
|
+
)
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
function loadedPath(row, path) {
|
|
1591
|
+
if (!row || typeof row !== "object") return false;
|
|
1592
|
+
if (path in row) return true;
|
|
1593
|
+
let value = row;
|
|
1594
|
+
for (const part of path.split(".")) {
|
|
1595
|
+
if (value === null) return true;
|
|
1596
|
+
if (!value || typeof value !== "object" || !(part in value)) return false;
|
|
1597
|
+
value = value[part];
|
|
1598
|
+
}
|
|
1599
|
+
return true;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// src/useAggregations.ts
|
|
1603
|
+
function useAggregations(query, schema, transport, clientRows, debounceMs, nonce, validateQuery, options = {}, enabled = true) {
|
|
1604
|
+
const [results, setResults] = useState6(null);
|
|
1605
|
+
const [loading, setLoading] = useState6(false);
|
|
1606
|
+
const [error, setError] = useState6(null);
|
|
1607
|
+
const latest = useRef6({
|
|
1608
|
+
query,
|
|
1609
|
+
schema,
|
|
1610
|
+
transport,
|
|
1611
|
+
clientRows,
|
|
1612
|
+
options,
|
|
1613
|
+
validateQuery,
|
|
1614
|
+
enabled
|
|
1615
|
+
});
|
|
1616
|
+
latest.current = {
|
|
1617
|
+
query,
|
|
1618
|
+
schema,
|
|
1619
|
+
transport,
|
|
1620
|
+
clientRows,
|
|
1621
|
+
options,
|
|
1622
|
+
validateQuery,
|
|
1623
|
+
enabled
|
|
1624
|
+
};
|
|
1625
|
+
const preview = useCallback6(
|
|
1626
|
+
async (clauses, signal) => {
|
|
1627
|
+
const current = latest.current;
|
|
1628
|
+
if (!current.enabled) throw Error("Resolve the query link before previewing metrics.");
|
|
1629
|
+
const draft = { ...current.query, aggregations: clauses };
|
|
1630
|
+
current.validateQuery?.(draft);
|
|
1631
|
+
return executeMetrics(
|
|
1632
|
+
draft,
|
|
1633
|
+
{
|
|
1634
|
+
schema: current.schema,
|
|
1635
|
+
transport: current.transport,
|
|
1636
|
+
clientRows: current.clientRows,
|
|
1637
|
+
...current.options
|
|
1638
|
+
},
|
|
1639
|
+
signal
|
|
1640
|
+
);
|
|
1641
|
+
},
|
|
1642
|
+
[]
|
|
1643
|
+
);
|
|
1644
|
+
const shown = (query.aggregations ?? []).some((c) => c.scope === "shownRows");
|
|
1645
|
+
const localPage = !options.executeV3 && shown && (!transport?.fetchMetrics || transport.metricCapabilities?.version !== 2 || (query.aggregations ?? []).some((clause) => {
|
|
1646
|
+
if (clause.scope !== "shownRows") return false;
|
|
1647
|
+
try {
|
|
1648
|
+
return metricPlans2(clause, schema, options.resolveComputed).some(
|
|
1649
|
+
hasRegex3
|
|
1650
|
+
);
|
|
1651
|
+
} catch {
|
|
1652
|
+
return true;
|
|
1653
|
+
}
|
|
1654
|
+
}));
|
|
1655
|
+
const key = useMemo5(
|
|
1656
|
+
() => JSON.stringify([
|
|
1657
|
+
query.where,
|
|
1658
|
+
metricComputationKey(query.aggregations ?? []),
|
|
1659
|
+
shown ? [query.orderBy, query.limit, query.offset] : null
|
|
1660
|
+
]),
|
|
1661
|
+
[
|
|
1662
|
+
query.where,
|
|
1663
|
+
query.aggregations,
|
|
1664
|
+
query.orderBy,
|
|
1665
|
+
query.limit,
|
|
1666
|
+
query.offset,
|
|
1667
|
+
shown
|
|
1668
|
+
]
|
|
1669
|
+
);
|
|
1670
|
+
const dependencyKey = JSON.stringify([
|
|
1671
|
+
options.revisions ?? {},
|
|
1672
|
+
options.execution ?? null,
|
|
1673
|
+
options.now,
|
|
1674
|
+
options.strictLocalMetrics,
|
|
1675
|
+
options.queryContextKey
|
|
1676
|
+
]);
|
|
1677
|
+
useEffect6(() => {
|
|
1678
|
+
if (!enabled || !latest.current.query.aggregations?.length) {
|
|
458
1679
|
setResults(null);
|
|
459
|
-
setError(null);
|
|
460
1680
|
setLoading(false);
|
|
1681
|
+
setError(null);
|
|
461
1682
|
return;
|
|
462
1683
|
}
|
|
463
1684
|
const ac = new AbortController();
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
1685
|
+
setLoading(true);
|
|
1686
|
+
setResults(null);
|
|
1687
|
+
if (localPage && transport && !options.rowsReady) {
|
|
1688
|
+
try {
|
|
1689
|
+
latest.current.validateQuery?.(latest.current.query);
|
|
1690
|
+
setError(options.rowsError ?? null);
|
|
1691
|
+
if (options.rowsError) setLoading(false);
|
|
1692
|
+
} catch (e) {
|
|
1693
|
+
setError(e instanceof Error ? e : Error(String(e)));
|
|
1694
|
+
setLoading(false);
|
|
1695
|
+
}
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
const timer = setTimeout(() => {
|
|
467
1699
|
setError(null);
|
|
468
1700
|
try {
|
|
469
|
-
validateQuery?.(query);
|
|
470
|
-
let res;
|
|
471
|
-
if (transport?.fetchAggregations) {
|
|
472
|
-
res = await transport.fetchAggregations(toAggregationQuery(query, schema), ac.signal);
|
|
473
|
-
} else if (clientRows) {
|
|
474
|
-
res = applyAggregations(clientRows, query, schema);
|
|
475
|
-
} else {
|
|
476
|
-
res = EMPTY_RESULT;
|
|
477
|
-
}
|
|
478
|
-
if (!cancelled) setResults(res);
|
|
1701
|
+
latest.current.validateQuery?.(latest.current.query);
|
|
479
1702
|
} catch (e) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
} finally {
|
|
485
|
-
if (!cancelled) setLoading(false);
|
|
1703
|
+
setError(e instanceof Error ? e : Error(String(e)));
|
|
1704
|
+
setLoading(false);
|
|
1705
|
+
return;
|
|
486
1706
|
}
|
|
487
|
-
|
|
488
|
-
|
|
1707
|
+
void preview(latest.current.query.aggregations ?? [], ac.signal).then((result) => {
|
|
1708
|
+
if (!ac.signal.aborted) setResults(result);
|
|
1709
|
+
}).catch((e) => {
|
|
1710
|
+
if (!ac.signal.aborted)
|
|
1711
|
+
setError(e instanceof Error ? e : Error(String(e)));
|
|
1712
|
+
}).finally(() => {
|
|
1713
|
+
if (!ac.signal.aborted) setLoading(false);
|
|
1714
|
+
});
|
|
1715
|
+
}, debounceMs);
|
|
489
1716
|
return () => {
|
|
490
|
-
cancelled = true;
|
|
491
|
-
clearTimeout(t);
|
|
492
1717
|
ac.abort();
|
|
1718
|
+
clearTimeout(timer);
|
|
493
1719
|
};
|
|
494
|
-
}, [
|
|
495
|
-
|
|
1720
|
+
}, [
|
|
1721
|
+
key,
|
|
1722
|
+
schema,
|
|
1723
|
+
transport,
|
|
1724
|
+
clientRows,
|
|
1725
|
+
debounceMs,
|
|
1726
|
+
nonce,
|
|
1727
|
+
validateQuery,
|
|
1728
|
+
enabled,
|
|
1729
|
+
dependencyKey,
|
|
1730
|
+
localPage,
|
|
1731
|
+
localPage ? options.shownRows : void 0,
|
|
1732
|
+
localPage ? options.rowsReady : void 0,
|
|
1733
|
+
localPage ? options.rowsError : void 0,
|
|
1734
|
+
preview
|
|
1735
|
+
]);
|
|
1736
|
+
return { results, loading, error, preview };
|
|
496
1737
|
}
|
|
497
1738
|
|
|
498
1739
|
// src/useSelection.ts
|
|
499
|
-
import { useCallback as
|
|
1740
|
+
import { useCallback as useCallback7, useEffect as useEffect7, useMemo as useMemo6, useState as useState7 } from "react";
|
|
1741
|
+
function sameOrder(left, right) {
|
|
1742
|
+
return left.length === right.length && left.every((id, index) => id === right[index]);
|
|
1743
|
+
}
|
|
500
1744
|
function useSelection(displayedIds) {
|
|
501
|
-
const [selected,
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
1745
|
+
const [{ selected }, setState] = useState7(() => ({
|
|
1746
|
+
selected: /* @__PURE__ */ new Set(),
|
|
1747
|
+
range: null
|
|
1748
|
+
}));
|
|
1749
|
+
useEffect7(() => {
|
|
1750
|
+
setState((prev) => prev.range && !sameOrder(prev.range.displayedIds, displayedIds) ? { selected: prev.selected, range: null } : prev);
|
|
1751
|
+
}, [displayedIds]);
|
|
1752
|
+
const toggle = useCallback7(
|
|
1753
|
+
(id, shiftKey = false, additive = false) => {
|
|
1754
|
+
setState((prev) => {
|
|
1755
|
+
const range = prev.range;
|
|
1756
|
+
if (shiftKey && range && sameOrder(range.displayedIds, displayedIds)) {
|
|
1757
|
+
const i = displayedIds.indexOf(range.anchor);
|
|
510
1758
|
const j = displayedIds.indexOf(id);
|
|
511
1759
|
if (i !== -1 && j !== -1) {
|
|
512
1760
|
const [lo, hi] = i <= j ? [i, j] : [j, i];
|
|
513
|
-
const
|
|
514
|
-
const
|
|
515
|
-
for (const rowId of
|
|
516
|
-
if (
|
|
517
|
-
else
|
|
1761
|
+
const activeRange = additive ? { ...range, baseline: prev.selected, selecting: true } : range;
|
|
1762
|
+
const next2 = new Set(activeRange.baseline);
|
|
1763
|
+
for (const rowId of displayedIds.slice(lo, hi + 1)) {
|
|
1764
|
+
if (activeRange.selecting) next2.add(rowId);
|
|
1765
|
+
else next2.delete(rowId);
|
|
518
1766
|
}
|
|
519
|
-
|
|
520
|
-
return next;
|
|
1767
|
+
return { selected: next2, range: activeRange };
|
|
521
1768
|
}
|
|
522
1769
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1770
|
+
const next = new Set(prev.selected);
|
|
1771
|
+
const selecting = !next.has(id);
|
|
1772
|
+
if (selecting) next.add(id);
|
|
1773
|
+
else next.delete(id);
|
|
1774
|
+
return {
|
|
1775
|
+
selected: next,
|
|
1776
|
+
range: { anchor: id, baseline: prev.selected, selecting, displayedIds: [...displayedIds] }
|
|
1777
|
+
};
|
|
527
1778
|
});
|
|
528
1779
|
},
|
|
529
1780
|
[displayedIds]
|
|
530
1781
|
);
|
|
531
|
-
const setPage =
|
|
532
|
-
|
|
533
|
-
const next = new Set(prev);
|
|
1782
|
+
const setPage = useCallback7((ids, select) => {
|
|
1783
|
+
setState((prev) => {
|
|
1784
|
+
const next = new Set(prev.selected);
|
|
534
1785
|
for (const id of ids) {
|
|
535
1786
|
if (select) next.add(id);
|
|
536
1787
|
else next.delete(id);
|
|
537
1788
|
}
|
|
538
|
-
return next;
|
|
1789
|
+
return { selected: next, range: null };
|
|
539
1790
|
});
|
|
540
1791
|
}, []);
|
|
541
|
-
const replace =
|
|
1792
|
+
const replace = useCallback7((ids) => {
|
|
542
1793
|
const next = new Set(ids);
|
|
543
|
-
|
|
544
|
-
setSelected(next);
|
|
1794
|
+
setState({ selected: next, range: null });
|
|
545
1795
|
}, []);
|
|
546
|
-
const retain =
|
|
1796
|
+
const retain = useCallback7((ids) => {
|
|
547
1797
|
const allowed = new Set(ids);
|
|
548
|
-
|
|
549
|
-
setSelected((prev) => {
|
|
1798
|
+
setState((prev) => {
|
|
550
1799
|
const next = /* @__PURE__ */ new Set();
|
|
551
|
-
for (const id of prev) if (allowed.has(id)) next.add(id);
|
|
552
|
-
return next;
|
|
1800
|
+
for (const id of prev.selected) if (allowed.has(id)) next.add(id);
|
|
1801
|
+
return { selected: next, range: null };
|
|
553
1802
|
});
|
|
554
1803
|
}, []);
|
|
555
|
-
const pageState =
|
|
1804
|
+
const pageState = useCallback7(
|
|
556
1805
|
(pageIds) => {
|
|
557
1806
|
if (pageIds.length === 0) return "none";
|
|
558
1807
|
let n = 0;
|
|
@@ -561,11 +1810,10 @@ function useSelection(displayedIds) {
|
|
|
561
1810
|
},
|
|
562
1811
|
[selected]
|
|
563
1812
|
);
|
|
564
|
-
const clear =
|
|
565
|
-
|
|
566
|
-
setSelected(/* @__PURE__ */ new Set());
|
|
1813
|
+
const clear = useCallback7(() => {
|
|
1814
|
+
setState({ selected: /* @__PURE__ */ new Set(), range: null });
|
|
567
1815
|
}, []);
|
|
568
|
-
return
|
|
1816
|
+
return useMemo6(
|
|
569
1817
|
() => ({
|
|
570
1818
|
selected,
|
|
571
1819
|
isSelected: (id) => selected.has(id),
|
|
@@ -582,27 +1830,29 @@ function useSelection(displayedIds) {
|
|
|
582
1830
|
}
|
|
583
1831
|
|
|
584
1832
|
// src/useSelect.ts
|
|
585
|
-
import { useCallback as
|
|
1833
|
+
import { useCallback as useCallback8, useMemo as useMemo7 } from "react";
|
|
586
1834
|
import { selectedFields } from "@pythia-software/query-table-core";
|
|
587
1835
|
function useSelect(query, setQuery, schema) {
|
|
588
|
-
const fields =
|
|
589
|
-
const visible =
|
|
590
|
-
const hidden =
|
|
1836
|
+
const fields = useMemo7(() => selectedFields(schema, query), [schema, query]);
|
|
1837
|
+
const visible = useMemo7(() => fields.map((f) => columnFor(query.select, f.name)), [fields, query.select]);
|
|
1838
|
+
const hidden = useMemo7(() => {
|
|
591
1839
|
const shown = new Set(fields.map((f) => f.name));
|
|
592
1840
|
return schema.fields.filter((f) => !shown.has(f.name) && (f.select?.enabled ?? true));
|
|
593
1841
|
}, [schema, fields]);
|
|
594
|
-
const writeSelect =
|
|
595
|
-
(mutate) => {
|
|
1842
|
+
const writeSelect = useCallback8(
|
|
1843
|
+
(mutate, clearFitToScreen = false) => {
|
|
596
1844
|
setQuery((prev) => {
|
|
597
1845
|
const current = prev.select.length ? prev.select : visible;
|
|
598
|
-
|
|
1846
|
+
const next = { ...prev, select: mutate([...current]) };
|
|
1847
|
+
if (clearFitToScreen) delete next.fitToScreen;
|
|
1848
|
+
return next;
|
|
599
1849
|
});
|
|
600
1850
|
},
|
|
601
1851
|
[setQuery, visible]
|
|
602
1852
|
);
|
|
603
|
-
const show =
|
|
604
|
-
const hide =
|
|
605
|
-
const move =
|
|
1853
|
+
const show = useCallback8((field) => writeSelect((c) => c.some((x) => x.field === field) ? c : [...c, { field }]), [writeSelect]);
|
|
1854
|
+
const hide = useCallback8((field) => writeSelect((c) => c.filter((x) => x.field !== field)), [writeSelect]);
|
|
1855
|
+
const move = useCallback8(
|
|
606
1856
|
(field, toIndex) => writeSelect((c) => {
|
|
607
1857
|
const from = c.findIndex((x) => x.field === field);
|
|
608
1858
|
if (from === -1) return c;
|
|
@@ -612,11 +1862,11 @@ function useSelect(query, setQuery, schema) {
|
|
|
612
1862
|
}),
|
|
613
1863
|
[writeSelect]
|
|
614
1864
|
);
|
|
615
|
-
const setWidth =
|
|
616
|
-
(field, width) => writeSelect((c) => c.map((x) => x.field === field ? { ...x, width: Math.round(width) } : x)),
|
|
1865
|
+
const setWidth = useCallback8(
|
|
1866
|
+
(field, width) => writeSelect((c) => c.map((x) => x.field === field ? { ...x, width: Math.round(width) } : x), true),
|
|
617
1867
|
[writeSelect]
|
|
618
1868
|
);
|
|
619
|
-
const reset =
|
|
1869
|
+
const reset = useCallback8(() => setQuery((prev) => ({ ...prev, select: [] })), [setQuery]);
|
|
620
1870
|
return { visible, fields, hidden, show, hide, move, setWidth, reset };
|
|
621
1871
|
}
|
|
622
1872
|
function columnFor(select, name) {
|
|
@@ -625,92 +1875,139 @@ function columnFor(select, name) {
|
|
|
625
1875
|
}
|
|
626
1876
|
|
|
627
1877
|
// src/useColumnDrag.ts
|
|
628
|
-
import { useCallback as
|
|
1878
|
+
import { useCallback as useCallback9, useMemo as useMemo8, useState as useState8 } from "react";
|
|
629
1879
|
function useColumnDrag() {
|
|
630
|
-
const [state, setState] =
|
|
631
|
-
const
|
|
632
|
-
const
|
|
633
|
-
(index) => (
|
|
1880
|
+
const [state, setState] = useState8(null);
|
|
1881
|
+
const [dropRevision, setDropRevision] = useState8(0);
|
|
1882
|
+
const start = useCallback9(
|
|
1883
|
+
(field, index, order) => setState({
|
|
1884
|
+
source: field,
|
|
1885
|
+
overIndex: index,
|
|
1886
|
+
order: order?.slice(),
|
|
1887
|
+
moved: false
|
|
1888
|
+
}),
|
|
1889
|
+
[]
|
|
1890
|
+
);
|
|
1891
|
+
const over = useCallback9(
|
|
1892
|
+
(index, order) => (
|
|
634
1893
|
// Keep the SAME object reference when unchanged so React can bail out of the
|
|
635
1894
|
// re-render — onDragOver fires continuously while the cursor moves.
|
|
636
|
-
setState((s) =>
|
|
1895
|
+
setState((s) => {
|
|
1896
|
+
if (!s) return s;
|
|
1897
|
+
if (s.moved && s.overIndex === index && (!order && !s.order || order && s.order && order.length === s.order.length && order.every((name, i) => name === s.order[i])))
|
|
1898
|
+
return s;
|
|
1899
|
+
return {
|
|
1900
|
+
source: s.source,
|
|
1901
|
+
overIndex: index,
|
|
1902
|
+
order: order?.slice(),
|
|
1903
|
+
moved: true
|
|
1904
|
+
};
|
|
1905
|
+
})
|
|
637
1906
|
),
|
|
638
1907
|
[]
|
|
639
1908
|
);
|
|
640
|
-
const end =
|
|
641
|
-
|
|
1909
|
+
const end = useCallback9((committed = false) => {
|
|
1910
|
+
if (committed) setDropRevision((revision) => revision + 1);
|
|
1911
|
+
setState(null);
|
|
1912
|
+
}, []);
|
|
1913
|
+
const preview = useCallback9(
|
|
642
1914
|
(order) => {
|
|
643
|
-
if (!state) return order;
|
|
1915
|
+
if (!state || state.order && !state.moved) return order;
|
|
644
1916
|
const without = order.filter((n) => n !== state.source);
|
|
645
1917
|
if (without.length === order.length) return order;
|
|
646
|
-
|
|
1918
|
+
let at = Math.max(0, Math.min(state.overIndex, without.length));
|
|
1919
|
+
if (state.order) {
|
|
1920
|
+
const reference = state.order.filter((name) => name !== state.source);
|
|
1921
|
+
const slot = Math.max(0, Math.min(state.overIndex, reference.length));
|
|
1922
|
+
const following = reference.slice(slot).find((name) => without.includes(name));
|
|
1923
|
+
const preceding = reference.slice(0, slot).reverse().find((name) => without.includes(name));
|
|
1924
|
+
if (following != null) at = without.indexOf(following);
|
|
1925
|
+
else if (preceding != null) at = without.indexOf(preceding) + 1;
|
|
1926
|
+
}
|
|
647
1927
|
return [...without.slice(0, at), state.source, ...without.slice(at)];
|
|
648
1928
|
},
|
|
649
1929
|
[state]
|
|
650
1930
|
);
|
|
651
|
-
return
|
|
1931
|
+
return useMemo8(
|
|
652
1932
|
() => ({
|
|
653
1933
|
source: state?.source ?? null,
|
|
654
1934
|
overIndex: state?.overIndex ?? null,
|
|
655
1935
|
active: state != null,
|
|
1936
|
+
dropRevision,
|
|
656
1937
|
start,
|
|
657
1938
|
over,
|
|
658
1939
|
end,
|
|
659
1940
|
preview
|
|
660
1941
|
}),
|
|
661
|
-
[state, start, over, end, preview]
|
|
1942
|
+
[state, dropRevision, start, over, end, preview]
|
|
662
1943
|
);
|
|
663
1944
|
}
|
|
664
1945
|
|
|
665
1946
|
// src/useSavedQueries.ts
|
|
666
|
-
import { useCallback as
|
|
667
|
-
function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
|
|
668
|
-
const [items, setItems] =
|
|
669
|
-
const [defaultId, setDefaultId] =
|
|
670
|
-
const [loading, setLoading] =
|
|
671
|
-
const
|
|
1947
|
+
import { useCallback as useCallback10, useEffect as useEffect8, useRef as useRef7, useState as useState9 } from "react";
|
|
1948
|
+
function useSavedQueries(key, currentQuery, applyQueryState, storage, now, snapshot) {
|
|
1949
|
+
const [items, setItems] = useState9([]);
|
|
1950
|
+
const [defaultId, setDefaultId] = useState9(null);
|
|
1951
|
+
const [loading, setLoading] = useState9(true);
|
|
1952
|
+
const storageRef = useRef7(storage);
|
|
1953
|
+
storageRef.current = storage;
|
|
1954
|
+
const refresh = useCallback10(async () => {
|
|
1955
|
+
const storage2 = storageRef.current;
|
|
672
1956
|
setLoading(true);
|
|
673
1957
|
try {
|
|
674
|
-
const [savedItems, defaultSaved] = await Promise.
|
|
675
|
-
|
|
676
|
-
|
|
1958
|
+
const [savedItems, defaultSaved] = await Promise.allSettled([
|
|
1959
|
+
storage2.listSaved(key),
|
|
1960
|
+
storage2.loadDefaultSaved?.(key) ?? Promise.resolve(null)
|
|
677
1961
|
]);
|
|
678
|
-
setItems(savedItems);
|
|
679
|
-
setDefaultId(defaultSaved?.id ?? null);
|
|
1962
|
+
if (savedItems.status === "fulfilled") setItems(savedItems.value);
|
|
1963
|
+
if (defaultSaved.status === "fulfilled") setDefaultId(defaultSaved.value?.id ?? null);
|
|
680
1964
|
} finally {
|
|
681
1965
|
setLoading(false);
|
|
682
1966
|
}
|
|
683
|
-
}, [
|
|
684
|
-
|
|
1967
|
+
}, [key]);
|
|
1968
|
+
useEffect8(() => {
|
|
685
1969
|
void refresh();
|
|
686
1970
|
}, [refresh]);
|
|
687
|
-
const save =
|
|
1971
|
+
const save = useCallback10(
|
|
688
1972
|
async (name) => {
|
|
689
|
-
|
|
1973
|
+
name = name.trim();
|
|
1974
|
+
if (!name || name.length > 200) throw Error("Saved query name must be between 1 and 200 characters.");
|
|
1975
|
+
const immutable = await snapshot?.(currentQuery);
|
|
1976
|
+
const saved = await storage.saveNamed(key, name, currentQuery, now(), immutable?.code);
|
|
690
1977
|
await refresh();
|
|
691
1978
|
return saved;
|
|
692
1979
|
},
|
|
693
|
-
[storage, key, currentQuery, now, refresh]
|
|
1980
|
+
[storage, key, currentQuery, now, refresh, snapshot]
|
|
694
1981
|
);
|
|
695
|
-
const load =
|
|
1982
|
+
const load = useCallback10(
|
|
696
1983
|
(id) => {
|
|
697
1984
|
const found = items.find((q) => q.id === id);
|
|
698
1985
|
if (found) applyQueryState(found.query);
|
|
699
1986
|
},
|
|
700
1987
|
[items, applyQueryState]
|
|
701
1988
|
);
|
|
702
|
-
const
|
|
1989
|
+
const update = useCallback10(
|
|
1990
|
+
async (id) => {
|
|
1991
|
+
if (!storage.updateNamed) throw Error("This storage adapter does not support saved-query updates.");
|
|
1992
|
+
const immutable = await snapshot?.(currentQuery);
|
|
1993
|
+
const saved = await storage.updateNamed(key, id, currentQuery, now(), immutable?.code);
|
|
1994
|
+
await refresh();
|
|
1995
|
+
return saved;
|
|
1996
|
+
},
|
|
1997
|
+
[storage, key, currentQuery, now, refresh, snapshot]
|
|
1998
|
+
);
|
|
1999
|
+
const setDefault = useCallback10(
|
|
703
2000
|
async (id) => {
|
|
704
2001
|
await storage.setDefaultSaved?.(key, id);
|
|
705
2002
|
await refresh();
|
|
706
2003
|
},
|
|
707
2004
|
[storage, key, refresh]
|
|
708
2005
|
);
|
|
709
|
-
const clearDefault =
|
|
2006
|
+
const clearDefault = useCallback10(async () => {
|
|
710
2007
|
await storage.setDefaultSaved?.(key, null);
|
|
711
2008
|
await refresh();
|
|
712
2009
|
}, [storage, key, refresh]);
|
|
713
|
-
const remove =
|
|
2010
|
+
const remove = useCallback10(
|
|
714
2011
|
async (id) => {
|
|
715
2012
|
if (id === defaultId) await storage.setDefaultSaved?.(key, null);
|
|
716
2013
|
await storage.deleteSaved(key, id);
|
|
@@ -718,14 +2015,118 @@ function useSavedQueries(key, currentQuery, applyQueryState, storage, now) {
|
|
|
718
2015
|
},
|
|
719
2016
|
[storage, key, defaultId, refresh]
|
|
720
2017
|
);
|
|
721
|
-
return { items, loading, defaultId, save, load, setDefault, clearDefault, remove };
|
|
2018
|
+
return { items, loading, defaultId, save, canUpdate: Boolean(storage.updateNamed), update, load, setDefault, clearDefault, remove };
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// src/useQueryLinks.ts
|
|
2022
|
+
import { useCallback as useCallback11, useEffect as useEffect9, useRef as useRef8, useState as useState10 } from "react";
|
|
2023
|
+
import { encodeQuery } from "@pythia-software/query-table-core";
|
|
2024
|
+
function useQueryLinks(query, service, syncUrl, ready, parameter, maxUrlBytes, onUrlChange) {
|
|
2025
|
+
const latest = useRef8(query);
|
|
2026
|
+
latest.current = query;
|
|
2027
|
+
const [kind, setKind] = useState10(null);
|
|
2028
|
+
const [busy, setBusy] = useState10(false);
|
|
2029
|
+
const [error, setError] = useState10(null);
|
|
2030
|
+
const mounted = useRef8(true);
|
|
2031
|
+
const operations = useRef8(0);
|
|
2032
|
+
useEffect9(() => {
|
|
2033
|
+
mounted.current = true;
|
|
2034
|
+
return () => {
|
|
2035
|
+
mounted.current = false;
|
|
2036
|
+
};
|
|
2037
|
+
}, []);
|
|
2038
|
+
const queryKey = JSON.stringify(query);
|
|
2039
|
+
useEffect9(() => {
|
|
2040
|
+
if (!syncUrl || !ready || typeof window === "undefined") return;
|
|
2041
|
+
try {
|
|
2042
|
+
const result = service.local(query, window.location.href, parameter);
|
|
2043
|
+
window.history.replaceState(window.history.state, "", result.url);
|
|
2044
|
+
onUrlChange(result.url);
|
|
2045
|
+
setKind(result.kind);
|
|
2046
|
+
setError(null);
|
|
2047
|
+
} catch (e) {
|
|
2048
|
+
setError(e instanceof Error ? e : Error(String(e)));
|
|
2049
|
+
}
|
|
2050
|
+
}, [queryKey, syncUrl, ready, service, parameter, onUrlChange]);
|
|
2051
|
+
const snapshot = useCallback11(async (q) => {
|
|
2052
|
+
const captured = encodeQuery(q);
|
|
2053
|
+
const base = typeof window !== "undefined" ? window.location.href : void 0;
|
|
2054
|
+
operations.current++;
|
|
2055
|
+
setBusy(true);
|
|
2056
|
+
setError(null);
|
|
2057
|
+
try {
|
|
2058
|
+
if (!ready) throw Error("Resolve or reset the query link before saving it.");
|
|
2059
|
+
const saved = await service.snapshot(q);
|
|
2060
|
+
if (saved && base && mounted.current && syncUrl && ready && window.location.href === base && encodeQuery(latest.current) === captured) {
|
|
2061
|
+
const url = new URL(base);
|
|
2062
|
+
url.searchParams.set(parameter, "@" + saved.code);
|
|
2063
|
+
const result = service.local(q, url.toString(), parameter);
|
|
2064
|
+
window.history.replaceState(window.history.state, "", result.url);
|
|
2065
|
+
onUrlChange(result.url);
|
|
2066
|
+
setKind("durable");
|
|
2067
|
+
}
|
|
2068
|
+
return saved;
|
|
2069
|
+
} catch (e) {
|
|
2070
|
+
if (mounted.current) setError(e instanceof Error ? e : Error(String(e)));
|
|
2071
|
+
throw e;
|
|
2072
|
+
} finally {
|
|
2073
|
+
operations.current--;
|
|
2074
|
+
if (mounted.current) setBusy(operations.current > 0);
|
|
2075
|
+
}
|
|
2076
|
+
}, [service, syncUrl, ready, parameter, onUrlChange]);
|
|
2077
|
+
const create = useCallback11(async (q = latest.current, options = {}) => {
|
|
2078
|
+
const baseUrl = options.baseUrl ?? window.location.href;
|
|
2079
|
+
const param = options.parameter ?? parameter;
|
|
2080
|
+
const captured = encodeQuery(q);
|
|
2081
|
+
operations.current++;
|
|
2082
|
+
setBusy(true);
|
|
2083
|
+
setError(null);
|
|
2084
|
+
try {
|
|
2085
|
+
if (!ready) throw Error("Resolve or reset the query link before sharing it.");
|
|
2086
|
+
const result = await service.durable(q, baseUrl, param);
|
|
2087
|
+
if (mounted.current && syncUrl && ready && param === parameter && window.location.href === baseUrl && encodeQuery(latest.current) === captured) {
|
|
2088
|
+
window.history.replaceState(window.history.state, "", result.url);
|
|
2089
|
+
onUrlChange(result.url);
|
|
2090
|
+
setKind(result.kind);
|
|
2091
|
+
}
|
|
2092
|
+
return result;
|
|
2093
|
+
} catch (e) {
|
|
2094
|
+
if (mounted.current) setError(e instanceof Error ? e : Error(String(e)));
|
|
2095
|
+
throw e;
|
|
2096
|
+
} finally {
|
|
2097
|
+
operations.current--;
|
|
2098
|
+
if (mounted.current) setBusy(operations.current > 0);
|
|
2099
|
+
}
|
|
2100
|
+
}, [service, syncUrl, ready, parameter, onUrlChange]);
|
|
2101
|
+
const local = useCallback11((q, base = window.location.href, param = parameter) => service.local(q, base, param), [service, parameter]);
|
|
2102
|
+
return { maxUrlBytes, kind, busy, error, create, local, snapshot };
|
|
722
2103
|
}
|
|
723
2104
|
|
|
724
2105
|
// src/useQueryTable.ts
|
|
2106
|
+
import { createQueryLinkService } from "@pythia-software/query-table-core";
|
|
725
2107
|
var AUTOCOMPLETE_LIMIT = 50;
|
|
726
2108
|
var MAX_AUTO_REFRESH_POLLS = 1e3;
|
|
727
|
-
function
|
|
728
|
-
return
|
|
2109
|
+
function detailColumnSchema(schema, enabled) {
|
|
2110
|
+
if (!enabled) return schema;
|
|
2111
|
+
return {
|
|
2112
|
+
...schema,
|
|
2113
|
+
defaultSelect: [...schema.defaultSelect ?? selectedFields2(schema, { select: [] }).map((field) => ({ field: field.name })), { field: ITEM_DETAILS_COLUMN }],
|
|
2114
|
+
fields: [...schema.fields, {
|
|
2115
|
+
name: ITEM_DETAILS_COLUMN,
|
|
2116
|
+
label: "Detail",
|
|
2117
|
+
type: "text",
|
|
2118
|
+
source: { kind: "derived", accessor: () => null },
|
|
2119
|
+
select: { default: true, width: 112 },
|
|
2120
|
+
filter: { enabled: false },
|
|
2121
|
+
sort: { enabled: false },
|
|
2122
|
+
aggregate: { measure: false, groupable: false },
|
|
2123
|
+
property: { enabled: false }
|
|
2124
|
+
}]
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
function defaultsFor(schema, details = false) {
|
|
2128
|
+
schema = detailColumnSchema(schema, details);
|
|
2129
|
+
return normalizeQueryState3({
|
|
729
2130
|
select: schema.defaultSelect ?? [],
|
|
730
2131
|
where: [],
|
|
731
2132
|
orderBy: schema.defaultSort ?? [],
|
|
@@ -734,31 +2135,35 @@ function defaultsFor(schema) {
|
|
|
734
2135
|
});
|
|
735
2136
|
}
|
|
736
2137
|
function cloneWhereTerm(term) {
|
|
737
|
-
const copy =
|
|
738
|
-
if (term.setFilter)
|
|
2138
|
+
const copy = isOrGroup2(term) ? { ...term, any: term.any.map((c) => ({ ...c })) } : { ...term };
|
|
2139
|
+
if (term.setFilter)
|
|
2140
|
+
copy.setFilter = { ...term.setFilter, values: [...term.setFilter.values] };
|
|
739
2141
|
return copy;
|
|
740
2142
|
}
|
|
741
2143
|
function cloneQueryState(q) {
|
|
742
2144
|
const clone = {
|
|
743
2145
|
select: q.select.map((column) => ({ ...column })),
|
|
744
2146
|
where: q.where.map(cloneWhereTerm),
|
|
745
|
-
orderBy: q.orderBy.map(
|
|
2147
|
+
orderBy: q.orderBy.map(
|
|
2148
|
+
(term) => term.extract ? { ...term, extract: { ...term.extract } } : { ...term }
|
|
2149
|
+
),
|
|
746
2150
|
limit: q.limit,
|
|
747
2151
|
offset: q.offset
|
|
748
2152
|
};
|
|
749
|
-
if (q.
|
|
2153
|
+
if (q.fitToScreen) clone.fitToScreen = true;
|
|
2154
|
+
if (q.aggregations)
|
|
2155
|
+
clone.aggregations = JSON.parse(
|
|
2156
|
+
JSON.stringify(q.aggregations)
|
|
2157
|
+
);
|
|
2158
|
+
if (q.grouping) clone.grouping = normalizeQueryState3(q).grouping;
|
|
2159
|
+
if (q.itemDetails) clone.itemDetails = JSON.parse(JSON.stringify(q.itemDetails));
|
|
750
2160
|
return clone;
|
|
751
2161
|
}
|
|
752
2162
|
function mergeAggregation(a, patch) {
|
|
753
|
-
const next = {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
};
|
|
758
|
-
const field = "field" in patch ? patch.field : a.field;
|
|
759
|
-
if (field) next.field = field;
|
|
760
|
-
const label = "label" in patch ? patch.label : a.label;
|
|
761
|
-
if (label) next.label = label;
|
|
2163
|
+
const next = { ...a, ...patch };
|
|
2164
|
+
for (const key of Object.keys(next)) {
|
|
2165
|
+
if (next[key] === void 0) delete next[key];
|
|
2166
|
+
}
|
|
762
2167
|
return next;
|
|
763
2168
|
}
|
|
764
2169
|
function isNullLike(value) {
|
|
@@ -773,7 +2178,7 @@ function asDistinctValue(value) {
|
|
|
773
2178
|
}
|
|
774
2179
|
function fieldHasNullInRows(field, rows) {
|
|
775
2180
|
for (const row of rows) {
|
|
776
|
-
if (isNullLike(
|
|
2181
|
+
if (isNullLike(readFieldValue4(field, row))) return true;
|
|
777
2182
|
}
|
|
778
2183
|
return false;
|
|
779
2184
|
}
|
|
@@ -782,116 +2187,485 @@ function distinctValuesFromRows(field, rows, search) {
|
|
|
782
2187
|
const values = [];
|
|
783
2188
|
const seen = /* @__PURE__ */ new Set();
|
|
784
2189
|
let hasMore = false;
|
|
785
|
-
for (const row of rows) {
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
seen.
|
|
791
|
-
|
|
792
|
-
|
|
2190
|
+
rowsLoop: for (const row of rows) {
|
|
2191
|
+
const value = readFieldValue4(field, row);
|
|
2192
|
+
const candidates = field.type === "textarray" && Array.isArray(value) ? value : [value];
|
|
2193
|
+
for (const candidate of candidates) {
|
|
2194
|
+
const raw = asDistinctValue(candidate);
|
|
2195
|
+
if (raw == null || seen.has(raw)) continue;
|
|
2196
|
+
if (target && !raw.toLowerCase().includes(target)) continue;
|
|
2197
|
+
if (values.length < AUTOCOMPLETE_LIMIT) {
|
|
2198
|
+
seen.add(raw);
|
|
2199
|
+
values.push(raw);
|
|
2200
|
+
continue;
|
|
2201
|
+
}
|
|
2202
|
+
hasMore = true;
|
|
2203
|
+
break rowsLoop;
|
|
793
2204
|
}
|
|
794
|
-
hasMore = true;
|
|
795
|
-
break;
|
|
796
2205
|
}
|
|
797
2206
|
return { values: values.sort((a, b) => a.localeCompare(b)), hasMore };
|
|
798
2207
|
}
|
|
2208
|
+
function hasQueryLink(url, parameter) {
|
|
2209
|
+
const values = new URL(url).searchParams.getAll(parameter);
|
|
2210
|
+
return values.length > 1 || values.length === 1 && values[0] !== "";
|
|
2211
|
+
}
|
|
2212
|
+
function queryParameter(opts) {
|
|
2213
|
+
return opts.queryParameter ?? (typeof opts.itemDetails === "object" ? opts.itemDetails.queryParameter : void 0) ?? "q";
|
|
2214
|
+
}
|
|
799
2215
|
function resolveInitial(opts) {
|
|
800
|
-
const defaults = defaultsFor(opts.schema);
|
|
801
|
-
if (opts.initialQuery)
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
2216
|
+
const defaults = defaultsFor(opts.schema, Boolean(opts.itemDetails));
|
|
2217
|
+
if (opts.initialQuery)
|
|
2218
|
+
return {
|
|
2219
|
+
q: normalizeQueryState3(opts.initialQuery, defaults),
|
|
2220
|
+
fromUrl: false
|
|
2221
|
+
};
|
|
2222
|
+
if (opts.syncUrl === true && opts.active !== false && typeof window !== "undefined") {
|
|
2223
|
+
const parameter = queryParameter(opts);
|
|
2224
|
+
if (hasQueryLink(window.location.href, parameter))
|
|
2225
|
+
return {
|
|
2226
|
+
q: defaults,
|
|
2227
|
+
fromUrl: true
|
|
2228
|
+
};
|
|
805
2229
|
}
|
|
806
2230
|
return { q: defaults, fromUrl: false };
|
|
807
2231
|
}
|
|
808
2232
|
function useQueryTable(opts) {
|
|
809
|
-
const {
|
|
810
|
-
|
|
2233
|
+
const {
|
|
2234
|
+
schema: sourceSchema,
|
|
2235
|
+
transport: sourceTransport,
|
|
2236
|
+
clientRows: providedRows,
|
|
2237
|
+
initialQuery,
|
|
2238
|
+
debounceMs = 200
|
|
2239
|
+
} = opts;
|
|
2240
|
+
const active = opts.active !== false;
|
|
2241
|
+
const urlSyncEnabled = opts.syncUrl === true;
|
|
2242
|
+
const syncUrl = urlSyncEnabled && active;
|
|
2243
|
+
const { transport: metadataTransport, activity: requestActivity } = useRequestActivity(sourceTransport);
|
|
2244
|
+
const transport = metadataTransport;
|
|
2245
|
+
const storage = useMemo9(
|
|
2246
|
+
() => opts.storage ?? memoryStorageAdapter(),
|
|
2247
|
+
[opts.storage]
|
|
2248
|
+
);
|
|
2249
|
+
const parameter = queryParameter(opts);
|
|
2250
|
+
const linkService = useMemo9(
|
|
2251
|
+
() => createQueryLinkService(sourceSchema.name, storage.snapshots, opts.links),
|
|
2252
|
+
[sourceSchema.name, storage.snapshots, opts.links?.cache, opts.links?.maxUrlBytes]
|
|
2253
|
+
);
|
|
811
2254
|
const now = opts.now ?? Date.now;
|
|
812
|
-
const nowRef =
|
|
813
|
-
const onRefreshRef =
|
|
814
|
-
const defaults =
|
|
815
|
-
const
|
|
816
|
-
const
|
|
817
|
-
const
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
const
|
|
821
|
-
const
|
|
822
|
-
const
|
|
823
|
-
const
|
|
824
|
-
const
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
2255
|
+
const nowRef = useRef9(now);
|
|
2256
|
+
const onRefreshRef = useRef9(opts.onRefresh);
|
|
2257
|
+
const defaults = useMemo9(() => defaultsFor(sourceSchema, Boolean(opts.itemDetails)), [sourceSchema, Boolean(opts.itemDetails)]);
|
|
2258
|
+
const initRef = useRef9(resolveInitial(opts));
|
|
2259
|
+
const [restoration, setRestoration] = useState11({ loading: initRef.current.fromUrl, error: null });
|
|
2260
|
+
const [storageReady, setStorageReady] = useState11(Boolean(initialQuery));
|
|
2261
|
+
const restorationCurrent = useRef9({ storageReady, restoration, active });
|
|
2262
|
+
restorationCurrent.current = { storageReady, restoration, active };
|
|
2263
|
+
const manualQueryEpoch = useRef9(0);
|
|
2264
|
+
const restoreController = useRef9(null);
|
|
2265
|
+
const restoreHandler = useRef9(null);
|
|
2266
|
+
const initialRestore = useRef9(null);
|
|
2267
|
+
const initialRestorationStarted = useRef9(false);
|
|
2268
|
+
const restoreLocation = useCallback12((location, signal) => {
|
|
2269
|
+
if (!restoreHandler.current) return Promise.reject(Error("Query controller is not mounted."));
|
|
2270
|
+
return restoreHandler.current(String(location), signal);
|
|
2271
|
+
}, []);
|
|
2272
|
+
const aggIdRef = useRef9(0);
|
|
2273
|
+
const savedBaseline = useRef9(
|
|
2274
|
+
initRef.current.q.grouping ? { source: sourceSchema.name, query: cloneQueryState(initRef.current.q) } : null
|
|
2275
|
+
);
|
|
2276
|
+
const viewRevision = useRef9(0);
|
|
2277
|
+
const [queryHistory, setQueryHistory] = useState11(
|
|
2278
|
+
() => ({ query: initRef.current.q, past: [], future: [] })
|
|
2279
|
+
);
|
|
2280
|
+
const queryHistoryRef = useRef9(queryHistory);
|
|
2281
|
+
const updateQueryHistory = useCallback12((update) => {
|
|
2282
|
+
const previous = queryHistoryRef.current;
|
|
2283
|
+
const next = update(previous);
|
|
2284
|
+
if (next === previous) return;
|
|
2285
|
+
queryHistoryRef.current = next;
|
|
2286
|
+
setQueryHistory(next);
|
|
2287
|
+
}, []);
|
|
2288
|
+
const storedQuery = queryHistory.query;
|
|
2289
|
+
const [nonce, setNonce] = useState11(0);
|
|
2290
|
+
const query = useMemo9(
|
|
2291
|
+
() => opts.canonicalizeQuery?.(storedQuery) ?? storedQuery,
|
|
2292
|
+
[storedQuery, opts.canonicalizeQuery]
|
|
2293
|
+
);
|
|
2294
|
+
const sourceCatalogue = useMemo9(() => sourceExecutionSchema(sourceSchema, opts.computedExecution), [sourceSchema, opts.computedExecution]);
|
|
2295
|
+
const sourceRows = providedRows;
|
|
2296
|
+
const groupingModel = useMemo9(() => createGroupingModel(sourceCatalogue), [sourceCatalogue]);
|
|
2297
|
+
const remoteGrouping = !!transport?.fetchQueryV3 && transport.queryCapabilities?.version === 3 && transport.queryCapabilities.groupedRows;
|
|
2298
|
+
const groupingSupported = (!transport && sourceRows !== void 0 || remoteGrouping) && opts.grouping !== false;
|
|
2299
|
+
const groupKey = JSON.stringify(query.grouping ?? null);
|
|
2300
|
+
const groupingNow = useMemo9(
|
|
2301
|
+
() => query.grouping ? (opts.evaluationNow ?? Date.now)() : void 0,
|
|
2302
|
+
[sourceRows, sourceSchema, groupingSupported, groupKey, nonce, opts.evaluationNow]
|
|
2303
|
+
);
|
|
2304
|
+
const prepared = useMemo9(() => {
|
|
2305
|
+
if (!query.grouping) return null;
|
|
2306
|
+
try {
|
|
2307
|
+
if (!groupingSupported) throw new Error("Grouped queries require a server with grouped-query support or a dataset already in memory.");
|
|
2308
|
+
if (remoteGrouping) {
|
|
2309
|
+
groupingModel.validate(query.grouping);
|
|
2310
|
+
return { inputs: [], rows: [], error: null };
|
|
2311
|
+
}
|
|
2312
|
+
return { ...prepareGrouping(sourceRows, sourceCatalogue, query.grouping, { now: groupingNow }), error: null };
|
|
2313
|
+
} catch (error2) {
|
|
2314
|
+
return { inputs: [], rows: [], error: error2 };
|
|
2315
|
+
}
|
|
2316
|
+
}, [sourceRows, sourceCatalogue, groupingModel, groupingSupported, groupKey, nonce, groupingNow, remoteGrouping]);
|
|
2317
|
+
const projectionKey = JSON.stringify(query.grouping ? [query.grouping.keys, query.grouping.summaries] : null);
|
|
2318
|
+
const schema = useMemo9(() => query.grouping ? groupingModel.resultSchema(query.grouping.keys, query.grouping.summaries) : sourceSchema, [sourceSchema, groupingModel, projectionKey]);
|
|
2319
|
+
const clientRows = prepared?.rows ?? sourceRows;
|
|
2320
|
+
const executionQuery = useMemo9(() => withoutGrouping2(query), [query]);
|
|
2321
|
+
const byName = useMemo9(() => new Map(schema.fields.map((field) => [field.name, field])), [schema]);
|
|
2322
|
+
const detailsEnabled = Boolean(opts.itemDetails) && !query.grouping;
|
|
2323
|
+
const validateExecution = useCallback12((q) => {
|
|
2324
|
+
if (prepared?.error) throw prepared.error;
|
|
2325
|
+
opts.validateQuery?.(query.grouping ? { ...q, grouping: query.grouping } : q);
|
|
2326
|
+
}, [prepared?.error, opts.validateQuery, groupKey]);
|
|
2327
|
+
const [committedRows, setRows] = useState11([]);
|
|
2328
|
+
const [serverComputed, setServerComputed] = useState11(/* @__PURE__ */ new Map());
|
|
2329
|
+
const [committedFields, setCommittedFields] = useState11([]);
|
|
2330
|
+
const [serverExecution, setServerExecution] = useState11();
|
|
2331
|
+
const [total, setTotal] = useState11(null);
|
|
2332
|
+
const [loading, setLoading] = useState11(false);
|
|
2333
|
+
const [error, setError] = useState11(null);
|
|
2334
|
+
const [committedPage, setCommittedPage] = useState11(null);
|
|
2335
|
+
const [committedSource, setCommittedSource] = useState11(null);
|
|
2336
|
+
const rows = useMemo9(() => committedSource?.schema === schema ? committedRows : [], [committedSource?.schema, schema, committedRows]);
|
|
2337
|
+
const [autoRefreshStatus, setAutoRefreshStatus] = useState11(null);
|
|
2338
|
+
useEffect10(() => {
|
|
829
2339
|
nowRef.current = now;
|
|
830
2340
|
}, [now]);
|
|
831
|
-
|
|
2341
|
+
useEffect10(() => {
|
|
832
2342
|
onRefreshRef.current = opts.onRefresh;
|
|
833
2343
|
}, [opts.onRefresh]);
|
|
834
|
-
const setQuery =
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
2344
|
+
const setQuery = useCallback12(
|
|
2345
|
+
(next) => {
|
|
2346
|
+
manualQueryEpoch.current++;
|
|
2347
|
+
initialRestorationStarted.current = true;
|
|
2348
|
+
restoreController.current?.abort();
|
|
2349
|
+
setStorageReady(true);
|
|
2350
|
+
setRestoration((previous) => !previous.loading && !previous.error ? previous : { loading: false, error: null });
|
|
2351
|
+
updateQueryHistory((history) => {
|
|
2352
|
+
const prev = history.query;
|
|
2353
|
+
const candidate = typeof next === "function" ? next(prev) : next;
|
|
2354
|
+
const nextQuery = normalizeQueryState3(candidate, defaults);
|
|
2355
|
+
if (queriesEqual2(prev, nextQuery)) return history;
|
|
2356
|
+
return { query: cloneQueryState(nextQuery), past: [...history.past, cloneQueryState(prev)], future: [] };
|
|
2357
|
+
});
|
|
2358
|
+
},
|
|
2359
|
+
[defaults, updateQueryHistory]
|
|
2360
|
+
);
|
|
2361
|
+
const sourceQuery = query.grouping?.source ?? query;
|
|
2362
|
+
const setSourceQuery = useCallback12((next) => {
|
|
2363
|
+
setQuery((current) => {
|
|
2364
|
+
const previous = current.grouping?.source ?? current;
|
|
2365
|
+
const source = withoutGrouping2(typeof next === "function" ? next(previous) : next);
|
|
2366
|
+
return current.grouping ? { ...current, offset: 0, grouping: { ...current.grouping, source } } : source;
|
|
842
2367
|
});
|
|
843
|
-
}, [
|
|
844
|
-
|
|
845
|
-
|
|
2368
|
+
}, [setQuery]);
|
|
2369
|
+
const sourceFilters = useQueryFilters(sourceCatalogue, setSourceQuery);
|
|
2370
|
+
const observedQueryUrl = useRef9(null);
|
|
2371
|
+
const recordQueryUrl = useCallback12((url) => {
|
|
2372
|
+
observedQueryUrl.current = JSON.stringify(new URL(url).searchParams.getAll(parameter));
|
|
2373
|
+
}, [parameter]);
|
|
2374
|
+
const links = useQueryLinks(
|
|
2375
|
+
query,
|
|
2376
|
+
linkService,
|
|
2377
|
+
syncUrl,
|
|
2378
|
+
storageReady && !restoration.loading && !restoration.error,
|
|
2379
|
+
parameter,
|
|
2380
|
+
opts.links?.maxUrlBytes ?? 2e3,
|
|
2381
|
+
recordQueryUrl
|
|
2382
|
+
);
|
|
2383
|
+
const restoreSources = useRef9({ storage, linkService, defaults, initialQuery });
|
|
2384
|
+
restoreSources.current = { storage, linkService, defaults, initialQuery };
|
|
2385
|
+
useEffect10(() => {
|
|
846
2386
|
let cancelled = false;
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
2387
|
+
let controller = new AbortController();
|
|
2388
|
+
let generation = 0;
|
|
2389
|
+
initialRestorationStarted.current = Boolean(restoreSources.current.initialQuery);
|
|
2390
|
+
async function restore(navigation = false, location = typeof window !== "undefined" ? window.location.href : "", signal) {
|
|
2391
|
+
initialRestorationStarted.current = true;
|
|
2392
|
+
const { storage: storage2, linkService: linkService2, defaults: defaults2 } = restoreSources.current;
|
|
2393
|
+
const run = ++generation;
|
|
2394
|
+
const epoch = manualQueryEpoch.current;
|
|
2395
|
+
controller.abort();
|
|
2396
|
+
controller = new AbortController();
|
|
2397
|
+
restoreController.current = controller;
|
|
2398
|
+
const currentController = controller;
|
|
2399
|
+
const abort2 = () => currentController.abort();
|
|
2400
|
+
signal?.addEventListener("abort", abort2, { once: true });
|
|
2401
|
+
if (signal?.aborted) currentController.abort();
|
|
2402
|
+
setStorageReady(false);
|
|
2403
|
+
const explicitLink = !!location && (navigation || urlSyncEnabled) && hasQueryLink(location, parameter);
|
|
2404
|
+
setRestoration({ loading: true, error: null });
|
|
2405
|
+
try {
|
|
2406
|
+
if (currentController.signal.aborted) throw new DOMException("Query restoration cancelled.", "AbortError");
|
|
2407
|
+
const fromUrl = location && (navigation || urlSyncEnabled) ? await linkService2.resolve(location, parameter, currentController.signal) : null;
|
|
2408
|
+
const defaultSaved = fromUrl ? null : await storage2.loadDefaultSaved?.(sourceSchema.name);
|
|
2409
|
+
const restored = fromUrl ?? defaultSaved?.query ?? (navigation ? defaults2 : await storage2.loadLast(sourceSchema.name));
|
|
2410
|
+
if (currentController.signal.aborted) throw new DOMException("Query restoration cancelled.", "AbortError");
|
|
2411
|
+
if (!cancelled && run === generation && epoch === manualQueryEpoch.current) {
|
|
2412
|
+
if (restored) {
|
|
2413
|
+
const normalized = normalizeQueryState3(restored, defaults2);
|
|
2414
|
+
savedBaseline.current = defaultSaved || fromUrl?.grouping ? { source: sourceSchema.name, query: cloneQueryState(normalized) } : null;
|
|
2415
|
+
viewRevision.current++;
|
|
2416
|
+
updateQueryHistory(() => ({ query: normalized, past: [], future: [] }));
|
|
2417
|
+
}
|
|
2418
|
+
setStorageReady(true);
|
|
2419
|
+
setRestoration((previous) => !previous.loading && !previous.error ? previous : { loading: false, error: null });
|
|
2420
|
+
}
|
|
2421
|
+
} catch (e) {
|
|
2422
|
+
if (!cancelled && run === generation && epoch === manualQueryEpoch.current) {
|
|
2423
|
+
if (explicitLink || navigation && currentController.signal.aborted) {
|
|
2424
|
+
setRestoration({ loading: false, error: e instanceof Error ? e : Error(String(e)) });
|
|
2425
|
+
} else {
|
|
2426
|
+
updateQueryHistory(() => ({ query: defaults2, past: [], future: [] }));
|
|
2427
|
+
setRestoration({ loading: false, error: null });
|
|
2428
|
+
}
|
|
2429
|
+
setStorageReady(true);
|
|
2430
|
+
}
|
|
2431
|
+
if (navigation && !cancelled && run === generation && epoch === manualQueryEpoch.current) throw e;
|
|
2432
|
+
} finally {
|
|
2433
|
+
signal?.removeEventListener("abort", abort2);
|
|
855
2434
|
}
|
|
856
|
-
}
|
|
2435
|
+
}
|
|
2436
|
+
restoreHandler.current = async (location, signal) => {
|
|
2437
|
+
location = new URL(location, typeof window !== "undefined" ? window.location.href : void 0).href;
|
|
2438
|
+
const key = JSON.stringify(new URL(location).searchParams.getAll(parameter));
|
|
2439
|
+
const current = restorationCurrent.current;
|
|
2440
|
+
if (key === observedQueryUrl.current && current.storageReady && !current.restoration.loading && !current.restoration.error) return;
|
|
2441
|
+
recordQueryUrl(location);
|
|
2442
|
+
await restore(true, location, signal);
|
|
2443
|
+
};
|
|
2444
|
+
initialRestore.current = () => restore();
|
|
2445
|
+
if (typeof window !== "undefined") recordQueryUrl(window.location.href);
|
|
2446
|
+
const navigate = () => {
|
|
2447
|
+
if (!restorationCurrent.current.active) return;
|
|
2448
|
+
const key = JSON.stringify(new URL(window.location.href).searchParams.getAll(parameter));
|
|
2449
|
+
if (key === observedQueryUrl.current) return;
|
|
2450
|
+
observedQueryUrl.current = key;
|
|
2451
|
+
void restore(true).catch(() => {
|
|
2452
|
+
});
|
|
2453
|
+
};
|
|
2454
|
+
if (urlSyncEnabled && typeof window !== "undefined") window.addEventListener("popstate", navigate);
|
|
857
2455
|
return () => {
|
|
858
2456
|
cancelled = true;
|
|
2457
|
+
controller.abort();
|
|
2458
|
+
restoreHandler.current = null;
|
|
2459
|
+
initialRestore.current = null;
|
|
2460
|
+
if (urlSyncEnabled && typeof window !== "undefined") window.removeEventListener("popstate", navigate);
|
|
859
2461
|
};
|
|
860
|
-
}, []);
|
|
2462
|
+
}, [sourceSchema.name, urlSyncEnabled, parameter, recordQueryUrl, updateQueryHistory]);
|
|
2463
|
+
useEffect10(() => {
|
|
2464
|
+
if (active && !initialRestorationStarted.current) void initialRestore.current?.();
|
|
2465
|
+
}, [active, sourceSchema.name, urlSyncEnabled, parameter, recordQueryUrl]);
|
|
861
2466
|
const { api: computed, displaySchema } = useComputedColumns({
|
|
862
2467
|
schema,
|
|
863
|
-
query,
|
|
2468
|
+
query: executionQuery,
|
|
864
2469
|
rows,
|
|
2470
|
+
serverComputed,
|
|
2471
|
+
...serverExecution ? { serverExecution } : {},
|
|
2472
|
+
...opts.computedExecution ? { execution: opts.computedExecution } : {},
|
|
865
2473
|
...opts.computedColumnStore ? { store: opts.computedColumnStore } : {},
|
|
866
2474
|
...opts.formulaWorkerFactory ? { workerFactory: opts.formulaWorkerFactory } : {},
|
|
867
2475
|
...transport ? { transport } : {},
|
|
868
2476
|
...clientRows ? { clientRows } : {}
|
|
869
2477
|
});
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
() => transport ? `server:${JSON.stringify(serverRowQuery)}` : `client:${JSON.stringify([query.where, query.orderBy, query.limit, query.offset])}`,
|
|
874
|
-
[transport, serverRowQuery, query.where, query.orderBy, query.limit, query.offset]
|
|
2478
|
+
const resolveComputed = useCallback12(
|
|
2479
|
+
(name) => isComputedField4(name) ? computed.compile(`[${name.replace(/]/g, "]] ".trim())}]`) : void 0,
|
|
2480
|
+
[computed.compile]
|
|
875
2481
|
);
|
|
876
|
-
|
|
2482
|
+
const metricCompile = useCallback12(
|
|
2483
|
+
(clause) => {
|
|
2484
|
+
const diagnostics = validateMetric2(
|
|
2485
|
+
clause,
|
|
2486
|
+
displaySchema,
|
|
2487
|
+
resolveComputed
|
|
2488
|
+
);
|
|
2489
|
+
if (diagnostics.length)
|
|
2490
|
+
throw new Error(diagnostics.map((d) => d.message).join(" "));
|
|
2491
|
+
return metricPlans3(clause, displaySchema, resolveComputed);
|
|
2492
|
+
},
|
|
2493
|
+
[displaySchema, resolveComputed]
|
|
2494
|
+
);
|
|
2495
|
+
const computedRevisions = useMemo9(
|
|
2496
|
+
() => Object.fromEntries(computed.definitions.map((d) => [d.id, d.revision])),
|
|
2497
|
+
[computed.definitions]
|
|
2498
|
+
);
|
|
2499
|
+
const definitionKey = JSON.stringify(computedRevisions);
|
|
2500
|
+
const queryKey = useMemo9(() => JSON.stringify(query), [query]);
|
|
2501
|
+
const serverRowQuery = useMemo9(() => {
|
|
2502
|
+
const execution = opts.computedExecution;
|
|
2503
|
+
const detailConfig = typeof opts.itemDetails === "object" ? opts.itemDetails : void 0;
|
|
2504
|
+
const page = query.itemDetails?.page ?? detailConfig?.page;
|
|
2505
|
+
const pageEnabled = Boolean(opts.itemDetails) && (query.itemDetails?.modes ?? initialDetailModes(detailConfig)).includes("page") && (!detailConfig?.modes || detailConfig.modes.includes("page"));
|
|
2506
|
+
const pageFields = pageEnabled ? itemPageFields(page?.urlTemplate ?? "") : [];
|
|
2507
|
+
const requestQuery = pageFields.length ? { ...executionQuery, select: [
|
|
2508
|
+
...query.select.length ? query.select : defaults.select,
|
|
2509
|
+
...pageFields.filter((name) => displaySchema.fields.some((field) => field.name === name)).map((field) => ({ field }))
|
|
2510
|
+
] } : executionQuery;
|
|
2511
|
+
const v2Request = execution && transport?.fetchRowsV2 ? toServerQueryV2(requestQuery, displaySchema, execution) : void 0;
|
|
2512
|
+
const request = v2Request ?? toServerQuery2(requestQuery, displaySchema);
|
|
2513
|
+
if (v2Request)
|
|
2514
|
+
for (const definition of computed.definitions) {
|
|
2515
|
+
if ((execution?.fields[computedFieldName2(definition.id)]?.select || v2Request.orderBy.some(
|
|
2516
|
+
(term) => term.field === computedFieldName2(definition.id)
|
|
2517
|
+
)) && executionRevision(execution, definition.id) !== void 0 && executionRevision(execution, definition.id) !== definition.revision)
|
|
2518
|
+
v2Request.diagnostics.push({
|
|
2519
|
+
from: 0,
|
|
2520
|
+
to: 1,
|
|
2521
|
+
code: "definition_changed",
|
|
2522
|
+
message: "Computed definitions changed. Refresh the server execution handshake."
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
for (const clause of query.aggregations ?? [])
|
|
2526
|
+
if (clause.scope === "shownRows") {
|
|
2527
|
+
try {
|
|
2528
|
+
for (const name of metricDependencies(
|
|
2529
|
+
clause,
|
|
2530
|
+
schema,
|
|
2531
|
+
resolveComputed
|
|
2532
|
+
)) {
|
|
2533
|
+
const field = schema.fields.find((f) => f.name === name);
|
|
2534
|
+
if (field?.source.kind === "backend") request.select.push(name);
|
|
2535
|
+
else
|
|
2536
|
+
for (const dep of field?.source.kind === "derived" ? field.source.dependencies ?? [] : [])
|
|
2537
|
+
request.select.push(dep);
|
|
2538
|
+
}
|
|
2539
|
+
} catch {
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
request.select = [...new Set(request.select)].sort();
|
|
2543
|
+
return request;
|
|
2544
|
+
}, [
|
|
2545
|
+
queryKey,
|
|
2546
|
+
displaySchema,
|
|
2547
|
+
transport,
|
|
2548
|
+
computedRevisions,
|
|
2549
|
+
schema,
|
|
2550
|
+
resolveComputed,
|
|
2551
|
+
opts.computedExecution,
|
|
2552
|
+
opts.itemDetails,
|
|
2553
|
+
defaults
|
|
2554
|
+
]);
|
|
2555
|
+
const rowQueryKey = useMemo9(
|
|
2556
|
+
() => transport ? `server:${JSON.stringify(serverRowQuery)}:${transport.fetchQueryV3 ? JSON.stringify([
|
|
2557
|
+
groupKey,
|
|
2558
|
+
groupingNow,
|
|
2559
|
+
query.select.map((column) => column.field),
|
|
2560
|
+
query.where,
|
|
2561
|
+
query.orderBy,
|
|
2562
|
+
query.limit,
|
|
2563
|
+
query.offset
|
|
2564
|
+
]) : ""}` : `client:${JSON.stringify([query.where, query.orderBy, query.limit, query.offset])}`,
|
|
2565
|
+
[
|
|
2566
|
+
transport,
|
|
2567
|
+
serverRowQuery,
|
|
2568
|
+
groupKey,
|
|
2569
|
+
groupingNow,
|
|
2570
|
+
query.select,
|
|
2571
|
+
query.where,
|
|
2572
|
+
query.orderBy,
|
|
2573
|
+
query.limit,
|
|
2574
|
+
query.offset
|
|
2575
|
+
]
|
|
2576
|
+
);
|
|
2577
|
+
const executionKey = JSON.stringify(opts.computedExecution ?? null);
|
|
2578
|
+
const pageContext = `${schema.name}:${rowQueryKey}:${nonce}:${executionKey}`;
|
|
2579
|
+
useEffect10(() => {
|
|
2580
|
+
if (!active || !storageReady || restoration.loading || restoration.error) {
|
|
2581
|
+
setRows((previous) => previous.length ? [] : previous);
|
|
2582
|
+
setTotal(null);
|
|
2583
|
+
setCommittedPage(null);
|
|
2584
|
+
setLoading(false);
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
877
2587
|
const ac = new AbortController();
|
|
878
2588
|
let cancelled = false;
|
|
879
2589
|
const run = async () => {
|
|
880
2590
|
setLoading(true);
|
|
881
2591
|
setError(null);
|
|
882
2592
|
try {
|
|
883
|
-
|
|
2593
|
+
validateExecution(query);
|
|
884
2594
|
if (transport) {
|
|
885
|
-
|
|
2595
|
+
if (transport.fetchQueryV3 && transport.queryCapabilities?.version === 3 && (query.grouping || query.select.some((column) => isComputedField4(column.field)) || query.orderBy.some((term) => isComputedField4(term.field)) || query.where.some((term) => ("any" in term ? term.any : [term]).some((predicate) => isComputedField4(predicate.field))))) {
|
|
2596
|
+
const request = toQueryRequestV3(
|
|
2597
|
+
{ ...query, aggregations: [], select: [
|
|
2598
|
+
...query.select.filter((column) => isComputedField4(column.field)),
|
|
2599
|
+
...serverRowQuery.select.map((field) => ({ field }))
|
|
2600
|
+
] },
|
|
2601
|
+
sourceSchema,
|
|
2602
|
+
transport.queryCapabilities,
|
|
2603
|
+
opts.computedExecution,
|
|
2604
|
+
{ now: groupingNow ?? (opts.evaluationNow ?? Date.now)() }
|
|
2605
|
+
);
|
|
2606
|
+
if (request.diagnostics.length) throw Error(request.diagnostics.join(" "));
|
|
2607
|
+
const result = await transport.fetchQueryV3(request, ac.signal);
|
|
2608
|
+
validateQueryResultV3(result, request, query.grouping ? schema : sourceCatalogue);
|
|
2609
|
+
if (!cancelled) {
|
|
2610
|
+
setCommittedFields(request.result.select);
|
|
2611
|
+
setServerExecution(result.execution);
|
|
2612
|
+
setServerComputed(new Map(result.rows.map((row) => [
|
|
2613
|
+
row[schema.idField],
|
|
2614
|
+
Object.fromEntries(Object.entries(row).filter(([name]) => isComputedField4(name)).map(([name, value]) => [name.slice(10), value && typeof value === "object" && "computedError" in value ? { value: null, error: { code: String(value.computedError) } } : { value }]))
|
|
2615
|
+
])));
|
|
2616
|
+
setRows(result.rows);
|
|
2617
|
+
setTotal(result.total);
|
|
2618
|
+
setCommittedPage(pageContext);
|
|
2619
|
+
setCommittedSource({ schema, transport, clientRows });
|
|
2620
|
+
}
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
const useV2 = "version" in serverRowQuery && transport.fetchRowsV2;
|
|
2624
|
+
if ("diagnostics" in serverRowQuery && Array.isArray(serverRowQuery.diagnostics) && serverRowQuery.diagnostics.length)
|
|
2625
|
+
throw Error(
|
|
2626
|
+
serverRowQuery.diagnostics.map((d) => d.message).join(" ")
|
|
2627
|
+
);
|
|
2628
|
+
if (!useV2 && query.orderBy.some((s) => isComputedField4(s.field)))
|
|
2629
|
+
throw Error(
|
|
2630
|
+
"Global computed sorting requires a server execution handshake and fetchRowsV2."
|
|
2631
|
+
);
|
|
2632
|
+
const res = useV2 ? await transport.fetchRowsV2(
|
|
2633
|
+
serverRowQuery,
|
|
2634
|
+
ac.signal
|
|
2635
|
+
) : await transport.fetchRows(serverRowQuery, ac.signal);
|
|
2636
|
+
if (useV2)
|
|
2637
|
+
validateComputedResponse(
|
|
2638
|
+
serverRowQuery,
|
|
2639
|
+
res,
|
|
2640
|
+
schema
|
|
2641
|
+
);
|
|
886
2642
|
if (!cancelled) {
|
|
2643
|
+
setCommittedFields(serverRowQuery.select);
|
|
2644
|
+
setServerExecution(
|
|
2645
|
+
useV2 ? res.execution : void 0
|
|
2646
|
+
);
|
|
2647
|
+
if ("computed" in res && Array.isArray(res.computed))
|
|
2648
|
+
setServerComputed(
|
|
2649
|
+
new Map(res.computed.map((c) => [c.id, c.values]))
|
|
2650
|
+
);
|
|
2651
|
+
else setServerComputed(/* @__PURE__ */ new Map());
|
|
887
2652
|
setRows(res.rows);
|
|
888
2653
|
setTotal(res.total);
|
|
2654
|
+
setCommittedPage(pageContext);
|
|
2655
|
+
setCommittedSource({ schema, transport, clientRows });
|
|
889
2656
|
}
|
|
890
2657
|
} else {
|
|
891
|
-
const res = applyQuery2(
|
|
2658
|
+
const res = applyQuery2(
|
|
2659
|
+
clientRows ?? [],
|
|
2660
|
+
executionQuery,
|
|
2661
|
+
schema,
|
|
2662
|
+
groupingNow !== void 0 ? { now: groupingNow } : {}
|
|
2663
|
+
);
|
|
892
2664
|
if (!cancelled) {
|
|
893
2665
|
setRows(res.rows);
|
|
894
2666
|
setTotal(res.total);
|
|
2667
|
+
setCommittedPage(pageContext);
|
|
2668
|
+
setCommittedSource({ schema, transport, clientRows });
|
|
895
2669
|
}
|
|
896
2670
|
}
|
|
897
2671
|
} catch (e) {
|
|
@@ -899,6 +2673,7 @@ function useQueryTable(opts) {
|
|
|
899
2673
|
setError(e);
|
|
900
2674
|
setRows([]);
|
|
901
2675
|
setTotal(null);
|
|
2676
|
+
setCommittedPage(null);
|
|
902
2677
|
}
|
|
903
2678
|
} finally {
|
|
904
2679
|
if (!cancelled) setLoading(false);
|
|
@@ -910,159 +2685,211 @@ function useQueryTable(opts) {
|
|
|
910
2685
|
clearTimeout(t);
|
|
911
2686
|
ac.abort();
|
|
912
2687
|
};
|
|
913
|
-
}, [
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
2688
|
+
}, [
|
|
2689
|
+
rowQueryKey,
|
|
2690
|
+
transport,
|
|
2691
|
+
clientRows,
|
|
2692
|
+
schema,
|
|
2693
|
+
debounceMs,
|
|
2694
|
+
nonce,
|
|
2695
|
+
validateExecution,
|
|
2696
|
+
executionKey,
|
|
2697
|
+
groupingNow,
|
|
2698
|
+
restoration,
|
|
2699
|
+
active,
|
|
2700
|
+
storageReady
|
|
2701
|
+
]);
|
|
2702
|
+
useEffect10(() => {
|
|
2703
|
+
if (!storageReady || restoration.loading || restoration.error) return;
|
|
2704
|
+
const t = setTimeout(() => void storage.saveLast(sourceSchema.name, query), 400);
|
|
924
2705
|
return () => clearTimeout(t);
|
|
925
|
-
}, [queryKey, storage,
|
|
926
|
-
const idField =
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
2706
|
+
}, [queryKey, storage, sourceSchema.name, query, restoration, storageReady]);
|
|
2707
|
+
const idField = useMemo9(
|
|
2708
|
+
() => schema.fields.find((f) => f.name === schema.idField),
|
|
2709
|
+
[schema]
|
|
2710
|
+
);
|
|
2711
|
+
const rowId = useCallback12(
|
|
2712
|
+
(row) => (() => {
|
|
2713
|
+
const value = idField ? readFieldValue4(idField, row) : row?.[schema.idField];
|
|
2714
|
+
return typeof value === "string" || typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
2715
|
+
})(),
|
|
2716
|
+
[idField, schema.idField]
|
|
2717
|
+
);
|
|
2718
|
+
const displayedIds = useMemo9(
|
|
2719
|
+
() => rows.map(rowId).filter((id) => id != null),
|
|
2720
|
+
[rows, rowId]
|
|
930
2721
|
);
|
|
931
|
-
const displayedIds = useMemo6(() => rows.map(rowId).filter((id) => id != null), [rows, rowId]);
|
|
932
2722
|
const selection = useSelection(displayedIds);
|
|
933
|
-
const
|
|
2723
|
+
const identityScope = JSON.stringify([sourceSchema.name, query.grouping?.keys ?? null]);
|
|
2724
|
+
useEffect10(() => {
|
|
2725
|
+
selection.clear();
|
|
2726
|
+
}, [identityScope, selection.clear]);
|
|
2727
|
+
const columnSchema = useMemo9(() => detailColumnSchema(displaySchema, detailsEnabled), [displaySchema, detailsEnabled]);
|
|
2728
|
+
const resultDefaults = useMemo9(() => query.grouping ? defaultsFor(schema) : defaults, [Boolean(query.grouping), schema, defaults]);
|
|
2729
|
+
const select = useSelect(query, setQuery, columnSchema);
|
|
934
2730
|
const columnDrag = useColumnDrag();
|
|
935
|
-
const
|
|
936
|
-
|
|
2731
|
+
const savedQueries = useSavedQueries(
|
|
2732
|
+
sourceSchema.name,
|
|
937
2733
|
query,
|
|
938
|
-
(q) =>
|
|
2734
|
+
(q) => {
|
|
2735
|
+
const normalized = normalizeQueryState3(q, defaults);
|
|
2736
|
+
savedBaseline.current = { source: sourceSchema.name, query: cloneQueryState(normalized) };
|
|
2737
|
+
viewRevision.current++;
|
|
2738
|
+
setQuery(normalized);
|
|
2739
|
+
},
|
|
939
2740
|
storage,
|
|
940
|
-
now
|
|
941
|
-
|
|
942
|
-
const aggState = useAggregations(query, schema, transport, clientRows, debounceMs, nonce, opts.validateQuery);
|
|
943
|
-
const canUndo = undoStack.current.length > 0;
|
|
944
|
-
const canRedo = redoStack.current.length > 0;
|
|
945
|
-
const undo = useCallback6(() => {
|
|
946
|
-
setQueryState((prev) => {
|
|
947
|
-
const previous = undoStack.current.pop();
|
|
948
|
-
if (!previous) return prev;
|
|
949
|
-
redoStack.current.push(cloneQueryState(prev));
|
|
950
|
-
return cloneQueryState(previous);
|
|
951
|
-
});
|
|
952
|
-
}, []);
|
|
953
|
-
const redo = useCallback6(() => {
|
|
954
|
-
setQueryState((prev) => {
|
|
955
|
-
const next = redoStack.current.pop();
|
|
956
|
-
if (!next) return prev;
|
|
957
|
-
undoStack.current.push(cloneQueryState(prev));
|
|
958
|
-
return cloneQueryState(next);
|
|
959
|
-
});
|
|
960
|
-
}, []);
|
|
961
|
-
const patch = useCallback6((p) => setQuery((prev) => ({ ...prev, ...p })), [setQuery]);
|
|
962
|
-
const addFilter = useCallback6((clause) => setQuery((q) => ({ ...q, offset: 0, where: [...q.where, clause] })), [setQuery]);
|
|
963
|
-
const updateFilter = useCallback6(
|
|
964
|
-
(index, clause) => setQuery((q) => ({ ...q, offset: 0, where: q.where.map((t, i) => i === index ? clause : t) })),
|
|
965
|
-
[setQuery]
|
|
966
|
-
);
|
|
967
|
-
const removeFilter = useCallback6(
|
|
968
|
-
(index) => setQuery((q) => ({ ...q, offset: 0, where: q.where.filter((_, i) => i !== index) })),
|
|
969
|
-
[setQuery]
|
|
970
|
-
);
|
|
971
|
-
const clearFilters = useCallback6(() => setQuery((q) => ({ ...q, offset: 0, where: [] })), [setQuery]);
|
|
972
|
-
const updatePredicate = useCallback6(
|
|
973
|
-
(termIndex, predIndex, clause) => setQuery((q) => ({
|
|
974
|
-
...q,
|
|
975
|
-
offset: 0,
|
|
976
|
-
where: q.where.map((term, i) => {
|
|
977
|
-
if (i !== termIndex) return term;
|
|
978
|
-
if (isOrGroup(term)) return { any: term.any.map((c, j) => j === predIndex ? clause : c) };
|
|
979
|
-
return clause;
|
|
980
|
-
})
|
|
981
|
-
})),
|
|
982
|
-
[setQuery]
|
|
2741
|
+
now,
|
|
2742
|
+
links.snapshot
|
|
983
2743
|
);
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
else if (members.length > 1) where.push({ any: members });
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
});
|
|
999
|
-
return { ...q, offset: 0, where };
|
|
1000
|
-
}),
|
|
1001
|
-
[setQuery]
|
|
2744
|
+
const saved = { ...savedQueries, save: async (name) => {
|
|
2745
|
+
const item = await savedQueries.save(name);
|
|
2746
|
+
savedBaseline.current = { source: sourceSchema.name, query: cloneQueryState(normalizeQueryState3(item.query, defaults)) };
|
|
2747
|
+
viewRevision.current++;
|
|
2748
|
+
return item;
|
|
2749
|
+
} };
|
|
2750
|
+
const pageReady = committedPage === pageContext && committedSource?.schema === schema && committedSource?.transport === transport && committedSource?.clientRows === clientRows && !error;
|
|
2751
|
+
const dataToken = useRef9(0);
|
|
2752
|
+
const dataVersion = useMemo9(
|
|
2753
|
+
() => ++dataToken.current,
|
|
2754
|
+
[schema, transport, clientRows, nonce, definitionKey, executionKey]
|
|
1002
2755
|
);
|
|
1003
|
-
const
|
|
1004
|
-
(
|
|
1005
|
-
|
|
1006
|
-
|
|
2756
|
+
const executeV3Metrics = useCallback12(async (metricQuery, signal) => {
|
|
2757
|
+
if (!transport?.fetchQueryV3 || transport.queryCapabilities?.version !== 3) throw Error("The v3 query executor is unavailable.");
|
|
2758
|
+
const request = toQueryRequestV3(
|
|
2759
|
+
query.grouping ? { ...metricQuery, grouping: query.grouping } : metricQuery,
|
|
2760
|
+
sourceSchema,
|
|
2761
|
+
transport.queryCapabilities,
|
|
2762
|
+
opts.computedExecution,
|
|
2763
|
+
{ now: groupingNow ?? (opts.evaluationNow ?? Date.now)(), rows: false }
|
|
2764
|
+
);
|
|
2765
|
+
if (request.diagnostics.length) throw Error(request.diagnostics.join(" "));
|
|
2766
|
+
const result = await transport.fetchQueryV3(request, signal);
|
|
2767
|
+
validateQueryResultV3(result, request, query.grouping ? schema : sourceCatalogue);
|
|
2768
|
+
return { metrics: result.metrics };
|
|
2769
|
+
}, [transport, sourceSchema, sourceCatalogue, schema, opts.computedExecution, groupKey, groupingNow, opts.evaluationNow]);
|
|
2770
|
+
const previewGrouping = useCallback12(async (draft, signal) => {
|
|
2771
|
+
if (!active || !storageReady || restoration.loading || restoration.error) throw Error("Wait for query restoration before previewing groups.");
|
|
2772
|
+
if (!transport?.fetchQueryV3 || !transport.queryCapabilities?.groupedRows || !draft.grouping) throw Error("The server group preview is unavailable.");
|
|
2773
|
+
const request = toQueryRequestV3(
|
|
2774
|
+
{ ...draft, aggregations: [] },
|
|
2775
|
+
sourceSchema,
|
|
2776
|
+
transport.queryCapabilities,
|
|
2777
|
+
opts.computedExecution,
|
|
2778
|
+
{ now: groupingNow ?? (opts.evaluationNow ?? Date.now)() }
|
|
2779
|
+
);
|
|
2780
|
+
if (request.diagnostics.length) throw Error(request.diagnostics.join(" "));
|
|
2781
|
+
const result = await transport.fetchQueryV3(request, signal);
|
|
2782
|
+
validateQueryResultV3(result, request, groupingModel.resultSchema(draft.grouping.keys, draft.grouping.summaries));
|
|
2783
|
+
return { rows: result.rows, total: result.total, sourceTotal: result.sourceTotal };
|
|
2784
|
+
}, [transport, sourceSchema, opts.computedExecution, groupingModel, groupingNow, opts.evaluationNow, active, storageReady, restoration.loading, restoration.error]);
|
|
2785
|
+
const aggState = useAggregations(
|
|
2786
|
+
executionQuery,
|
|
2787
|
+
schema,
|
|
2788
|
+
transport,
|
|
2789
|
+
clientRows,
|
|
2790
|
+
debounceMs,
|
|
2791
|
+
nonce,
|
|
2792
|
+
validateExecution,
|
|
2793
|
+
{
|
|
2794
|
+
...transport?.fetchQueryV3 && transport.queryCapabilities?.version === 3 && (query.grouping || query.aggregations?.some((metric) => metric.dateGrouping) || query.where.some((term) => ("any" in term ? term.any : [term]).some((predicate) => isComputedField4(predicate.field)))) ? { executeV3: executeV3Metrics, queryContextKey: groupKey } : {},
|
|
2795
|
+
...groupingNow !== void 0 ? { now: groupingNow, strictLocalMetrics: true } : {},
|
|
2796
|
+
shownRows: rows,
|
|
2797
|
+
rowsReady: pageReady,
|
|
2798
|
+
rowsError: error,
|
|
2799
|
+
...transport ? { shownFields: committedFields } : {},
|
|
2800
|
+
resolveComputed,
|
|
2801
|
+
revisions: computedRevisions,
|
|
2802
|
+
...opts.computedExecution ? {
|
|
2803
|
+
execution: pageReady && serverExecution ? serverExecution : opts.computedExecution
|
|
2804
|
+
} : {},
|
|
2805
|
+
...opts.formulaWorkerFactory ? { workerFactory: opts.formulaWorkerFactory } : {}
|
|
1007
2806
|
},
|
|
1008
|
-
|
|
2807
|
+
active && storageReady && !restoration.loading && !restoration.error
|
|
1009
2808
|
);
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
2809
|
+
const queryRef = useRef9(query);
|
|
2810
|
+
queryRef.current = query;
|
|
2811
|
+
const replaceAggregations = useCallback12(
|
|
2812
|
+
(clauses, expected) => {
|
|
2813
|
+
if (expected && JSON.stringify(queryRef.current.aggregations ?? []) !== JSON.stringify(expected))
|
|
2814
|
+
throw new Error(
|
|
2815
|
+
"Metrics changed while editing. Reopen the editor to use the latest configuration."
|
|
2816
|
+
);
|
|
2817
|
+
for (const clause of clauses) metricCompile(clause);
|
|
2818
|
+
setQuery((q) => ({
|
|
2819
|
+
...q,
|
|
2820
|
+
aggregations: JSON.parse(
|
|
2821
|
+
JSON.stringify(clauses)
|
|
2822
|
+
)
|
|
2823
|
+
}));
|
|
2824
|
+
},
|
|
2825
|
+
[setQuery, metricCompile]
|
|
1021
2826
|
);
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
})
|
|
2827
|
+
const canUndo = queryHistory.past.length > 0;
|
|
2828
|
+
const canRedo = queryHistory.future.length > 0;
|
|
2829
|
+
const undo = useCallback12(() => {
|
|
2830
|
+
if (!queryHistoryRef.current.past.length) return;
|
|
2831
|
+
manualQueryEpoch.current++;
|
|
2832
|
+
restoreController.current?.abort();
|
|
2833
|
+
setStorageReady(true);
|
|
2834
|
+
setRestoration({ loading: false, error: null });
|
|
2835
|
+
updateQueryHistory((history) => {
|
|
2836
|
+
const previous = history.past[history.past.length - 1];
|
|
2837
|
+
if (!previous) return history;
|
|
2838
|
+
return { query: cloneQueryState(previous), past: history.past.slice(0, -1), future: [...history.future, cloneQueryState(history.query)] };
|
|
2839
|
+
});
|
|
2840
|
+
}, [updateQueryHistory]);
|
|
2841
|
+
const redo = useCallback12(() => {
|
|
2842
|
+
if (!queryHistoryRef.current.future.length) return;
|
|
2843
|
+
manualQueryEpoch.current++;
|
|
2844
|
+
restoreController.current?.abort();
|
|
2845
|
+
setStorageReady(true);
|
|
2846
|
+
setRestoration({ loading: false, error: null });
|
|
2847
|
+
updateQueryHistory((history) => {
|
|
2848
|
+
const next = history.future[history.future.length - 1];
|
|
2849
|
+
if (!next) return history;
|
|
2850
|
+
return { query: cloneQueryState(next), past: [...history.past, cloneQueryState(history.query)], future: history.future.slice(0, -1) };
|
|
2851
|
+
});
|
|
2852
|
+
}, [updateQueryHistory]);
|
|
2853
|
+
const patch = useCallback12(
|
|
2854
|
+
(p) => setQuery((prev) => ({ ...prev, ...p })),
|
|
1030
2855
|
[setQuery]
|
|
1031
2856
|
);
|
|
1032
|
-
const mergeFilters =
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
const
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
q.where.forEach((term, i) => {
|
|
1041
|
-
if (i === sourceIndex) return;
|
|
1042
|
-
where.push(i === targetIndex ? merged : term);
|
|
1043
|
-
});
|
|
1044
|
-
return { ...q, offset: 0, where };
|
|
1045
|
-
}),
|
|
1046
|
-
[setQuery]
|
|
2857
|
+
const { addFilter, updateFilter, removeFilter, clearFilters, updatePredicate, removePredicate, negatePredicate, reorderFilters, mergeFilters } = useQueryFilters(schema, setQuery);
|
|
2858
|
+
const resetAll = useCallback12(
|
|
2859
|
+
() => {
|
|
2860
|
+
const baseline = savedBaseline.current;
|
|
2861
|
+
viewRevision.current++;
|
|
2862
|
+
setQuery(cloneQueryState(baseline?.source === sourceSchema.name ? baseline.query : defaults));
|
|
2863
|
+
},
|
|
2864
|
+
[setQuery, defaults, sourceSchema.name]
|
|
1047
2865
|
);
|
|
1048
|
-
const
|
|
1049
|
-
const addAggregation = useCallback6(
|
|
2866
|
+
const addAggregation = useCallback12(
|
|
1050
2867
|
(partial) => {
|
|
1051
2868
|
const id = `a${nowRef.current()}-${aggIdRef.current++}`;
|
|
1052
|
-
const clause = {
|
|
1053
|
-
|
|
2869
|
+
const clause = {
|
|
2870
|
+
id,
|
|
2871
|
+
op: "count",
|
|
2872
|
+
groupBy: [],
|
|
2873
|
+
...partial
|
|
2874
|
+
};
|
|
2875
|
+
setQuery((q) => ({
|
|
2876
|
+
...q,
|
|
2877
|
+
aggregations: [...q.aggregations ?? [], clause]
|
|
2878
|
+
}));
|
|
1054
2879
|
return id;
|
|
1055
2880
|
},
|
|
1056
2881
|
[setQuery]
|
|
1057
2882
|
);
|
|
1058
|
-
const updateAggregation =
|
|
2883
|
+
const updateAggregation = useCallback12(
|
|
1059
2884
|
(id, patch2) => setQuery((q) => ({
|
|
1060
2885
|
...q,
|
|
1061
|
-
aggregations: (q.aggregations ?? []).map(
|
|
2886
|
+
aggregations: (q.aggregations ?? []).map(
|
|
2887
|
+
(a) => a.id === id ? mergeAggregation(a, patch2) : a
|
|
2888
|
+
)
|
|
1062
2889
|
})),
|
|
1063
2890
|
[setQuery]
|
|
1064
2891
|
);
|
|
1065
|
-
const moveAggregation =
|
|
2892
|
+
const moveAggregation = useCallback12(
|
|
1066
2893
|
(id, toIndex) => setQuery((q) => {
|
|
1067
2894
|
const current = q.aggregations ?? [];
|
|
1068
2895
|
const from = current.findIndex((a) => a.id === id);
|
|
@@ -1074,26 +2901,45 @@ function useQueryTable(opts) {
|
|
|
1074
2901
|
}),
|
|
1075
2902
|
[setQuery]
|
|
1076
2903
|
);
|
|
1077
|
-
const removeAggregation =
|
|
1078
|
-
(id) => setQuery((q) => ({
|
|
2904
|
+
const removeAggregation = useCallback12(
|
|
2905
|
+
(id) => setQuery((q) => ({
|
|
2906
|
+
...q,
|
|
2907
|
+
aggregations: (q.aggregations ?? []).filter((a) => a.id !== id)
|
|
2908
|
+
})),
|
|
1079
2909
|
[setQuery]
|
|
1080
2910
|
);
|
|
1081
|
-
const clearAggregations =
|
|
2911
|
+
const clearAggregations = useCallback12(
|
|
1082
2912
|
() => setQuery((q) => ({ ...q, aggregations: [] })),
|
|
1083
2913
|
[setQuery]
|
|
1084
2914
|
);
|
|
1085
|
-
const setSort =
|
|
1086
|
-
|
|
1087
|
-
|
|
2915
|
+
const setSort = useCallback12(
|
|
2916
|
+
(orderBy) => setQuery((q) => ({ ...q, offset: 0, orderBy })),
|
|
2917
|
+
[setQuery]
|
|
2918
|
+
);
|
|
2919
|
+
const toggleSort = useCallback12(
|
|
2920
|
+
(field, additive = false) => setQuery((q) => ({
|
|
2921
|
+
...q,
|
|
2922
|
+
offset: 0,
|
|
2923
|
+
orderBy: nextOrderBy(q.orderBy, field, additive)
|
|
2924
|
+
})),
|
|
2925
|
+
[setQuery]
|
|
2926
|
+
);
|
|
2927
|
+
const setLimit = useCallback12(
|
|
2928
|
+
(limit) => patch({ limit, offset: 0 }),
|
|
2929
|
+
[patch]
|
|
2930
|
+
);
|
|
2931
|
+
const setOffset = useCallback12((offset) => patch({ offset }), [patch]);
|
|
2932
|
+
const nextPage = useCallback12(
|
|
2933
|
+
() => setQuery((q) => ({ ...q, offset: q.offset + q.limit })),
|
|
2934
|
+
[setQuery]
|
|
2935
|
+
);
|
|
2936
|
+
const prevPage = useCallback12(
|
|
2937
|
+
() => setQuery((q) => ({ ...q, offset: Math.max(0, q.offset - q.limit) })),
|
|
1088
2938
|
[setQuery]
|
|
1089
2939
|
);
|
|
1090
|
-
const
|
|
1091
|
-
const setOffset = useCallback6((offset) => patch({ offset }), [patch]);
|
|
1092
|
-
const nextPage = useCallback6(() => setQuery((q) => ({ ...q, offset: q.offset + q.limit })), [setQuery]);
|
|
1093
|
-
const prevPage = useCallback6(() => setQuery((q) => ({ ...q, offset: Math.max(0, q.offset - q.limit) })), [setQuery]);
|
|
1094
|
-
const filterValues = useCallback6(
|
|
2940
|
+
const filterValues = useCallback12(
|
|
1095
2941
|
async (field, search) => {
|
|
1096
|
-
if (!
|
|
2942
|
+
if (!metadataTransport?.fetchDistinctValues || query.grouping && !query.grouping.keys.includes(field)) {
|
|
1097
2943
|
const f = byName.get(field);
|
|
1098
2944
|
if (!f || !clientRows) return { values: [], hasMore: false };
|
|
1099
2945
|
const fieldHasNull = fieldHasNullInRows(f, clientRows);
|
|
@@ -1104,18 +2950,18 @@ function useQueryTable(opts) {
|
|
|
1104
2950
|
hasNull: fieldHasNull
|
|
1105
2951
|
};
|
|
1106
2952
|
}
|
|
1107
|
-
return
|
|
2953
|
+
return metadataTransport.fetchDistinctValues({ field, search });
|
|
1108
2954
|
},
|
|
1109
|
-
[
|
|
2955
|
+
[metadataTransport, byName, clientRows, groupKey]
|
|
1110
2956
|
);
|
|
1111
|
-
const fieldStats =
|
|
2957
|
+
const fieldStats = useCallback12(
|
|
1112
2958
|
async (fields, signal) => {
|
|
1113
2959
|
const requested = [...new Set(fields)];
|
|
1114
|
-
if (
|
|
2960
|
+
if (metadataTransport?.fetchFieldStats && !query.grouping) {
|
|
1115
2961
|
const backendFields = requested.filter(
|
|
1116
2962
|
(name) => byName.get(name)?.source.kind === "backend"
|
|
1117
2963
|
);
|
|
1118
|
-
return backendFields.length ?
|
|
2964
|
+
return backendFields.length ? metadataTransport.fetchFieldStats(backendFields, signal) : {};
|
|
1119
2965
|
}
|
|
1120
2966
|
if (!clientRows) return {};
|
|
1121
2967
|
const result = {};
|
|
@@ -1125,21 +2971,21 @@ function useQueryTable(opts) {
|
|
|
1125
2971
|
if (!field) continue;
|
|
1126
2972
|
const distinct = /* @__PURE__ */ new Set();
|
|
1127
2973
|
for (const row of clientRows) {
|
|
1128
|
-
const value =
|
|
2974
|
+
const value = readFieldValue4(field, row);
|
|
1129
2975
|
if (value !== null) distinct.add(JSON.stringify(value));
|
|
1130
2976
|
}
|
|
1131
2977
|
result[name] = { distinct: distinct.size };
|
|
1132
2978
|
}
|
|
1133
2979
|
return result;
|
|
1134
2980
|
},
|
|
1135
|
-
[
|
|
2981
|
+
[metadataTransport, byName, clientRows, groupKey]
|
|
1136
2982
|
);
|
|
1137
|
-
const refresh =
|
|
2983
|
+
const refresh = useCallback12(() => {
|
|
1138
2984
|
setNonce((n) => n + 1);
|
|
1139
2985
|
onRefreshRef.current?.();
|
|
1140
2986
|
}, []);
|
|
1141
|
-
const stopAutoRefresh =
|
|
1142
|
-
const startAutoRefresh =
|
|
2987
|
+
const stopAutoRefresh = useCallback12(() => setAutoRefreshStatus(null), []);
|
|
2988
|
+
const startAutoRefresh = useCallback12(
|
|
1143
2989
|
({ frequencyMs, turnOffAfterMs }) => {
|
|
1144
2990
|
const pollCount = frequencyMs > 0 && turnOffAfterMs > 0 ? Math.floor(turnOffAfterMs / frequencyMs) : 0;
|
|
1145
2991
|
if (pollCount < 1 || pollCount > MAX_AUTO_REFRESH_POLLS) {
|
|
@@ -1159,11 +3005,13 @@ function useQueryTable(opts) {
|
|
|
1159
3005
|
);
|
|
1160
3006
|
const autoRefreshFrequencyMs = autoRefreshStatus?.frequencyMs ?? null;
|
|
1161
3007
|
const autoRefreshStopsAt = autoRefreshStatus?.stopsAt ?? null;
|
|
1162
|
-
|
|
3008
|
+
useEffect10(() => {
|
|
1163
3009
|
if (autoRefreshFrequencyMs == null || autoRefreshStopsAt == null) return;
|
|
1164
3010
|
const tick = () => {
|
|
1165
3011
|
if (nowRef.current() >= autoRefreshStopsAt) {
|
|
1166
|
-
setAutoRefreshStatus(
|
|
3012
|
+
setAutoRefreshStatus(
|
|
3013
|
+
(current) => current?.stopsAt === autoRefreshStopsAt ? null : current
|
|
3014
|
+
);
|
|
1167
3015
|
return;
|
|
1168
3016
|
}
|
|
1169
3017
|
refresh();
|
|
@@ -1175,7 +3023,9 @@ function useQueryTable(opts) {
|
|
|
1175
3023
|
};
|
|
1176
3024
|
const interval = setInterval(tick, autoRefreshFrequencyMs);
|
|
1177
3025
|
const timeout = setTimeout(
|
|
1178
|
-
() => setAutoRefreshStatus(
|
|
3026
|
+
() => setAutoRefreshStatus(
|
|
3027
|
+
(current) => current?.stopsAt === autoRefreshStopsAt ? null : current
|
|
3028
|
+
),
|
|
1179
3029
|
Math.max(0, autoRefreshStopsAt - nowRef.current())
|
|
1180
3030
|
);
|
|
1181
3031
|
return () => {
|
|
@@ -1183,7 +3033,7 @@ function useQueryTable(opts) {
|
|
|
1183
3033
|
clearTimeout(timeout);
|
|
1184
3034
|
};
|
|
1185
3035
|
}, [autoRefreshFrequencyMs, autoRefreshStopsAt, refresh]);
|
|
1186
|
-
const autoRefresh =
|
|
3036
|
+
const autoRefresh = useMemo9(
|
|
1187
3037
|
() => ({
|
|
1188
3038
|
status: autoRefreshStatus,
|
|
1189
3039
|
start: startAutoRefresh,
|
|
@@ -1191,16 +3041,25 @@ function useQueryTable(opts) {
|
|
|
1191
3041
|
}),
|
|
1192
3042
|
[autoRefreshStatus, startAutoRefresh, stopAutoRefresh]
|
|
1193
3043
|
);
|
|
1194
|
-
const refreshRow =
|
|
3044
|
+
const refreshRow = useCallback12(
|
|
1195
3045
|
async (id) => {
|
|
3046
|
+
if (opts.computedExecution && transport?.fetchRowsV2) {
|
|
3047
|
+
refresh();
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
1196
3050
|
if (!transport?.fetchRow) return;
|
|
1197
3051
|
const updated = await transport.fetchRow(id);
|
|
1198
|
-
setRows(
|
|
3052
|
+
setRows(
|
|
3053
|
+
(prev) => prev.map((r) => rowId(r) === id ? updated ?? r : r)
|
|
3054
|
+
);
|
|
1199
3055
|
},
|
|
1200
|
-
[transport, rowId]
|
|
3056
|
+
[transport, rowId, opts.computedExecution, refresh]
|
|
3057
|
+
);
|
|
3058
|
+
const visibleFields = useMemo9(
|
|
3059
|
+
() => selectedFields2(columnSchema, query),
|
|
3060
|
+
[columnSchema, query]
|
|
1201
3061
|
);
|
|
1202
|
-
const
|
|
1203
|
-
const aggregations = useMemo6(
|
|
3062
|
+
const aggregations = useMemo9(
|
|
1204
3063
|
() => ({
|
|
1205
3064
|
clauses: query.aggregations ?? [],
|
|
1206
3065
|
results: aggState.results,
|
|
@@ -1210,7 +3069,11 @@ function useQueryTable(opts) {
|
|
|
1210
3069
|
update: updateAggregation,
|
|
1211
3070
|
move: moveAggregation,
|
|
1212
3071
|
remove: removeAggregation,
|
|
1213
|
-
clear: clearAggregations
|
|
3072
|
+
clear: clearAggregations,
|
|
3073
|
+
preview: aggState.preview,
|
|
3074
|
+
replace: replaceAggregations,
|
|
3075
|
+
compile: metricCompile,
|
|
3076
|
+
contextKey: `${dataVersion}:${pageContext}:${pageReady}:${JSON.stringify(query.where)}`
|
|
1214
3077
|
}),
|
|
1215
3078
|
[
|
|
1216
3079
|
query.aggregations,
|
|
@@ -1221,17 +3084,53 @@ function useQueryTable(opts) {
|
|
|
1221
3084
|
updateAggregation,
|
|
1222
3085
|
moveAggregation,
|
|
1223
3086
|
removeAggregation,
|
|
1224
|
-
clearAggregations
|
|
3087
|
+
clearAggregations,
|
|
3088
|
+
aggState.preview,
|
|
3089
|
+
replaceAggregations,
|
|
3090
|
+
metricCompile,
|
|
3091
|
+
pageContext,
|
|
3092
|
+
pageReady,
|
|
3093
|
+
dataVersion,
|
|
3094
|
+
query.where
|
|
1225
3095
|
]
|
|
1226
3096
|
);
|
|
1227
|
-
|
|
3097
|
+
const sourceRowId = useCallback12((row) => {
|
|
3098
|
+
const field = sourceSchema.fields.find((f) => f.name === sourceSchema.idField);
|
|
3099
|
+
const value = field ? readFieldValue4(field, row) : row[sourceSchema.idField];
|
|
3100
|
+
return typeof value === "string" || typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3101
|
+
}, [sourceSchema]);
|
|
3102
|
+
const itemDetails = useItemDetails({
|
|
3103
|
+
schema: query.grouping ? sourceSchema : displaySchema,
|
|
3104
|
+
query: sourceQuery,
|
|
3105
|
+
setQuery: setSourceQuery,
|
|
3106
|
+
options: opts.itemDetails,
|
|
3107
|
+
rows: query.grouping ? sourceRows ?? [] : rows,
|
|
3108
|
+
clientRows: sourceRows,
|
|
3109
|
+
rowId: sourceRowId,
|
|
3110
|
+
transport: metadataTransport,
|
|
3111
|
+
refreshKey: nonce,
|
|
3112
|
+
links,
|
|
3113
|
+
queryParameter: parameter,
|
|
3114
|
+
queryReady: storageReady && !restoration.loading && !restoration.error
|
|
3115
|
+
});
|
|
3116
|
+
const api = {
|
|
3117
|
+
restoration: {
|
|
3118
|
+
status: restoration.error ? "error" : !storageReady || restoration.loading ? "resolving" : "ready",
|
|
3119
|
+
ready: active && storageReady && !restoration.loading && !restoration.error,
|
|
3120
|
+
error: restoration.error
|
|
3121
|
+
},
|
|
3122
|
+
restoreLocation,
|
|
3123
|
+
links,
|
|
3124
|
+
schema: columnSchema,
|
|
3125
|
+
...itemDetails && detailsEnabled ? { itemDetails } : {},
|
|
1228
3126
|
query,
|
|
1229
|
-
defaults,
|
|
3127
|
+
defaults: resultDefaults,
|
|
1230
3128
|
setQuery,
|
|
1231
3129
|
rows,
|
|
1232
3130
|
total,
|
|
1233
|
-
loading,
|
|
1234
|
-
error,
|
|
3131
|
+
loading: loading || restoration.loading,
|
|
3132
|
+
error: restoration.error ?? error,
|
|
3133
|
+
requestActivity,
|
|
1235
3134
|
canUndo,
|
|
1236
3135
|
canRedo,
|
|
1237
3136
|
undo,
|
|
@@ -1266,6 +3165,48 @@ function useQueryTable(opts) {
|
|
|
1266
3165
|
visibleFields,
|
|
1267
3166
|
rowId
|
|
1268
3167
|
};
|
|
3168
|
+
const sourceApi = {
|
|
3169
|
+
...sourceFilters,
|
|
3170
|
+
loading,
|
|
3171
|
+
rowId: sourceRowId,
|
|
3172
|
+
query: sourceQuery,
|
|
3173
|
+
setQuery: setSourceQuery,
|
|
3174
|
+
...itemDetails ? { itemDetails } : {},
|
|
3175
|
+
schema: sourceCatalogue,
|
|
3176
|
+
filterValues: async (name, search) => {
|
|
3177
|
+
if (metadataTransport?.fetchDistinctValues) return metadataTransport.fetchDistinctValues({ field: name, search });
|
|
3178
|
+
const field = groupingModel.sourceField(name);
|
|
3179
|
+
return field && sourceRows ? { ...distinctValuesFromRows(field, sourceRows, search), hasNull: fieldHasNullInRows(field, sourceRows) } : { values: [], hasMore: false };
|
|
3180
|
+
},
|
|
3181
|
+
fieldStats: async (names, signal) => {
|
|
3182
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
3183
|
+
const requested = [...new Set(names)];
|
|
3184
|
+
if (metadataTransport?.fetchFieldStats) {
|
|
3185
|
+
const backendFields = requested.filter(
|
|
3186
|
+
(name) => sourceCatalogue.fields.find((field) => field.name === name)?.source.kind === "backend"
|
|
3187
|
+
);
|
|
3188
|
+
return backendFields.length ? metadataTransport.fetchFieldStats(backendFields, signal) : {};
|
|
3189
|
+
}
|
|
3190
|
+
return Object.fromEntries(requested.flatMap((name) => {
|
|
3191
|
+
const field = groupingModel.sourceField(name);
|
|
3192
|
+
return field && sourceRows ? [[name, { distinct: new Set(sourceRows.map((row) => JSON.stringify(readFieldValue4(field, row)))).size }]] : [];
|
|
3193
|
+
}));
|
|
3194
|
+
}
|
|
3195
|
+
};
|
|
3196
|
+
const grouping = useGroupingController(
|
|
3197
|
+
api,
|
|
3198
|
+
sourceApi,
|
|
3199
|
+
groupingModel,
|
|
3200
|
+
prepared?.inputs ?? sourceRows ?? [],
|
|
3201
|
+
groupingSupported,
|
|
3202
|
+
viewRevision.current,
|
|
3203
|
+
{ inputsReady: !remoteGrouping, ...remoteGrouping ? { preview: previewGrouping } : {} }
|
|
3204
|
+
);
|
|
3205
|
+
return {
|
|
3206
|
+
...api,
|
|
3207
|
+
...groupingSupported || query.grouping ? { grouping } : {},
|
|
3208
|
+
select: { ...select, hide: (field) => query.grouping?.keys.includes(field) ? grouping.changeKeys(query.grouping.keys.filter((key) => key !== field)) : select.hide(field) }
|
|
3209
|
+
};
|
|
1269
3210
|
}
|
|
1270
3211
|
function nextOrderBy(existing, field, additive) {
|
|
1271
3212
|
const i = existing.findIndex((s) => s.field === field);
|
|
@@ -1285,11 +3226,67 @@ function nextOrderBy(existing, field, additive) {
|
|
|
1285
3226
|
}
|
|
1286
3227
|
return existing.filter((_, k) => k !== i);
|
|
1287
3228
|
}
|
|
3229
|
+
|
|
3230
|
+
// src/useObjectReferences.ts
|
|
3231
|
+
import { useEffect as useEffect11, useMemo as useMemo10, useState as useState12 } from "react";
|
|
3232
|
+
import {
|
|
3233
|
+
objectReferenceKey
|
|
3234
|
+
} from "@pythia-software/query-table-core";
|
|
3235
|
+
function useObjectReferences(references, adapter) {
|
|
3236
|
+
const fingerprint = JSON.stringify(references.map(objectReferenceKey).sort());
|
|
3237
|
+
const batch = useMemo10(
|
|
3238
|
+
() => [
|
|
3239
|
+
...new Map(
|
|
3240
|
+
references.map((reference) => [
|
|
3241
|
+
objectReferenceKey(reference),
|
|
3242
|
+
reference
|
|
3243
|
+
])
|
|
3244
|
+
).values()
|
|
3245
|
+
],
|
|
3246
|
+
[fingerprint]
|
|
3247
|
+
);
|
|
3248
|
+
const [state, setState] = useState12({
|
|
3249
|
+
fingerprint: "",
|
|
3250
|
+
adapter: void 0,
|
|
3251
|
+
values: /* @__PURE__ */ new Map(),
|
|
3252
|
+
error: null
|
|
3253
|
+
});
|
|
3254
|
+
useEffect11(() => {
|
|
3255
|
+
const controller = new AbortController();
|
|
3256
|
+
if (!adapter || batch.length === 0) return;
|
|
3257
|
+
const allowed = new Set(batch.map(objectReferenceKey));
|
|
3258
|
+
void Promise.resolve().then(() => adapter.resolve(batch, controller.signal)).then((results) => {
|
|
3259
|
+
if (controller.signal.aborted) return;
|
|
3260
|
+
const values = /* @__PURE__ */ new Map();
|
|
3261
|
+
for (const result of results) {
|
|
3262
|
+
const key = objectReferenceKey(result.reference);
|
|
3263
|
+
if (allowed.has(key)) values.set(key, result);
|
|
3264
|
+
}
|
|
3265
|
+
setState({ fingerprint, adapter, values, error: null });
|
|
3266
|
+
}).catch((error) => {
|
|
3267
|
+
if (!controller.signal.aborted)
|
|
3268
|
+
setState({
|
|
3269
|
+
fingerprint,
|
|
3270
|
+
adapter,
|
|
3271
|
+
values: /* @__PURE__ */ new Map(),
|
|
3272
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
3273
|
+
});
|
|
3274
|
+
});
|
|
3275
|
+
return () => controller.abort();
|
|
3276
|
+
}, [batch, adapter]);
|
|
3277
|
+
const current = state.fingerprint === fingerprint && state.adapter === adapter;
|
|
3278
|
+
return {
|
|
3279
|
+
values: current ? state.values : /* @__PURE__ */ new Map(),
|
|
3280
|
+
error: current ? state.error : null
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
1288
3283
|
export {
|
|
1289
3284
|
createFormulaWorker,
|
|
1290
3285
|
evaluateFormulaRows,
|
|
1291
3286
|
useAggregations,
|
|
1292
3287
|
useColumnDrag,
|
|
3288
|
+
useObjectReferences,
|
|
3289
|
+
usePropertyView,
|
|
1293
3290
|
useQueryTable,
|
|
1294
3291
|
useSavedQueries,
|
|
1295
3292
|
useSelect,
|