@malloy-publisher/sdk 0.0.146 → 0.0.148
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 +26 -3
- package/dist/components/RenderedResult/ResultContainer.d.ts +1 -1
- package/dist/components/filter/utils.d.ts +5 -0
- package/dist/hooks/useDimensionalFilterRangeData.d.ts +2 -0
- package/dist/index.cjs.js +65 -63
- package/dist/index.es.js +6028 -6036
- package/package.json +1 -1
- package/src/components/Notebook/Notebook.tsx +1 -1
- package/src/components/Notebook/NotebookCell.tsx +12 -4
- package/src/components/RenderedResult/RenderedResult.tsx +36 -106
- package/src/components/RenderedResult/ResultContainer.tsx +28 -154
- package/src/components/filter/DimensionFilter.tsx +89 -5
- package/src/components/filter/utils.ts +23 -2
- package/src/components/styles.ts +2 -1
- package/src/hooks/useDimensionalFilterRangeData.ts +2 -0
package/package.json
CHANGED
|
@@ -67,6 +67,14 @@ export function NotebookCell({
|
|
|
67
67
|
const IMPORT_MODEL_PATH_REGEX =
|
|
68
68
|
/import\s*(?:\{[^}]*\}\s*from\s*)?['"`]([^'"`]+)['"`]/;
|
|
69
69
|
|
|
70
|
+
// Filter out lines starting with ## from Malloy code
|
|
71
|
+
const filterMalloyCode = (code: string): string => {
|
|
72
|
+
return code
|
|
73
|
+
.split("\n")
|
|
74
|
+
.filter((line) => !line.trimStart().startsWith("##"))
|
|
75
|
+
.join("\n");
|
|
76
|
+
};
|
|
77
|
+
|
|
70
78
|
const hasValidImport =
|
|
71
79
|
!!cell.text &&
|
|
72
80
|
(IMPORT_NAMES_REGEX.test(cell.text) ||
|
|
@@ -124,7 +132,7 @@ export function NotebookCell({
|
|
|
124
132
|
|
|
125
133
|
useEffect(() => {
|
|
126
134
|
if (cell.type === "code")
|
|
127
|
-
highlight(cell.text, "malloy").then((code) => {
|
|
135
|
+
highlight(filterMalloyCode(cell.text), "malloy").then((code) => {
|
|
128
136
|
setHighlightedMalloyCode(code);
|
|
129
137
|
});
|
|
130
138
|
}, [cell]);
|
|
@@ -214,7 +222,7 @@ export function NotebookCell({
|
|
|
214
222
|
sx={{
|
|
215
223
|
flexDirection: "column",
|
|
216
224
|
gap: "8px",
|
|
217
|
-
marginBottom: "
|
|
225
|
+
marginBottom: "2px",
|
|
218
226
|
}}
|
|
219
227
|
>
|
|
220
228
|
{cell.newSources && cell.newSources.length > 0 && (
|
|
@@ -433,8 +441,8 @@ export function NotebookCell({
|
|
|
433
441
|
>
|
|
434
442
|
<ResultContainer
|
|
435
443
|
result={cell.result}
|
|
436
|
-
minHeight={
|
|
437
|
-
maxHeight={
|
|
444
|
+
minHeight={200}
|
|
445
|
+
maxHeight={700}
|
|
438
446
|
maxResultSize={maxResultSize}
|
|
439
447
|
/>
|
|
440
448
|
</Box>
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
Suspense,
|
|
3
|
-
useEffect,
|
|
4
|
-
useLayoutEffect,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
7
|
-
} from "react";
|
|
1
|
+
import React, { Suspense, useLayoutEffect, useRef } from "react";
|
|
8
2
|
|
|
9
3
|
type MalloyRenderElement = HTMLElement & Record<string, unknown>;
|
|
10
4
|
|
|
@@ -45,31 +39,14 @@ const createRenderer = async (onDrill?: (element: unknown) => void) => {
|
|
|
45
39
|
return renderer.createViz();
|
|
46
40
|
};
|
|
47
41
|
|
|
48
|
-
function RenderResultSimple({ result, onDrill }: RenderedResultProps) {
|
|
49
|
-
const ref = useRef<HTMLDivElement>(null);
|
|
50
|
-
|
|
51
|
-
useLayoutEffect(() => {
|
|
52
|
-
if (!ref.current || !result) return;
|
|
53
|
-
const element = ref.current;
|
|
54
|
-
|
|
55
|
-
createRenderer(onDrill).then((viz) => {
|
|
56
|
-
viz.setResult(JSON.parse(result));
|
|
57
|
-
viz.render(element);
|
|
58
|
-
});
|
|
59
|
-
}, [result, onDrill]);
|
|
60
|
-
|
|
61
|
-
return <div ref={ref} style={{ width: "100%", height: "100%" }} />;
|
|
62
|
-
}
|
|
63
42
|
// Inner component that actually renders the visualization
|
|
64
43
|
function RenderedResultInner({
|
|
65
44
|
result,
|
|
66
45
|
height,
|
|
67
|
-
isFillElement,
|
|
68
|
-
onSizeChange,
|
|
69
46
|
onDrill,
|
|
47
|
+
onSizeChange,
|
|
70
48
|
}: RenderedResultProps) {
|
|
71
49
|
const ref = useRef<HTMLDivElement>(null);
|
|
72
|
-
const [isRendered, setIsRendered] = useState(false);
|
|
73
50
|
|
|
74
51
|
// Render the visualization once the component mounts
|
|
75
52
|
useLayoutEffect(() => {
|
|
@@ -83,37 +60,44 @@ function RenderedResultInner({
|
|
|
83
60
|
element.removeChild(element.firstChild);
|
|
84
61
|
}
|
|
85
62
|
|
|
63
|
+
// Set up observer to measure size after render completes
|
|
64
|
+
let observer: MutationObserver | null = null;
|
|
65
|
+
let measureTimeout: NodeJS.Timeout | null = null;
|
|
66
|
+
|
|
67
|
+
const measureRenderedSize = () => {
|
|
68
|
+
if (!isMounted || !element.firstElementChild) return;
|
|
69
|
+
|
|
70
|
+
// It's the grandchild that is the actual visualization.
|
|
71
|
+
const child = element.firstElementChild as HTMLElement;
|
|
72
|
+
const grandchild = child.firstElementChild as HTMLElement;
|
|
73
|
+
if (!grandchild) return;
|
|
74
|
+
const renderedHeight =
|
|
75
|
+
grandchild.scrollHeight || grandchild.offsetHeight || 0;
|
|
76
|
+
|
|
77
|
+
if (renderedHeight > 0 && onSizeChange) {
|
|
78
|
+
onSizeChange(renderedHeight);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
86
82
|
createRenderer(onDrill)
|
|
87
83
|
.then((viz) => {
|
|
88
84
|
if (!isMounted) return;
|
|
89
85
|
|
|
90
|
-
// Set up
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
);
|
|
100
|
-
if (hasContent) {
|
|
101
|
-
observer.disconnect();
|
|
102
|
-
setTimeout(() => {
|
|
103
|
-
if (isMounted) {
|
|
104
|
-
setIsRendered(true);
|
|
105
|
-
}
|
|
106
|
-
}, 50);
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
86
|
+
// Set up mutation observer to detect when rendering is complete
|
|
87
|
+
observer = new MutationObserver(() => {
|
|
88
|
+
// Debounce - wait for mutations to settle
|
|
89
|
+
if (measureTimeout) clearTimeout(measureTimeout);
|
|
90
|
+
measureTimeout = setTimeout(() => {
|
|
91
|
+
measureRenderedSize();
|
|
92
|
+
// Disconnect after measuring to prevent infinite loops
|
|
93
|
+
observer?.disconnect();
|
|
94
|
+
}, 100);
|
|
111
95
|
});
|
|
112
96
|
|
|
113
97
|
observer.observe(element, {
|
|
114
98
|
childList: true,
|
|
115
99
|
subtree: true,
|
|
116
|
-
|
|
100
|
+
attributes: true,
|
|
117
101
|
});
|
|
118
102
|
|
|
119
103
|
try {
|
|
@@ -121,71 +105,21 @@ function RenderedResultInner({
|
|
|
121
105
|
viz.render(element);
|
|
122
106
|
} catch (error) {
|
|
123
107
|
console.error("Error rendering visualization:", error);
|
|
124
|
-
observer
|
|
125
|
-
if (isMounted) {
|
|
126
|
-
setIsRendered(true);
|
|
127
|
-
}
|
|
108
|
+
observer?.disconnect();
|
|
128
109
|
}
|
|
129
110
|
})
|
|
130
111
|
.catch((error) => {
|
|
131
112
|
console.error("Failed to create renderer:", error);
|
|
132
|
-
if (isMounted) {
|
|
133
|
-
setIsRendered(true);
|
|
134
|
-
}
|
|
135
113
|
});
|
|
136
114
|
|
|
137
115
|
return () => {
|
|
138
116
|
isMounted = false;
|
|
139
|
-
};
|
|
140
|
-
}, [result, onDrill]);
|
|
141
|
-
|
|
142
|
-
// Set up size measurement using scrollHeight instead of ResizeObserver
|
|
143
|
-
useEffect(() => {
|
|
144
|
-
if (!ref.current || !isRendered) return;
|
|
145
|
-
const element = ref.current;
|
|
146
|
-
|
|
147
|
-
// Function to measure and report size
|
|
148
|
-
const measureSize = () => {
|
|
149
|
-
if (element) {
|
|
150
|
-
const measuredHeight = element.offsetHeight;
|
|
151
|
-
if (measuredHeight > 0) {
|
|
152
|
-
if (onSizeChange) {
|
|
153
|
-
onSizeChange(measuredHeight);
|
|
154
|
-
}
|
|
155
|
-
} else if (isFillElement && element.firstChild) {
|
|
156
|
-
// HACK- we If there's a child and it's height is 0, then we're in a fill element
|
|
157
|
-
// We use the callback `isFillElement` to notify the parent that we're in a fill element
|
|
158
|
-
// the parent should then set height for this element, otherwise it will have size 0.
|
|
159
|
-
const child = element.firstChild as HTMLElement;
|
|
160
|
-
const childHeight = child.offsetHeight;
|
|
161
|
-
if (childHeight == 0) {
|
|
162
|
-
isFillElement(true);
|
|
163
|
-
} else {
|
|
164
|
-
isFillElement(false);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
// Initial measurement after a brief delay to let content render
|
|
171
|
-
const timeoutId = setTimeout(measureSize, 100);
|
|
172
|
-
|
|
173
|
-
let observer: MutationObserver | null = null;
|
|
174
|
-
// Also measure when the malloy result changes
|
|
175
|
-
observer = new MutationObserver(measureSize);
|
|
176
|
-
observer.observe(element, {
|
|
177
|
-
childList: true,
|
|
178
|
-
subtree: true,
|
|
179
|
-
attributes: true,
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// Cleanup
|
|
183
|
-
return () => {
|
|
184
|
-
clearTimeout(timeoutId);
|
|
185
117
|
observer?.disconnect();
|
|
118
|
+
if (measureTimeout) clearTimeout(measureTimeout);
|
|
186
119
|
};
|
|
187
|
-
}, [
|
|
120
|
+
}, [result, onDrill, onSizeChange]);
|
|
188
121
|
|
|
122
|
+
// Always use fixed height - no measurement, no resizing
|
|
189
123
|
return (
|
|
190
124
|
<div
|
|
191
125
|
ref={ref}
|
|
@@ -234,11 +168,7 @@ export default function RenderedResult(props: RenderedResultProps) {
|
|
|
234
168
|
</div>
|
|
235
169
|
}
|
|
236
170
|
>
|
|
237
|
-
{props
|
|
238
|
-
<RenderedResultInner {...props} />
|
|
239
|
-
) : (
|
|
240
|
-
<RenderResultSimple {...props} />
|
|
241
|
-
)}
|
|
171
|
+
<RenderedResultInner {...props} />
|
|
242
172
|
</Suspense>
|
|
243
173
|
);
|
|
244
174
|
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Box, Button,
|
|
3
|
-
import {
|
|
4
|
-
lazy,
|
|
5
|
-
Suspense,
|
|
6
|
-
useCallback,
|
|
7
|
-
useEffect,
|
|
8
|
-
useRef,
|
|
9
|
-
useState,
|
|
10
|
-
} from "react";
|
|
1
|
+
import { Warning } from "@mui/icons-material";
|
|
2
|
+
import { Box, Button, Typography } from "@mui/material";
|
|
3
|
+
import { lazy, Suspense, useRef, useState } from "react";
|
|
11
4
|
import { Loading } from "../Loading";
|
|
12
5
|
|
|
13
6
|
const RenderedResult = lazy(() => import("../RenderedResult/RenderedResult"));
|
|
@@ -31,62 +24,12 @@ export default function ResultContainer({
|
|
|
31
24
|
result,
|
|
32
25
|
minHeight,
|
|
33
26
|
maxHeight,
|
|
34
|
-
hideToggle = false,
|
|
27
|
+
hideToggle: _hideToggle = false,
|
|
35
28
|
maxResultSize = 0,
|
|
36
29
|
}: ResultContainerProps) {
|
|
37
|
-
const [isExpanded, setIsExpanded] = useState(false);
|
|
38
|
-
const [contentHeight, setContentHeight] = useState<number>(0);
|
|
39
|
-
const [shouldShowToggle, setShouldShowToggle] = useState(false);
|
|
40
|
-
const contentRef = useRef<HTMLDivElement>(null);
|
|
41
30
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
42
|
-
const [
|
|
43
|
-
const [isFillElement, setIsFillElement] = useState(false);
|
|
31
|
+
const [measuredHeight, setMeasuredHeight] = useState(maxHeight);
|
|
44
32
|
const [userAcknowledged, setUserAcknowledged] = useState(false);
|
|
45
|
-
const handleToggle = useCallback(() => {
|
|
46
|
-
const wasExpanded = isExpanded;
|
|
47
|
-
setIsExpanded(!isExpanded);
|
|
48
|
-
|
|
49
|
-
// If we're collapsing (going from expanded to collapsed), scroll to top
|
|
50
|
-
if (wasExpanded && containerRef.current) {
|
|
51
|
-
setTimeout(() => {
|
|
52
|
-
containerRef.current?.scrollIntoView({
|
|
53
|
-
behavior: "smooth",
|
|
54
|
-
block: "start",
|
|
55
|
-
});
|
|
56
|
-
}, 100); // Small delay to allow the collapse animation to start
|
|
57
|
-
}
|
|
58
|
-
}, [isExpanded]);
|
|
59
|
-
|
|
60
|
-
// Handle size changes from RenderedResult
|
|
61
|
-
const handleSizeChange = useCallback((height: number) => {
|
|
62
|
-
setContentHeight(height);
|
|
63
|
-
}, []);
|
|
64
|
-
|
|
65
|
-
// Determine if toggle should be shown based on content height vs container height
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
if (hideToggle) {
|
|
68
|
-
setShouldShowToggle(false);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
if (isFillElement) {
|
|
72
|
-
setShouldShowToggle(true);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
// Only proceed if we have a measured content height
|
|
76
|
-
if (contentHeight === 0) {
|
|
77
|
-
setShouldShowToggle(false);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// The available height should be the minHeight minus the padding
|
|
82
|
-
// We don't subtract toggle button height here since we're deciding whether to show it
|
|
83
|
-
const availableHeight = minHeight - 20; // Estimate padding
|
|
84
|
-
const exceedsHeight = contentHeight > availableHeight;
|
|
85
|
-
if (contentHeight < availableHeight) {
|
|
86
|
-
setExplicitHeight(contentHeight + 20);
|
|
87
|
-
}
|
|
88
|
-
setShouldShowToggle(exceedsHeight);
|
|
89
|
-
}, [contentHeight, isFillElement, minHeight, hideToggle]);
|
|
90
33
|
|
|
91
34
|
if (!result) {
|
|
92
35
|
return null;
|
|
@@ -128,98 +71,29 @@ export default function ResultContainer({
|
|
|
128
71
|
}
|
|
129
72
|
|
|
130
73
|
const loading = <Loading text="Loading..." centered={true} size={32} />;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
? maxHeight - 40
|
|
134
|
-
: minHeight - 40
|
|
135
|
-
: undefined;
|
|
136
|
-
const height = explicitHeight
|
|
137
|
-
? {
|
|
138
|
-
minHeight: `${explicitHeight}px`,
|
|
139
|
-
height: `100%`,
|
|
140
|
-
}
|
|
141
|
-
: { height: `100%` };
|
|
142
|
-
return (
|
|
143
|
-
<>
|
|
144
|
-
<Box
|
|
145
|
-
ref={containerRef}
|
|
146
|
-
sx={{
|
|
147
|
-
position: "relative",
|
|
148
|
-
minHeight: `${minHeight}px`,
|
|
149
|
-
maxHeight: `${isExpanded ? maxHeight : minHeight}px`,
|
|
150
|
-
border: "0px",
|
|
151
|
-
borderRadius: 0,
|
|
152
|
-
overflow: "hidden",
|
|
153
|
-
display: "flex",
|
|
154
|
-
flexDirection: "column",
|
|
155
|
-
...height,
|
|
156
|
-
}}
|
|
157
|
-
>
|
|
158
|
-
{/* Content area */}
|
|
159
|
-
<Box
|
|
160
|
-
ref={contentRef}
|
|
161
|
-
sx={{
|
|
162
|
-
flex: 1,
|
|
163
|
-
overflow: "hidden",
|
|
164
|
-
p: 0,
|
|
165
|
-
// Adjust bottom padding when toggle is shown to prevent content overlap
|
|
166
|
-
pb: shouldShowToggle ? "40px" : 1,
|
|
167
|
-
}}
|
|
168
|
-
>
|
|
169
|
-
{(result && (
|
|
170
|
-
<Suspense fallback={loading}>
|
|
171
|
-
<RenderedResult
|
|
172
|
-
result={result}
|
|
173
|
-
height={renderedHeight}
|
|
174
|
-
isFillElement={(isFill) => {
|
|
175
|
-
setIsFillElement(isFill);
|
|
176
|
-
}}
|
|
177
|
-
onSizeChange={handleSizeChange}
|
|
178
|
-
/>
|
|
179
|
-
</Suspense>
|
|
180
|
-
)) ||
|
|
181
|
-
loading}
|
|
182
|
-
</Box>
|
|
74
|
+
// Fixed height for content - no resizing
|
|
75
|
+
const renderedHeight = Math.min(maxHeight, measuredHeight);
|
|
183
76
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
backgroundColor: "rgba(0, 0, 0, 0.04)",
|
|
206
|
-
},
|
|
207
|
-
}}
|
|
208
|
-
title={
|
|
209
|
-
isExpanded
|
|
210
|
-
? "Collapse to original size"
|
|
211
|
-
: "Expand to full size"
|
|
212
|
-
}
|
|
213
|
-
>
|
|
214
|
-
{isExpanded ? (
|
|
215
|
-
<ExpandLess sx={{ fontSize: 30 }} />
|
|
216
|
-
) : (
|
|
217
|
-
<ExpandMore sx={{ fontSize: 30 }} />
|
|
218
|
-
)}
|
|
219
|
-
</IconButton>
|
|
220
|
-
</Box>
|
|
221
|
-
)}
|
|
222
|
-
</Box>
|
|
223
|
-
</>
|
|
77
|
+
return (
|
|
78
|
+
<Box
|
|
79
|
+
ref={containerRef}
|
|
80
|
+
sx={{
|
|
81
|
+
position: "relative",
|
|
82
|
+
height: `${renderedHeight}px`,
|
|
83
|
+
border: "0px",
|
|
84
|
+
borderRadius: 0,
|
|
85
|
+
overflow: "hidden",
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
{result && (
|
|
89
|
+
<Suspense fallback={loading}>
|
|
90
|
+
<RenderedResult
|
|
91
|
+
result={result}
|
|
92
|
+
height={renderedHeight}
|
|
93
|
+
onSizeChange={setMeasuredHeight}
|
|
94
|
+
/>
|
|
95
|
+
</Suspense>
|
|
96
|
+
)}
|
|
97
|
+
</Box>
|
|
224
98
|
);
|
|
225
99
|
}
|
|
@@ -311,11 +311,49 @@ export function DimensionFilter({
|
|
|
311
311
|
};
|
|
312
312
|
|
|
313
313
|
return (
|
|
314
|
-
<Box
|
|
315
|
-
{
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
314
|
+
<Box
|
|
315
|
+
sx={{
|
|
316
|
+
display: "flex",
|
|
317
|
+
flexDirection: "column",
|
|
318
|
+
gap: 1.5,
|
|
319
|
+
fontSize: "0.75rem", // 25% smaller than default
|
|
320
|
+
"& .MuiInputBase-root": { fontSize: "0.75rem" },
|
|
321
|
+
"& .MuiInputBase-input": { padding: "6px 10px" },
|
|
322
|
+
"& .MuiSelect-select": { padding: "6px 10px !important" },
|
|
323
|
+
"& .MuiAutocomplete-input": { padding: "0 !important" },
|
|
324
|
+
"& .MuiAutocomplete-root .MuiInputBase-root": {
|
|
325
|
+
padding: "5px 10px",
|
|
326
|
+
minHeight: "29.5px",
|
|
327
|
+
maxHeight: "29.5px",
|
|
328
|
+
boxSizing: "border-box",
|
|
329
|
+
overflow: "hidden",
|
|
330
|
+
},
|
|
331
|
+
"& .MuiChip-root": { height: "16px", margin: "0 2px 0 0" },
|
|
332
|
+
"& .MuiChip-label": { fontSize: "0.7rem", padding: "0 6px" },
|
|
333
|
+
"& .MuiChip-deleteIcon": {
|
|
334
|
+
fontSize: "14px",
|
|
335
|
+
margin: "0 2px 0 -4px",
|
|
336
|
+
},
|
|
337
|
+
"& .MuiInputLabel-root": {
|
|
338
|
+
fontSize: "0.75rem",
|
|
339
|
+
transform: "translate(10px, 6px) scale(1)",
|
|
340
|
+
},
|
|
341
|
+
"& .MuiInputLabel-shrink": {
|
|
342
|
+
fontSize: "0.85rem",
|
|
343
|
+
transform: "translate(14px, -9px) scale(0.75)",
|
|
344
|
+
},
|
|
345
|
+
"& .MuiOutlinedInput-notchedOutline legend": {
|
|
346
|
+
fontSize: "0.65rem",
|
|
347
|
+
},
|
|
348
|
+
"& .MuiFormHelperText-root": {
|
|
349
|
+
fontSize: "0.65rem",
|
|
350
|
+
marginTop: "2px",
|
|
351
|
+
},
|
|
352
|
+
"& .MuiSvgIcon-root": { fontSize: "1.25rem" },
|
|
353
|
+
}}
|
|
354
|
+
>
|
|
355
|
+
{/* Dimension Label/Name */}
|
|
356
|
+
<Box sx={{ fontWeight: 600 }}>{spec.label ?? spec.dimensionName}</Box>
|
|
319
357
|
|
|
320
358
|
{/* Match Type Selector */}
|
|
321
359
|
{spec.filterType !== "Boolean" && (
|
|
@@ -326,6 +364,17 @@ export function DimensionFilter({
|
|
|
326
364
|
label="Match Type"
|
|
327
365
|
onChange={handleMatchTypeChange}
|
|
328
366
|
disabled={availableMatchTypes.length === 1}
|
|
367
|
+
MenuProps={{
|
|
368
|
+
PaperProps: {
|
|
369
|
+
sx: {
|
|
370
|
+
"& .MuiMenuItem-root": {
|
|
371
|
+
fontSize: "0.75rem",
|
|
372
|
+
minHeight: "auto",
|
|
373
|
+
padding: "4px 10px",
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
}}
|
|
329
378
|
>
|
|
330
379
|
{availableMatchTypes.map((type) => (
|
|
331
380
|
<MenuItem key={type} value={type}>
|
|
@@ -379,11 +428,23 @@ export function DimensionFilter({
|
|
|
379
428
|
renderInput={(params) => (
|
|
380
429
|
<TextField
|
|
381
430
|
{...params}
|
|
431
|
+
size="small"
|
|
382
432
|
label="Values"
|
|
383
433
|
placeholder="Select values..."
|
|
384
434
|
/>
|
|
385
435
|
)}
|
|
386
436
|
freeSolo={!spec.values || spec.values.length === 0}
|
|
437
|
+
slotProps={{
|
|
438
|
+
paper: {
|
|
439
|
+
sx: {
|
|
440
|
+
"& .MuiAutocomplete-option": {
|
|
441
|
+
fontSize: "0.75rem",
|
|
442
|
+
minHeight: "auto",
|
|
443
|
+
padding: "4px 10px",
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
}}
|
|
387
448
|
/>
|
|
388
449
|
)}
|
|
389
450
|
|
|
@@ -412,6 +473,17 @@ export function DimensionFilter({
|
|
|
412
473
|
else if (val === "false") handleValueChange(false);
|
|
413
474
|
else handleClear();
|
|
414
475
|
}}
|
|
476
|
+
MenuProps={{
|
|
477
|
+
PaperProps: {
|
|
478
|
+
sx: {
|
|
479
|
+
"& .MuiMenuItem-root": {
|
|
480
|
+
fontSize: "0.75rem",
|
|
481
|
+
minHeight: "auto",
|
|
482
|
+
padding: "4px 10px",
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
}}
|
|
415
487
|
>
|
|
416
488
|
<MenuItem value="">
|
|
417
489
|
<em>Blank</em>
|
|
@@ -479,6 +551,7 @@ export function DimensionFilter({
|
|
|
479
551
|
renderInput={(params) => (
|
|
480
552
|
<TextField
|
|
481
553
|
{...params}
|
|
554
|
+
size="small"
|
|
482
555
|
label="Search Values"
|
|
483
556
|
placeholder="Type to search..."
|
|
484
557
|
onFocus={() => setRetrievalFocused(true)}
|
|
@@ -509,6 +582,17 @@ export function DimensionFilter({
|
|
|
509
582
|
)}
|
|
510
583
|
freeSolo
|
|
511
584
|
filterOptions={(x) => x}
|
|
585
|
+
slotProps={{
|
|
586
|
+
paper: {
|
|
587
|
+
sx: {
|
|
588
|
+
"& .MuiAutocomplete-option": {
|
|
589
|
+
fontSize: "0.75rem",
|
|
590
|
+
minHeight: "auto",
|
|
591
|
+
padding: "4px 10px",
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
}}
|
|
512
596
|
/>
|
|
513
597
|
)}
|
|
514
598
|
|
|
@@ -109,6 +109,19 @@ export function parseDimensionFilterAnnotation(
|
|
|
109
109
|
return null;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Parse # label="..." annotation from a dimension annotation string
|
|
114
|
+
* Returns the label value or null if not found
|
|
115
|
+
*/
|
|
116
|
+
export function parseLabelAnnotation(annotation: string): string | null {
|
|
117
|
+
// Match # label="..." pattern (with optional spaces around the equals sign)
|
|
118
|
+
const match = annotation.match(/^#\s*label\s*=\s*"([^"]+)"/);
|
|
119
|
+
if (match) {
|
|
120
|
+
return match[1];
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
112
125
|
/**
|
|
113
126
|
* Parse all source infos from notebook cells and create a map of source_name -> SourceInfo
|
|
114
127
|
* Also returns the model path from the first import statement found
|
|
@@ -206,20 +219,27 @@ export function extractDimensionSpecs(
|
|
|
206
219
|
continue;
|
|
207
220
|
}
|
|
208
221
|
|
|
209
|
-
// Check for #(filter) annotation
|
|
222
|
+
// Check for #(filter) annotation and # label="..." annotation
|
|
210
223
|
let filterType: FilterType = "Star"; // Default
|
|
224
|
+
let label: string | undefined = undefined;
|
|
211
225
|
|
|
212
226
|
// Check annotations on the field (dimension/measure fields have annotations)
|
|
213
227
|
if ("annotations" in field && field.annotations) {
|
|
214
228
|
for (const annotation of field.annotations) {
|
|
215
229
|
// Annotation type has a 'value' property
|
|
216
230
|
if (annotation.value) {
|
|
231
|
+
// Check for filter type annotation
|
|
217
232
|
const filterAnn = parseDimensionFilterAnnotation(
|
|
218
233
|
annotation.value,
|
|
219
234
|
);
|
|
220
235
|
if (filterAnn) {
|
|
221
236
|
filterType = filterAnn.type;
|
|
222
|
-
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check for label annotation
|
|
240
|
+
const labelValue = parseLabelAnnotation(annotation.value);
|
|
241
|
+
if (labelValue) {
|
|
242
|
+
label = labelValue;
|
|
223
243
|
}
|
|
224
244
|
}
|
|
225
245
|
}
|
|
@@ -230,6 +250,7 @@ export function extractDimensionSpecs(
|
|
|
230
250
|
model: modelPath,
|
|
231
251
|
dimensionName: dimension,
|
|
232
252
|
filterType,
|
|
253
|
+
label,
|
|
233
254
|
});
|
|
234
255
|
}
|
|
235
256
|
|
package/src/components/styles.ts
CHANGED
|
@@ -55,7 +55,8 @@ export const CleanNotebookCell = styled("div")({
|
|
|
55
55
|
|
|
56
56
|
export const CleanMetricCard = styled("div")({
|
|
57
57
|
backgroundColor: "#ffffff",
|
|
58
|
-
|
|
58
|
+
paddingTop: "12px",
|
|
59
|
+
paddingBottom: "2px",
|
|
59
60
|
borderRadius: "8px",
|
|
60
61
|
border: "1px solid #f0f0f0",
|
|
61
62
|
boxShadow: "0 1px 3px rgba(0, 0, 0, 0.04)",
|
|
@@ -26,6 +26,8 @@ export interface DimensionSpec {
|
|
|
26
26
|
source: string;
|
|
27
27
|
/** Model path */
|
|
28
28
|
model: string;
|
|
29
|
+
/** Label to display in the UI (derived from # label="..." annotation) */
|
|
30
|
+
label?: string;
|
|
29
31
|
/** Minimum similarity score for Retrieval filter type (default: 0.1) */
|
|
30
32
|
minSimilarityScore?: number;
|
|
31
33
|
/** Optional list of static values to use for the dropdown instead of querying */
|