@malloy-publisher/sdk 0.0.181 → 0.0.183-dev

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.
Files changed (32) hide show
  1. package/dist/ServerProvider-lOQXDlqB.cjs.js +1 -0
  2. package/dist/{ServerProvider-DN2wSIAZ.es.js → ServerProvider-on-8FH5Z.es.js} +1259 -764
  3. package/dist/client/api.d.ts +674 -20
  4. package/dist/client/index.cjs.js +1 -1
  5. package/dist/client/index.es.js +57 -44
  6. package/dist/core-CjeTkq8O.es.js +7515 -0
  7. package/dist/core-YNpOLuB1.cjs.js +148 -0
  8. package/dist/engine-oniguruma-BkproSVE.cjs.js +1 -0
  9. package/dist/engine-oniguruma-C4vnmooL.es.js +272 -0
  10. package/dist/github-light-BFTOhCbz.cjs.js +1 -0
  11. package/dist/github-light-JYsPkUQd.es.js +4 -0
  12. package/dist/hooks/useDimensionFilters.d.ts +2 -0
  13. package/dist/hooks/useDimensionalFilterRangeData.d.ts +19 -5
  14. package/dist/index-BOLBP6_i.cjs.js +233 -0
  15. package/dist/index-DRDu9kIV.es.js +53627 -0
  16. package/dist/index.cjs.js +1 -244
  17. package/dist/index.es.js +48 -58039
  18. package/dist/json-71t8ZF9g.es.js +6 -0
  19. package/dist/json-y-J1j5EW.cjs.js +1 -0
  20. package/dist/sql-BqWZrLHB.cjs.js +1 -0
  21. package/dist/sql-DCkt643-.es.js +6 -0
  22. package/dist/typescript-BqvpT6pB.cjs.js +1 -0
  23. package/dist/typescript-buWNZFwO.es.js +6 -0
  24. package/package.json +6 -8
  25. package/src/components/Notebook/Notebook.tsx +162 -108
  26. package/src/components/ServerProvider.tsx +8 -0
  27. package/src/components/filter/DimensionFilter.tsx +68 -39
  28. package/src/components/highlighter.ts +54 -32
  29. package/src/hooks/useDimensionFilters.ts +2 -0
  30. package/src/hooks/useDimensionalFilterRangeData.ts +27 -13
  31. package/src/index.ts +0 -5
  32. package/dist/ServerProvider-DSeOLGfV.cjs.js +0 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@malloy-publisher/sdk",
3
3
  "description": "Malloy Publisher SDK",
4
- "version": "0.0.181",
4
+ "version": "0.0.183-dev",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs.js",
7
7
  "module": "dist/index.es.js",
@@ -47,9 +47,7 @@
47
47
  },
48
48
  "style": "./dist/malloy-explorer.css",
49
49
  "sideEffects": [
50
- "**/*.css",
51
- "./src/index.ts",
52
- "./dist/index.*.js"
50
+ "**/*.css"
53
51
  ],
