@malloy-publisher/sdk 0.0.153 → 0.0.155
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/filter/utils.d.ts +1 -1
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/useDimensionFilters.d.ts +7 -5
- package/dist/hooks/useDimensionFiltersFromSpec.d.ts +1 -1
- package/dist/hooks/useDimensionalFilterRangeData.d.ts +10 -1
- package/dist/index.cjs.js +81 -81
- package/dist/index.es.js +6854 -6839
- package/package.json +1 -1
- package/src/components/Notebook/Notebook.tsx +19 -22
- package/src/components/filter/DimensionFilter.tsx +2 -0
- package/src/components/filter/utils.ts +8 -4
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useDimensionFilters.ts +24 -20
- package/src/hooks/useDimensionalFilterRangeData.ts +35 -18
package/package.json
CHANGED
|
@@ -3,7 +3,10 @@ import * as Malloy from "@malloydata/malloy-interfaces";
|
|
|
3
3
|
import { Box, Paper, Stack, Typography } from "@mui/material";
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { RawNotebook } from "../../client";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getDimensionKey,
|
|
8
|
+
useDimensionalFilterRangeData,
|
|
9
|
+
} from "../../hooks/useDimensionalFilterRangeData";
|
|
7
10
|
import {
|
|
8
11
|
FilterSelection,
|
|
9
12
|
useDimensionFilters,
|
|
@@ -121,11 +124,13 @@ export default function Notebook({
|
|
|
121
124
|
[filterStates, getActiveFilters],
|
|
122
125
|
);
|
|
123
126
|
|
|
124
|
-
// Create a map of dimension
|
|
127
|
+
// Create a map of dimension key -> source name for quick lookup
|
|
128
|
+
// Using composite keys (source:dimensionName) to avoid collisions
|
|
125
129
|
const dimensionToSourceMap = useMemo(() => {
|
|
126
130
|
const map = new Map<string, string>();
|
|
127
131
|
for (const spec of dimensionSpecs) {
|
|
128
|
-
|
|
132
|
+
const key = getDimensionKey(spec);
|
|
133
|
+
map.set(key, spec.source);
|
|
129
134
|
}
|
|
130
135
|
return map;
|
|
131
136
|
}, [dimensionSpecs]);
|
|
@@ -212,16 +217,12 @@ export default function Notebook({
|
|
|
212
217
|
new Set<string>();
|
|
213
218
|
|
|
214
219
|
// Filter to only include those matching this query's source or joined sources
|
|
220
|
+
// FilterSelection now includes source, so we can check directly
|
|
215
221
|
const filtersForSource = querySourceName
|
|
216
222
|
? filtersToApply.filter((filter) => {
|
|
217
|
-
const filterSourceName =
|
|
218
|
-
dimensionToSourceMap.get(
|
|
219
|
-
filter.dimensionName,
|
|
220
|
-
);
|
|
221
|
-
if (!filterSourceName) return false;
|
|
222
223
|
return (
|
|
223
|
-
|
|
224
|
-
joinedSources.has(
|
|
224
|
+
filter.source === querySourceName ||
|
|
225
|
+
joinedSources.has(filter.source)
|
|
225
226
|
);
|
|
226
227
|
})
|
|
227
228
|
: [];
|
|
@@ -374,10 +375,10 @@ export default function Notebook({
|
|
|
374
375
|
}
|
|
375
376
|
}, [activeFilters, isExecuting, executeCells]);
|
|
376
377
|
|
|
377
|
-
// Handle filter change
|
|
378
|
+
// Handle filter change using composite key
|
|
378
379
|
const handleFilterChange = useCallback(
|
|
379
|
-
(
|
|
380
|
-
updateFilter(
|
|
380
|
+
(key: string) => (selection: FilterSelection | null) => {
|
|
381
|
+
updateFilter(key, selection);
|
|
381
382
|
},
|
|
382
383
|
[updateFilter],
|
|
383
384
|
);
|
|
@@ -427,11 +428,9 @@ export default function Notebook({
|
|
|
427
428
|
}}
|
|
428
429
|
>
|
|
429
430
|
{dimensionSpecs.map((spec) => {
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
const filterState = filterStates.get(
|
|
433
|
-
spec.dimensionName,
|
|
434
|
-
);
|
|
431
|
+
const key = getDimensionKey(spec);
|
|
432
|
+
const values = filterValuesData.get(key) || [];
|
|
433
|
+
const filterState = filterStates.get(key);
|
|
435
434
|
// Skip Retrieval filters if no retrievalFn provided
|
|
436
435
|
if (
|
|
437
436
|
spec.filterType === "Retrieval" &&
|
|
@@ -441,14 +440,12 @@ export default function Notebook({
|
|
|
441
440
|
}
|
|
442
441
|
|
|
443
442
|
return (
|
|
444
|
-
<Box key={
|
|
443
|
+
<Box key={key}>
|
|
445
444
|
<DimensionFilter
|
|
446
445
|
spec={spec}
|
|
447
446
|
values={values}
|
|
448
447
|
selection={filterState?.selection}
|
|
449
|
-
onChange={handleFilterChange(
|
|
450
|
-
spec.dimensionName,
|
|
451
|
-
)}
|
|
448
|
+
onChange={handleFilterChange(key)}
|
|
452
449
|
retrievalFn={retrievalFn}
|
|
453
450
|
/>
|
|
454
451
|
</Box>
|
|
@@ -265,6 +265,7 @@ export function DimensionFilter({
|
|
|
265
265
|
if (value1) {
|
|
266
266
|
onChange({
|
|
267
267
|
dimensionName: spec.dimensionName,
|
|
268
|
+
source: spec.source,
|
|
268
269
|
matchType: newMatchType,
|
|
269
270
|
value: value1,
|
|
270
271
|
...(requiresTwoValues(newMatchType) && value2 && { value2 }),
|
|
@@ -294,6 +295,7 @@ export function DimensionFilter({
|
|
|
294
295
|
) {
|
|
295
296
|
onChange({
|
|
296
297
|
dimensionName: spec.dimensionName,
|
|
298
|
+
source: spec.source,
|
|
297
299
|
matchType,
|
|
298
300
|
value: newValue1,
|
|
299
301
|
...(needsTwoValues && newValue2 && { value2: newValue2 }),
|
|
@@ -263,16 +263,20 @@ export function extractDimensionSpecs(
|
|
|
263
263
|
*/
|
|
264
264
|
export function generateFilterClause(
|
|
265
265
|
activeFilters: FilterSelection[],
|
|
266
|
-
|
|
266
|
+
_dimensionToSourceMap: Map<string, string>,
|
|
267
267
|
querySourceName: string | null,
|
|
268
268
|
): string {
|
|
269
269
|
if (activeFilters.length === 0) return "";
|
|
270
270
|
|
|
271
271
|
const conditions = activeFilters
|
|
272
272
|
.map((selection) => {
|
|
273
|
-
const {
|
|
274
|
-
|
|
275
|
-
|
|
273
|
+
const {
|
|
274
|
+
dimensionName,
|
|
275
|
+
source: filterSourceName,
|
|
276
|
+
matchType,
|
|
277
|
+
value,
|
|
278
|
+
value2,
|
|
279
|
+
} = selection;
|
|
276
280
|
// Only use source.dimension format if the filter's source is different from the query's source
|
|
277
281
|
// (i.e., it's a joined source, not the main source)
|
|
278
282
|
const needsSourcePrefix =
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DimensionSpec,
|
|
4
|
+
getDimensionKey,
|
|
5
|
+
} from "./useDimensionalFilterRangeData";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Match types for filtering dimensions
|
|
@@ -29,6 +32,8 @@ export type FilterValue = FilterValuePrimitive | FilterValuePrimitive[];
|
|
|
29
32
|
*/
|
|
30
33
|
export interface FilterSelection {
|
|
31
34
|
dimensionName: string;
|
|
35
|
+
/** Source name - required to uniquely identify filters when same dimension name exists in multiple sources */
|
|
36
|
+
source: string;
|
|
32
37
|
matchType: MatchType;
|
|
33
38
|
value: FilterValue;
|
|
34
39
|
value2?: FilterValuePrimitive; // For "Between" match type
|
|
@@ -54,15 +59,12 @@ export interface UseDimensionFiltersParams {
|
|
|
54
59
|
* Result from the useDimensionFilters hook
|
|
55
60
|
*/
|
|
56
61
|
export interface UseDimensionFiltersResult {
|
|
57
|
-
/** Current filter states */
|
|
62
|
+
/** Current filter states, keyed by composite key (source:dimensionName) */
|
|
58
63
|
filterStates: Map<string, DimensionFilterState>;
|
|
59
|
-
/** Update a filter selection */
|
|
60
|
-
updateFilter: (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
) => void;
|
|
64
|
-
/** Clear a specific filter */
|
|
65
|
-
clearFilter: (dimensionName: string) => void;
|
|
64
|
+
/** Update a filter selection using composite key */
|
|
65
|
+
updateFilter: (key: string, selection: FilterSelection | null) => void;
|
|
66
|
+
/** Clear a specific filter using composite key */
|
|
67
|
+
clearFilter: (key: string) => void;
|
|
66
68
|
/** Clear all filters */
|
|
67
69
|
clearAllFilters: () => void;
|
|
68
70
|
/** Get active filters (with selections) */
|
|
@@ -207,13 +209,14 @@ export function useDimensionFilters(
|
|
|
207
209
|
): UseDimensionFiltersResult {
|
|
208
210
|
const { dimensionSpecs } = params;
|
|
209
211
|
|
|
210
|
-
// Initialize filter states
|
|
212
|
+
// Initialize filter states using composite keys (source:dimensionName)
|
|
211
213
|
const [filterStates, setFilterStates] = useState<
|
|
212
214
|
Map<string, DimensionFilterState>
|
|
213
215
|
>(() => {
|
|
214
216
|
const initialStates = new Map<string, DimensionFilterState>();
|
|
215
217
|
dimensionSpecs.forEach((spec) => {
|
|
216
|
-
|
|
218
|
+
const key = getDimensionKey(spec);
|
|
219
|
+
initialStates.set(key, {
|
|
217
220
|
spec,
|
|
218
221
|
selection: null,
|
|
219
222
|
});
|
|
@@ -227,9 +230,10 @@ export function useDimensionFilters(
|
|
|
227
230
|
const newStates = new Map<string, DimensionFilterState>();
|
|
228
231
|
|
|
229
232
|
dimensionSpecs.forEach((spec) => {
|
|
233
|
+
const key = getDimensionKey(spec);
|
|
230
234
|
// Preserve existing selection if the dimension already exists
|
|
231
|
-
const existingState = prevStates.get(
|
|
232
|
-
newStates.set(
|
|
235
|
+
const existingState = prevStates.get(key);
|
|
236
|
+
newStates.set(key, {
|
|
233
237
|
spec,
|
|
234
238
|
selection: existingState?.selection ?? null,
|
|
235
239
|
});
|
|
@@ -239,15 +243,15 @@ export function useDimensionFilters(
|
|
|
239
243
|
});
|
|
240
244
|
}, [dimensionSpecs]);
|
|
241
245
|
|
|
242
|
-
// Update a filter selection
|
|
246
|
+
// Update a filter selection using composite key
|
|
243
247
|
const updateFilter = useCallback(
|
|
244
|
-
(
|
|
248
|
+
(key: string, selection: FilterSelection | null) => {
|
|
245
249
|
setFilterStates((prevStates) => {
|
|
246
250
|
const newStates = new Map(prevStates);
|
|
247
|
-
const existingState = newStates.get(
|
|
251
|
+
const existingState = newStates.get(key);
|
|
248
252
|
|
|
249
253
|
if (existingState) {
|
|
250
|
-
newStates.set(
|
|
254
|
+
newStates.set(key, {
|
|
251
255
|
...existingState,
|
|
252
256
|
selection,
|
|
253
257
|
});
|
|
@@ -259,10 +263,10 @@ export function useDimensionFilters(
|
|
|
259
263
|
[],
|
|
260
264
|
);
|
|
261
265
|
|
|
262
|
-
// Clear a specific filter
|
|
266
|
+
// Clear a specific filter using composite key
|
|
263
267
|
const clearFilter = useCallback(
|
|
264
|
-
(
|
|
265
|
-
updateFilter(
|
|
268
|
+
(key: string) => {
|
|
269
|
+
updateFilter(key, null);
|
|
266
270
|
},
|
|
267
271
|
[updateFilter],
|
|
268
272
|
);
|
|
@@ -34,6 +34,24 @@ export interface DimensionSpec {
|
|
|
34
34
|
values?: string[];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Generates a unique key from source and dimension name strings.
|
|
39
|
+
* This prevents collisions when the same dimension name exists in different sources.
|
|
40
|
+
*/
|
|
41
|
+
export function makeDimensionKey(
|
|
42
|
+
source: string,
|
|
43
|
+
dimensionName: string,
|
|
44
|
+
): string {
|
|
45
|
+
return `${source}:${dimensionName}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Generates a unique key for a dimension by combining source and dimension name.
|
|
50
|
+
*/
|
|
51
|
+
export function getDimensionKey(spec: DimensionSpec): string {
|
|
52
|
+
return makeDimensionKey(spec.source, spec.dimensionName);
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
/**
|
|
38
56
|
* Value information for a dimension
|
|
39
57
|
*/
|
|
@@ -43,7 +61,7 @@ export interface DimensionValue {
|
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
/**
|
|
46
|
-
* Result type mapping dimension
|
|
64
|
+
* Result type mapping dimension keys (source:dimensionName) to their values
|
|
47
65
|
*/
|
|
48
66
|
export type DimensionValues = Map<string, DimensionValue[]>;
|
|
49
67
|
|
|
@@ -236,13 +254,7 @@ function buildDimensionalIndexQuery(
|
|
|
236
254
|
|
|
237
255
|
// Filter activeFilters to only include those for this source
|
|
238
256
|
const filtersForSource =
|
|
239
|
-
activeFilters?.filter((f) =>
|
|
240
|
-
// Find the spec for this filter's dimension
|
|
241
|
-
const spec = dimensionSpecs.find(
|
|
242
|
-
(s) => s.dimensionName === f.dimensionName,
|
|
243
|
-
);
|
|
244
|
-
return spec?.source === source;
|
|
245
|
-
}) || [];
|
|
257
|
+
activeFilters?.filter((f) => f.source === source) || [];
|
|
246
258
|
|
|
247
259
|
// Generate WHERE conditions from active filters (without 'where' keyword)
|
|
248
260
|
const whereConditions =
|
|
@@ -332,9 +344,10 @@ function parseIndexQueryResult(
|
|
|
332
344
|
): { values: DimensionValues; noRowsMatchedFilter: boolean } {
|
|
333
345
|
const dimensionValues = new Map<string, DimensionValue[]>();
|
|
334
346
|
|
|
335
|
-
// Initialize empty arrays for all dimensions
|
|
347
|
+
// Initialize empty arrays for all dimensions using composite keys
|
|
336
348
|
for (const spec of dimensionSpecs) {
|
|
337
|
-
|
|
349
|
+
const key = getDimensionKey(spec);
|
|
350
|
+
dimensionValues.set(key, []);
|
|
338
351
|
}
|
|
339
352
|
|
|
340
353
|
// Parse the result JSON if it's a string
|
|
@@ -460,6 +473,8 @@ function parseIndexQueryResult(
|
|
|
460
473
|
continue;
|
|
461
474
|
}
|
|
462
475
|
|
|
476
|
+
const specKey = getDimensionKey(spec);
|
|
477
|
+
|
|
463
478
|
if (spec.filterType === "Star") {
|
|
464
479
|
// For Star filter, we want all distinct values with their weights
|
|
465
480
|
// String fields return individual fieldValue entries
|
|
@@ -474,7 +489,7 @@ function parseIndexQueryResult(
|
|
|
474
489
|
(b.count ?? 0) - (a.count ?? 0),
|
|
475
490
|
); // Sort by count descending
|
|
476
491
|
|
|
477
|
-
dimensionValues.set(
|
|
492
|
+
dimensionValues.set(specKey, values);
|
|
478
493
|
} else if (spec.filterType === "MinMax") {
|
|
479
494
|
// For MinMax filter, numeric fields return a range in fieldValue
|
|
480
495
|
// Format is typically "min to max"
|
|
@@ -504,7 +519,7 @@ function parseIndexQueryResult(
|
|
|
504
519
|
{ value: parseFloat(rangeParts[0]) },
|
|
505
520
|
{ value: parseFloat(rangeParts[1]) },
|
|
506
521
|
];
|
|
507
|
-
dimensionValues.set(
|
|
522
|
+
dimensionValues.set(specKey, values);
|
|
508
523
|
} else {
|
|
509
524
|
console.warn(
|
|
510
525
|
`Could not parse numeric range for ${spec.dimensionName}: ${rangeString}`,
|
|
@@ -546,7 +561,7 @@ function parseIndexQueryResult(
|
|
|
546
561
|
{ value: new Date(rangeParts[0].trim()) },
|
|
547
562
|
{ value: new Date(rangeParts[1].trim()) },
|
|
548
563
|
];
|
|
549
|
-
dimensionValues.set(
|
|
564
|
+
dimensionValues.set(specKey, values);
|
|
550
565
|
} else {
|
|
551
566
|
console.warn(
|
|
552
567
|
`Could not parse date range for ${spec.dimensionName}: ${rangeString}`,
|
|
@@ -675,10 +690,11 @@ export function useDimensionalFilterRangeData(
|
|
|
675
690
|
],
|
|
676
691
|
queryFn: async () => {
|
|
677
692
|
if (!shouldExecuteQuery) {
|
|
678
|
-
// Return empty map if no query needed
|
|
693
|
+
// Return empty map if no query needed (using composite keys)
|
|
679
694
|
const emptyMap = new Map<string, DimensionValue[]>();
|
|
680
695
|
for (const spec of dimensionSpecs) {
|
|
681
|
-
|
|
696
|
+
const key = getDimensionKey(spec);
|
|
697
|
+
emptyMap.set(key, []);
|
|
682
698
|
}
|
|
683
699
|
return { values: emptyMap, noRowsMatchedFilter: false };
|
|
684
700
|
}
|
|
@@ -728,15 +744,16 @@ export function useDimensionalFilterRangeData(
|
|
|
728
744
|
const mergedData = useMemo(() => {
|
|
729
745
|
const result = new Map<string, DimensionValue[]>();
|
|
730
746
|
|
|
731
|
-
// Initialize with static values or empty arrays
|
|
747
|
+
// Initialize with static values or empty arrays using composite keys
|
|
732
748
|
for (const spec of dimensionSpecs) {
|
|
749
|
+
const key = getDimensionKey(spec);
|
|
733
750
|
if (spec.values) {
|
|
734
751
|
result.set(
|
|
735
|
-
|
|
752
|
+
key,
|
|
736
753
|
spec.values.map((v) => ({ value: v })),
|
|
737
754
|
);
|
|
738
755
|
} else {
|
|
739
|
-
result.set(
|
|
756
|
+
result.set(key, []);
|
|
740
757
|
}
|
|
741
758
|
}
|
|
742
759
|
|