@malloy-publisher/sdk 0.0.145 → 0.0.147
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 +6019 -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 +81 -6
- 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
|
}
|
|
@@ -187,7 +187,7 @@ export function DimensionFilter({
|
|
|
187
187
|
setRetrievalSearched(true);
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
-
},
|
|
190
|
+
}, 500);
|
|
191
191
|
|
|
192
192
|
// Cleanup: cancel timer on unmount or when dependencies change
|
|
193
193
|
return () => {
|
|
@@ -311,11 +311,41 @@ 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: "3px 6px !important" },
|
|
324
|
+
"& .MuiAutocomplete-root .MuiInputBase-root": {
|
|
325
|
+
padding: "3px 6px",
|
|
326
|
+
},
|
|
327
|
+
"& .MuiChip-root": { height: "20px" },
|
|
328
|
+
"& .MuiChip-label": { fontSize: "0.7rem", padding: "0 6px" },
|
|
329
|
+
"& .MuiInputLabel-root": {
|
|
330
|
+
fontSize: "0.75rem",
|
|
331
|
+
transform: "translate(10px, 6px) scale(1)",
|
|
332
|
+
},
|
|
333
|
+
"& .MuiInputLabel-shrink": {
|
|
334
|
+
fontSize: "0.85rem",
|
|
335
|
+
transform: "translate(14px, -9px) scale(0.75)",
|
|
336
|
+
},
|
|
337
|
+
"& .MuiOutlinedInput-notchedOutline legend": {
|
|
338
|
+
fontSize: "0.65rem",
|
|
339
|
+
},
|
|
340
|
+
"& .MuiFormHelperText-root": {
|
|
341
|
+
fontSize: "0.65rem",
|
|
342
|
+
marginTop: "2px",
|
|
343
|
+
},
|
|
344
|
+
"& .MuiSvgIcon-root": { fontSize: "1.25rem" },
|
|
345
|
+
}}
|
|
346
|
+
>
|
|
347
|
+
{/* Dimension Label/Name */}
|
|
348
|
+
<Box sx={{ fontWeight: 600 }}>{spec.label ?? spec.dimensionName}</Box>
|
|
319
349
|
|
|
320
350
|
{/* Match Type Selector */}
|
|
321
351
|
{spec.filterType !== "Boolean" && (
|
|
@@ -325,6 +355,18 @@ export function DimensionFilter({
|
|
|
325
355
|
value={matchType}
|
|
326
356
|
label="Match Type"
|
|
327
357
|
onChange={handleMatchTypeChange}
|
|
358
|
+
disabled={availableMatchTypes.length === 1}
|
|
359
|
+
MenuProps={{
|
|
360
|
+
PaperProps: {
|
|
361
|
+
sx: {
|
|
362
|
+
"& .MuiMenuItem-root": {
|
|
363
|
+
fontSize: "0.75rem",
|
|
364
|
+
minHeight: "auto",
|
|
365
|
+
padding: "4px 10px",
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
}}
|
|
328
370
|
>
|
|
329
371
|
{availableMatchTypes.map((type) => (
|
|
330
372
|
<MenuItem key={type} value={type}>
|
|
@@ -383,6 +425,17 @@ export function DimensionFilter({
|
|
|
383
425
|
/>
|
|
384
426
|
)}
|
|
385
427
|
freeSolo={!spec.values || spec.values.length === 0}
|
|
428
|
+
slotProps={{
|
|
429
|
+
paper: {
|
|
430
|
+
sx: {
|
|
431
|
+
"& .MuiAutocomplete-option": {
|
|
432
|
+
fontSize: "0.75rem",
|
|
433
|
+
minHeight: "auto",
|
|
434
|
+
padding: "4px 10px",
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
}}
|
|
386
439
|
/>
|
|
387
440
|
)}
|
|
388
441
|
|
|
@@ -411,6 +464,17 @@ export function DimensionFilter({
|
|
|
411
464
|
else if (val === "false") handleValueChange(false);
|
|
412
465
|
else handleClear();
|
|
413
466
|
}}
|
|
467
|
+
MenuProps={{
|
|
468
|
+
PaperProps: {
|
|
469
|
+
sx: {
|
|
470
|
+
"& .MuiMenuItem-root": {
|
|
471
|
+
fontSize: "0.75rem",
|
|
472
|
+
minHeight: "auto",
|
|
473
|
+
padding: "4px 10px",
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
}}
|
|
414
478
|
>
|
|
415
479
|
<MenuItem value="">
|
|
416
480
|
<em>Blank</em>
|
|
@@ -508,6 +572,17 @@ export function DimensionFilter({
|
|
|
508
572
|
)}
|
|
509
573
|
freeSolo
|
|
510
574
|
filterOptions={(x) => x}
|
|
575
|
+
slotProps={{
|
|
576
|
+
paper: {
|
|
577
|
+
sx: {
|
|
578
|
+
"& .MuiAutocomplete-option": {
|
|
579
|
+
fontSize: "0.75rem",
|
|
580
|
+
minHeight: "auto",
|
|
581
|
+
padding: "4px 10px",
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
}}
|
|
511
586
|
/>
|
|
512
587
|
)}
|
|
513
588
|
|
|
@@ -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 */
|