54
52
  "repository": {
55
53
  "type": "git",
@@ -72,10 +70,10 @@
72
70
  "react-dom": ">=19.1.0",
73
71
  "react-router-dom": ">=7.6.2",
74
72
  "@tanstack/react-query": ">=5.59.16",
75
- "@malloydata/malloy-explorer": "^0.0.374",
76
- "@malloydata/malloy-interfaces": "^0.0.374",
77
- "@malloydata/malloy-query-builder": "^0.0.374",
78
- "@malloydata/render": "^0.0.374",
73
+ "@malloydata/malloy-explorer": ">=0.0.338-dev260215172810",
74
+ "@malloydata/malloy-interfaces": ">=0.0.374",
75
+ "@malloydata/malloy-query-builder": ">=0.0.374",
76
+ "@malloydata/render": ">=0.0.374",
79
77
  "@mui/icons-material": ">=7.1.1",
80
78
  "@mui/x-date-pickers": ">=7.1.1",
81
79
  "@mui/material": ">=7.1.1",
@@ -2,7 +2,7 @@ import "@malloydata/malloy-explorer/styles.css";
2
2
  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
- import { RawNotebook } from "../../client";
5
+ import { RawNotebook, Source } from "../../client";
6
6
  import {
7
7
  getDimensionKey,
8
8
  useDimensionalFilterRangeData,
@@ -16,10 +16,6 @@ import { ApiErrorDisplay } from "../ApiErrorDisplay";
16
16
  import { DimensionFilter, RetrievalFunction } from "../filter/DimensionFilter";
17
17
  import {
18
18
  extractDimensionSpecs,
19
- extractSourceFromQuery,
20
- generateFilterClause,
21
- getJoinedSources,
22
- injectWhereClause,
23
19
  parseAllSourceInfos,
24
20
  parseNotebookFilterAnnotation,
25
21
  } from "../filter/utils";
@@ -81,7 +77,7 @@ export default function Notebook({
81
77
  const [isExecuting, setIsExecuting] = useState(false);
82
78
  const [executionError, setExecutionError] = useState<Error | null>(null);
83
79
 
84
- // Parse filter configuration from notebook annotations
80
+ // Parse filter configuration from notebook annotations (legacy ##(filters) approach)
85
81
  const filterConfig = useMemo(() => {
86
82
  if (!notebook) return null;
87
83
  return parseNotebookFilterAnnotation(notebook.annotations);
@@ -99,16 +95,112 @@ export default function Notebook({
99
95
  );
100
96
  const modelPath = sourceData?.modelPath ?? null;
101
97
 
98
+ // Extract server-side filter definitions from notebook sources
99
+ // These come from #(filter) annotations parsed by the server
100
+ const serverFilters = useMemo(() => {
101
+ const result = new Map<string, Source["filters"]>();
102
+ if (!notebook?.sources) return result;
103
+ for (const source of notebook.sources as Source[]) {
104
+ if (source.name && source.filters && source.filters.length > 0) {
105
+ // Exclude implicit filters from the UI
106
+ const visibleFilters = source.filters.filter((f) => !f.implicit);
107
+ if (visibleFilters.length > 0) {
108
+ result.set(source.name, visibleFilters);
109
+ }
110
+ }
111
+ }
112
+ return result;
113
+ }, [notebook]);
114
+
115
+ // Determine if we're using server-driven filters (#(filter)) or legacy (##(filters))
116
+ const useServerFilters = serverFilters.size > 0;
117
+
102
118
  // Build dimension specs from filter config and source info map
103
119
  // Each spec includes source and model for proper query routing
104
120
  const dimensionSpecs = useMemo(() => {
121
+ if (useServerFilters && modelPath) {
122
+ // Server-driven: build specs from #(filter) metadata
123
+ const specs: import("../../hooks/useDimensionalFilterRangeData").DimensionSpec[] =
124
+ [];
125
+ for (const [sourceName, filters] of serverFilters) {
126
+ for (const filter of filters ?? []) {
127
+ if (!filter.dimension || !filter.type) continue;
128
+
129
+ // Choose widget type based on the dimension's data type first,
130
+ // then fall back to the annotation's comparator type
131
+ type FT =
132
+ import("../../hooks/useDimensionalFilterRangeData").FilterType;
133
+ let filterType: FT;
134
+ const dimType = filter.dimensionType;
135
+ if (dimType === "boolean") {
136
+ filterType = "Boolean";
137
+ } else if (
138
+ dimType === "date" ||
139
+ dimType === "timestamp" ||
140
+ dimType === "timestamptz"
141
+ ) {
142
+ filterType = "DateMinMax";
143
+ } else if (dimType === "number") {
144
+ filterType =
145
+ filter.type === "equal" || filter.type === "in"
146
+ ? "Star"
147
+ : "MinMax";
148
+ } else {
149
+ const filterTypeMap: Record<string, FT> = {
150
+ equal: "Star",
151
+ in: "Star",
152
+ like: "Star",
153
+ greater_than: "MinMax",
154
+ less_than: "MinMax",
155
+ };
156
+ filterType = filterTypeMap[filter.type] ?? "Star";
157
+ }
158
+
159
+ // Derive the match type from the #(filter) annotation type so
160
+ // the UI never needs to show a match-type dropdown.
161
+ type MT = import("../../hooks/useDimensionFilters").MatchType;
162
+ const matchTypeMap: Record<string, MT> = {
163
+ equal: "Equals",
164
+ in: "Equals",
165
+ like: "Contains",
166
+ greater_than:
167
+ filterType === "DateMinMax" ? "After" : "Greater Than",
168
+ less_than:
169
+ filterType === "DateMinMax" ? "Before" : "Less Than",
170
+ };
171
+ const defaultMatchType: MT | undefined =
172
+ matchTypeMap[filter.type!];
173
+
174
+ const filterLabel =
175
+ filter.name !== filter.dimension ? filter.name : undefined;
176
+ specs.push({
177
+ source: sourceName,
178
+ model: modelPath,
179
+ dimensionName: filter.dimension!,
180
+ filterType,
181
+ label: filterLabel,
182
+ filterName: filter.name ?? filter.dimension!,
183
+ defaultMatchType,
184
+ required: filter.required ?? false,
185
+ });
186
+ }
187
+ }
188
+ return specs;
189
+ }
190
+ // Legacy: use ##(filters) + #(filter) annotation approach
105
191
  if (!filterConfig || sourceInfoMap.size === 0 || !modelPath) return [];
106
192
  return extractDimensionSpecs(
107
193
  sourceInfoMap,
108
194
  filterConfig.filters,
109
195
  modelPath,
110
196
  );
111
- }, [filterConfig, sourceInfoMap, modelPath]);
197
+ }, [
198
+ useServerFilters,
199
+ serverFilters,
200
+ filterConfig,
201
+ sourceInfoMap,
202
+ modelPath,
203
+ ]);
112
204
 
113
205
  // Initialize dimension filters hook
114
206
  const { filterStates, updateFilter, getActiveFilters } = useDimensionFilters(
@@ -124,9 +216,8 @@ export default function Notebook({
124
216
  [filterStates, getActiveFilters],
125
217
  );
126
218
 
127
- // Create a map of dimension key -> source name for quick lookup
128
- // Using composite keys (source:dimensionName) to avoid collisions
129
- const dimensionToSourceMap = useMemo(() => {
219
+ // Create a map of dimension key -> source name for quick lookup (used by filter UI)
220
+ const _dimensionToSourceMap = useMemo(() => {
130
221
  const map = new Map<string, string>();
131
222
  for (const spec of dimensionSpecs) {
132
223
  const key = getDimensionKey(spec);
@@ -135,28 +226,55 @@ export default function Notebook({
135
226
  return map;
136
227
  }, [dimensionSpecs]);
137
228
 
138
- // Create a map of source name -> set of joined source names
139
- const sourceJoinsMap = useMemo(() => {
140
- const map = new Map<string, Set<string>>();
141
- for (const [sourceName, sourceInfo] of sourceInfoMap) {
142
- map.set(sourceName, getJoinedSources(sourceInfo));
143
- }
144
- return map;
145
- }, [sourceInfoMap]);
146
-
147
- // Fetch filter range data when we have dimension specs
148
- // The hook now handles multiple source/model combos internally
229
+ // Fetch filter range data when we have dimension specs.
230
+ // Do NOT pass activeFilters here — the index query should return all
231
+ // possible values for each dimension, not just those matching the
232
+ // current selection. Otherwise selecting "FORD" would hide every other
233
+ // manufacturer from the dropdown.
149
234
  const { data: filterValuesData } = useDimensionalFilterRangeData({
150
235
  project: projectName,
151
236
  package: packageName,
152
237
  dimensionSpecs,
153
238
  versionId,
154
239
  enabled: dimensionSpecs.length > 0,
155
- activeFilters,
156
240
  });
157
241
 
242
+ /**
243
+ * Convert active FilterSelections into a flat { filterName: value } map
244
+ * suitable for the server's filter_params parameter.
245
+ * Uses filterName from the selection (propagated from the spec) as the
246
+ * API param key, falling back to dimensionName.
247
+ */
248
+ const buildFilterParams = useCallback(
249
+ (filtersToApply: FilterSelection[]): string | undefined => {
250
+ if (filtersToApply.length === 0) return undefined;
251
+
252
+ const toParamString = (v: unknown): string => {
253
+ if (v instanceof Date) {
254
+ return v.toISOString().slice(0, 10);
255
+ }
256
+ return String(v);
257
+ };
258
+
259
+ const params: { [key: string]: string | string[] } = {};
260
+ for (const f of filtersToApply) {
261
+ const paramName = f.filterName ?? f.dimensionName;
262
+ const val = f.value;
263
+ if (Array.isArray(val)) {
264
+ params[paramName] = val.map(toParamString);
265
+ } else if (val !== undefined && val !== null) {
266
+ params[paramName] = toParamString(val);
267
+ }
268
+ }
269
+ return Object.keys(params).length > 0
270
+ ? JSON.stringify(params)
271
+ : undefined;
272
+ },
273
+ [],
274
+ );
275
+
158
276
  // Unified cell execution function
159
- // Executes all notebook cells, optionally applying filters to query cells
277
+ // Executes all notebook cells, passing server-side filter params when available
160
278
  // Runs up to 4 requests in parallel for better performance
161
279
  const executeCells = useCallback(
162
280
  async (filtersToApply: FilterSelection[] = []) => {
@@ -176,6 +294,10 @@ export default function Notebook({
176
294
  setIsExecuting(true);
177
295
  setExecutionError(null);
178
296
 
297
+ const filterParams = useServerFilters
298
+ ? buildFilterParams(filtersToApply)
299
+ : undefined;
300
+
179
301
  try {
180
302
  // Build execution tasks for code cells
181
303
  const executionTasks: Array<() => Promise<void>> = [];
@@ -186,95 +308,30 @@ export default function Notebook({
186
308
  // Markdown cells don't need execution
187
309
  if (rawCell.type === "markdown") continue;
188
310
 
189
- // Execute code cells
190
- const cellText = rawCell.text || "";
191
- const hasQuery =
192
- cellText.includes("run:") ||
193
- cellText.includes("->") ||
194
- /^\s*(run|query)\s*:/m.test(cellText);
195
-
196
311
  // Capture cell index for closure
197
312
  const cellIndex = i;
198
313
 
199
314
  const executeCell = async () => {
200
315
  try {
201
- let result: string | undefined;
202
- let newSources: string[] | undefined;
203
-
204
- if (hasQuery && modelPath && filtersToApply.length > 0) {
205
- // Query cell - use models API with optional filters
206
- let queryToExecute = cellText;
207
-
208
- // Apply filters if any match this query's source
209
- if (filtersToApply.length > 0) {
210
- const querySourceName =
211
- extractSourceFromQuery(cellText);
212
-
213
- // Get the set of joined sources for this query's source
214
- const joinedSources =
215
- (querySourceName &&
216
- sourceJoinsMap.get(querySourceName)) ||
217
- new Set<string>();
218
-
219
- // Filter to only include those matching this query's source or joined sources
220
- // FilterSelection now includes source, so we can check directly
221
- const filtersForSource = querySourceName
222
- ? filtersToApply.filter((filter) => {
223
- return (
224
- filter.source === querySourceName ||
225
- joinedSources.has(filter.source)
226
- );
227
- })
228
- : [];
229
-
230
- if (filtersForSource.length > 0) {
231
- const filterClause = generateFilterClause(
232
- filtersForSource,
233
- dimensionToSourceMap,
234
- querySourceName,
235
- );
236
- if (filterClause) {
237
- queryToExecute = injectWhereClause(
238
- cellText,
239
- filterClause,
240
- );
241
- }
242
- }
243
- }
244
-
245
- // Execute using models API
246
- const response =
247
- await apiClients.models.executeQueryModel(
248
- projectName,
249
- packageName,
250
- modelPath,
251
- {
252
- query: queryToExecute,
253
- versionId,
254
- },
255
- );
256
- result = response.data.result;
257
- } else {
258
- // Non-query code cell (or no filters applied) - use notebook cell execution API
259
- const response =
260
- await apiClients.notebooks.executeNotebookCell(
261
- projectName,
262
- packageName,
263
- notebookPath,
264
- cellIndex,
265
- versionId,
266
- );
267
-
268
- const executedCell = response.data;
269
- result = executedCell.result;
270
- newSources =
271
- rawCell.newSources || executedCell.newSources;
272
- }
316
+ // Use notebook cell execution API with optional filter_params
317
+ const response =
318
+ await apiClients.notebooks.executeNotebookCell(
319
+ projectName,
320
+ packageName,
321
+ notebookPath,
322
+ cellIndex,
323
+ versionId,
324
+ filterParams,
325
+ );
326
+
327
+ const executedCell = response.data;
328
+ const result = executedCell.result;
329
+ const newSources =
330
+ rawCell.newSources || executedCell.newSources;
273
331
 
274
332
  // Update state incrementally
275
333
  setEnhancedCells((prev) => {
276
334
  const next = [...prev];
277
- // Ensure we have a cell to update (in case state was reset externally, though unlikely)
278
335
  if (!next[cellIndex]) {
279
336
  next[cellIndex] = { ...rawCell };
280
337
  }
@@ -290,7 +347,6 @@ export default function Notebook({
290
347
  `Error executing cell ${cellIndex}:`,
291
348
  cellError,
292
349
  );
293
- // Don't update result on error, leave as is (undefined)
294
350
  }
295
351
  };
296
352
 
@@ -323,14 +379,12 @@ export default function Notebook({
323
379
  [
324
380
  isSuccess,
325
381
  notebook,
326
- modelPath,
327
- dimensionToSourceMap,
328
- sourceJoinsMap,
382
+ useServerFilters,
383
+ buildFilterParams,
329
384
  projectName,
330
385
  packageName,
331
386
  notebookPath,
332
387
  versionId,
333
- apiClients.models,
334
388
  apiClients.notebooks,
335
389
  ],
336
390
  );
@@ -21,6 +21,14 @@ import {
21
21
  import { Configuration } from "../client/configuration";
22
22
  import { globalQueryClient } from "../utils/queryClient";
23
23
 
24
+ // There's a bug in the OpenAPI generator that causes it to ignore baseURL in
25
+ // the axios request if axios.defaults.baseURL is not set. The per-instance
26
+ // baseURL on the custom axios instance below is the real value we use; this
27
+ // sentinel exists only to satisfy the generated client's code path.
28
+ if (!axios.defaults.baseURL) {
29
+ axios.defaults.baseURL = "IfYouAreSeeingThis_baseURL_IsNotSet";
30
+ }
31
+
24
32
  export interface ServerContextValue {
25
33
  server: string;
26
34
  getAccessToken?: () => Promise<string>;
@@ -115,9 +115,9 @@ export function DimensionFilter({
115
115
  onChange,
116
116
  retrievalFn,
117
117
  }: DimensionFilterProps) {
118
- // Default to "Between" for date filters, otherwise use first available match type
119
118
  const getDefaultMatchType = () => {
120
119
  if (selection?.matchType) return selection.matchType;
120
+ if (spec.defaultMatchType) return spec.defaultMatchType;
121
121
  if (spec.filterType === "DateMinMax") return "Between";
122
122
  return getAvailableMatchTypes(spec.filterType)[0] || "Equals";
123
123
  };
@@ -203,11 +203,11 @@ export function DimensionFilter({
203
203
  // Clear internal state when selection is cleared externally
204
204
  setValue1("");
205
205
  setValue2("");
206
- // Reset to default match type (Between for dates, first available for others)
207
206
  setMatchType(
208
- spec.filterType === "DateMinMax"
209
- ? "Between"
210
- : getAvailableMatchTypes(spec.filterType)[0] || "Equals",
207
+ spec.defaultMatchType ??
208
+ (spec.filterType === "DateMinMax"
209
+ ? "Between"
210
+ : getAvailableMatchTypes(spec.filterType)[0] || "Equals"),
211
211
  );
212
212
  } else if (selection) {
213
213
  // Update internal state when selection changes externally
@@ -216,7 +216,7 @@ export function DimensionFilter({
216
216
  setValue1(selection.value ?? "");
217
217
  setValue2(selection.value2 ?? "");
218
218
  }
219
- }, [selection, spec.filterType]);
219
+ }, [selection, spec.filterType, spec.defaultMatchType]);
220
220
 
221
221
  const availableMatchTypes = getAvailableMatchTypes(spec.filterType);
222
222
  const isDate = isDateFilter(spec.filterType);
@@ -266,6 +266,7 @@ export function DimensionFilter({
266
266
  onChange({
267
267
  dimensionName: spec.dimensionName,
268
268
  source: spec.source,
269
+ filterName: spec.filterName,
269
270
  matchType: newMatchType,
270
271
  value: value1,
271
272
  ...(requiresTwoValues(newMatchType) && value2 && { value2 }),
@@ -296,6 +297,7 @@ export function DimensionFilter({
296
297
  onChange({
297
298
  dimensionName: spec.dimensionName,
298
299
  source: spec.source,
300
+ filterName: spec.filterName,
299
301
  matchType,
300
302
  value: newValue1,
301
303
  ...(needsTwoValues && newValue2 && { value2: newValue2 }),
@@ -326,9 +328,8 @@ export function DimensionFilter({
326
328
  "& .MuiAutocomplete-root .MuiInputBase-root": {
327
329
  padding: "5px 10px",
328
330
  minHeight: "29.5px",
329
- maxHeight: "29.5px",
330
331
  boxSizing: "border-box",
331
- overflow: "hidden",
332
+ flexWrap: "wrap",
332
333
  },
333
334
  "& .MuiChip-root": { height: "16px", margin: "0 2px 0 0" },
334
335
  "& .MuiChip-label": { fontSize: "0.7rem", padding: "0 6px" },
@@ -354,38 +355,62 @@ export function DimensionFilter({
354
355
  "& .MuiSvgIcon-root": { fontSize: "1.25rem" },
355
356
  }}
356
357
  >
357
- {/* Dimension Label/Name */}
358
- <Box sx={{ fontWeight: 600 }}>{spec.label ?? spec.dimensionName}</Box>
359
-
360
- {/* Match Type Selector */}
361
- {spec.filterType !== "Boolean" && (
362
- <FormControl size="small" fullWidth>
363
- <InputLabel>Match Type</InputLabel>
364
- <Select
365
- value={matchType}
366
- label="Match Type"
367
- onChange={handleMatchTypeChange}
368
- disabled={availableMatchTypes.length === 1}
369
- MenuProps={{
370
- PaperProps: {
371
- sx: {
372
- "& .MuiMenuItem-root": {
373
- fontSize: "0.75rem",
374
- minHeight: "auto",
375
- padding: "4px 10px",
376
- },
377
- },
378
- },
358
+ {/* Dimension Label/Name — red when required and not yet set */}
359
+ <Box
360
+ sx={{
361
+ fontWeight: 600,
362
+ color:
363
+ spec.required && !selection ? "error.main" : "text.primary",
364
+ }}
365
+ >
366
+ {spec.label ?? spec.dimensionName}
367
+ {spec.required && (
368
+ <Box
369
+ component="span"
370
+ sx={{
371
+ color: !selection ? "error.main" : "text.secondary",
372
+ fontWeight: 400,
373
+ fontSize: "0.7rem",
374
+ ml: 0.5,
379
375
  }}
380
376
  >
381
- {availableMatchTypes.map((type) => (
382
- <MenuItem key={type} value={type}>
383
- {type}
384
- </MenuItem>
385
- ))}
386
- </Select>
387
- </FormControl>
388
- )}
377
+ (required)
378
+ </Box>
379
+ )}
380
+ </Box>
381
+
382
+ {/* Match Type Selector — hidden when the filter annotation already
383
+ determines the match type (defaultMatchType is set). Only shown
384
+ for legacy/untyped filters where the user needs to choose. */}
385
+ {spec.filterType !== "Boolean" &&
386
+ !spec.defaultMatchType &&
387
+ availableMatchTypes.length > 1 && (
388
+ <FormControl size="small" fullWidth>
389
+ <InputLabel>Match Type</InputLabel>
390
+ <Select
391
+ value={matchType}
392
+ label="Match Type"
393
+ onChange={handleMatchTypeChange}
394
+ MenuProps={{
395
+ PaperProps: {
396
+ sx: {
397
+ "& .MuiMenuItem-root": {
398
+ fontSize: "0.75rem",
399
+ minHeight: "auto",
400
+ padding: "4px 10px",
401
+ },
402
+ },
403
+ },
404
+ }}
405
+ >
406
+ {availableMatchTypes.map((type) => (
407
+ <MenuItem key={type} value={type}>
408
+ {type}
409
+ </MenuItem>
410
+ ))}
411
+ </Select>
412
+ </FormControl>
413
+ )}
389
414
 
390
415
  {/* Value Input - varies by filter type */}
391
416
  {spec.filterType === "Star" && matchType === "Equals" && (
@@ -455,7 +480,11 @@ export function DimensionFilter({
455
480
  size="small"
456
481
  label="Search Text"
457
482
  value={value1}
458
- onChange={(e) => handleValueChange(e.target.value)}
483
+ onChange={(e) => setValue1(e.target.value)}
484
+ onBlur={() => handleValueChange(value1)}
485
+ onKeyDown={(e) => {
486
+ if (e.key === "Enter") handleValueChange(value1);
487
+ }}
459
488
  placeholder="Enter text to search..."
460
489
  fullWidth
461
490
  />