@malloy-publisher/sdk 0.0.199 → 0.0.201
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/ServerProvider-BSyxB6sf.cjs.js +1 -0
- package/dist/{ServerProvider-BuM1usxf.es.js → ServerProvider-DSnbMlP3.es.js} +330 -470
- package/dist/client/api.d.ts +21 -121
- package/dist/client/index.cjs.js +1 -1
- package/dist/client/index.es.js +1 -1
- package/dist/components/Connections/EditConnectionDialog.d.ts +2 -1
- package/dist/components/given/GivenInput.d.ts +16 -0
- package/dist/components/given/GivensPanel.d.ts +18 -0
- package/dist/components/given/index.d.ts +2 -0
- package/dist/{core-DfcpQGVP.es.js → core-B3IQNPBD.es.js} +1 -1
- package/dist/{core-yDgxkpo0.cjs.js → core-GkjltsUf.cjs.js} +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useGivensForm.d.ts +33 -0
- package/dist/hooks/useModelGivens.d.ts +10 -0
- package/dist/index-BpoC5QHF.cjs.js +229 -0
- package/dist/{index-Y4ooZDYA.es.js → index-uW-ZBpF2.es.js} +26102 -25626
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +28 -26
- package/package.json +1 -1
- package/src/components/Connections/DeleteConnectionDialog.tsx +10 -3
- package/src/components/Connections/EditConnectionDialog.tsx +11 -3
- package/src/components/Environment/Connections.tsx +388 -0
- package/src/components/Environment/Environment.tsx +7 -2
- package/src/components/Notebook/Notebook.tsx +81 -23
- package/src/components/Package/Package.tsx +0 -7
- package/src/components/given/GivenInput.tsx +190 -0
- package/src/components/given/GivensPanel.tsx +85 -0
- package/src/components/given/index.ts +2 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/useGivensForm.ts +110 -0
- package/src/hooks/useModelGivens.ts +26 -0
- package/dist/ServerProvider-C_Mnvmgc.cjs.js +0 -1
- package/dist/index-CMA8U4-B.cjs.js +0 -228
- package/src/components/Package/Connections.tsx +0 -373
- /package/dist/components/{Package → Environment}/Connections.d.ts +0 -0
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
FilterSelection,
|
|
12
12
|
useDimensionFilters,
|
|
13
13
|
} from "../../hooks/useDimensionFilters";
|
|
14
|
+
import { GivenValue, useGivensForm } from "../../hooks/useGivensForm";
|
|
15
|
+
import { useModelGivens } from "../../hooks/useModelGivens";
|
|
14
16
|
import { useQueryWithApiError } from "../../hooks/useQueryWithApiError";
|
|
17
|
+
import { parseResourceUri } from "../../utils/formatting";
|
|
15
18
|
import { ApiErrorDisplay } from "../ApiErrorDisplay";
|
|
16
19
|
import { DimensionFilter, RetrievalFunction } from "../filter/DimensionFilter";
|
|
17
20
|
import {
|
|
@@ -19,8 +22,7 @@ import {
|
|
|
19
22
|
parseAllSourceInfos,
|
|
20
23
|
parseNotebookFilterAnnotation,
|
|
21
24
|
} from "../filter/utils";
|
|
22
|
-
|
|
23
|
-
import { parseResourceUri } from "../../utils/formatting";
|
|
25
|
+
import { GivensPanel } from "../given";
|
|
24
26
|
import { Loading } from "../Loading";
|
|
25
27
|
import { useServer } from "../ServerProvider";
|
|
26
28
|
import { CleanNotebookContainer, CleanNotebookSection } from "../styles";
|
|
@@ -216,6 +218,14 @@ export default function Notebook({
|
|
|
216
218
|
[filterStates, getActiveFilters],
|
|
217
219
|
);
|
|
218
220
|
|
|
221
|
+
// Extract model-level givens declared via `given:` and manage user overrides
|
|
222
|
+
const declaredGivens = useModelGivens(notebook);
|
|
223
|
+
const {
|
|
224
|
+
givenValues,
|
|
225
|
+
updateGiven,
|
|
226
|
+
clearAll: clearAllGivens,
|
|
227
|
+
} = useGivensForm(declaredGivens);
|
|
228
|
+
|
|
219
229
|
// Create a map of dimension key -> source name for quick lookup (used by filter UI)
|
|
220
230
|
const _dimensionToSourceMap = useMemo(() => {
|
|
221
231
|
const map = new Map<string, string>();
|
|
@@ -273,11 +283,40 @@ export default function Notebook({
|
|
|
273
283
|
[],
|
|
274
284
|
);
|
|
275
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Serialize given-value overrides into the JSON-encoded string the server
|
|
288
|
+
* expects on the notebook-cell GET endpoint's `givens` query param.
|
|
289
|
+
* Date values are rendered as YYYY-MM-DD; everything else passes through
|
|
290
|
+
* the standard JSON encoder.
|
|
291
|
+
*/
|
|
292
|
+
const buildGivens = useCallback(
|
|
293
|
+
(values: Map<string, GivenValue>): string | undefined => {
|
|
294
|
+
if (values.size === 0) return undefined;
|
|
295
|
+
const encode = (v: GivenValue): unknown => {
|
|
296
|
+
if (v instanceof Date) return v.toISOString().slice(0, 10);
|
|
297
|
+
if (Array.isArray(v)) return v.map((item) => encode(item));
|
|
298
|
+
return v;
|
|
299
|
+
};
|
|
300
|
+
const params: Record<string, unknown> = {};
|
|
301
|
+
values.forEach((value, name) => {
|
|
302
|
+
if (value === null || value === undefined) return;
|
|
303
|
+
params[name] = encode(value);
|
|
304
|
+
});
|
|
305
|
+
return Object.keys(params).length > 0
|
|
306
|
+
? JSON.stringify(params)
|
|
307
|
+
: undefined;
|
|
308
|
+
},
|
|
309
|
+
[],
|
|
310
|
+
);
|
|
311
|
+
|
|
276
312
|
// Unified cell execution function
|
|
277
|
-
// Executes all notebook cells, passing server-side filter params
|
|
313
|
+
// Executes all notebook cells, passing server-side filter params and givens
|
|
278
314
|
// Runs up to 4 requests in parallel for better performance
|
|
279
315
|
const executeCells = useCallback(
|
|
280
|
-
async (
|
|
316
|
+
async (
|
|
317
|
+
filtersToApply: FilterSelection[] = [],
|
|
318
|
+
givensToApply: Map<string, GivenValue> = new Map(),
|
|
319
|
+
) => {
|
|
281
320
|
if (!isSuccess || !notebook?.notebookCells) return;
|
|
282
321
|
|
|
283
322
|
// Initialize or reset cells
|
|
@@ -297,6 +336,7 @@ export default function Notebook({
|
|
|
297
336
|
const filterParams = useServerFilters
|
|
298
337
|
? buildFilterParams(filtersToApply)
|
|
299
338
|
: undefined;
|
|
339
|
+
const givensParam = buildGivens(givensToApply);
|
|
300
340
|
|
|
301
341
|
try {
|
|
302
342
|
// Build execution tasks for code cells
|
|
@@ -313,7 +353,7 @@ export default function Notebook({
|
|
|
313
353
|
|
|
314
354
|
const executeCell = async () => {
|
|
315
355
|
try {
|
|
316
|
-
// Use notebook cell execution API with optional filter_params
|
|
356
|
+
// Use notebook cell execution API with optional filter_params and givens
|
|
317
357
|
const response =
|
|
318
358
|
await apiClients.notebooks.executeNotebookCell(
|
|
319
359
|
environmentName,
|
|
@@ -322,6 +362,8 @@ export default function Notebook({
|
|
|
322
362
|
cellIndex,
|
|
323
363
|
versionId,
|
|
324
364
|
filterParams,
|
|
365
|
+
undefined,
|
|
366
|
+
givensParam,
|
|
325
367
|
);
|
|
326
368
|
|
|
327
369
|
const executedCell = response.data;
|
|
@@ -381,6 +423,7 @@ export default function Notebook({
|
|
|
381
423
|
notebook,
|
|
382
424
|
useServerFilters,
|
|
383
425
|
buildFilterParams,
|
|
426
|
+
buildGivens,
|
|
384
427
|
environmentName,
|
|
385
428
|
packageName,
|
|
386
429
|
notebookPath,
|
|
@@ -389,45 +432,52 @@ export default function Notebook({
|
|
|
389
432
|
],
|
|
390
433
|
);
|
|
391
434
|
|
|
392
|
-
// Execute cells when notebook is loaded (no filters initially)
|
|
435
|
+
// Execute cells when notebook is loaded (no filters or givens initially)
|
|
393
436
|
useEffect(() => {
|
|
394
437
|
if (!isSuccess || !notebook?.notebookCells) return;
|
|
395
|
-
executeCells([]);
|
|
438
|
+
executeCells([], new Map());
|
|
396
439
|
}, [isSuccess, notebook, executeCells]);
|
|
397
440
|
|
|
398
|
-
// Re-execute when filters change
|
|
399
|
-
// Track previous
|
|
400
|
-
const
|
|
441
|
+
// Re-execute when filters or givens change
|
|
442
|
+
// Track previous input shape to detect actual value changes (not just reference changes)
|
|
443
|
+
const prevInputsRef = useRef<string>("");
|
|
401
444
|
|
|
402
445
|
useEffect(() => {
|
|
403
|
-
// Serialize activeFilters to detect actual value changes
|
|
404
|
-
const serialized = JSON.stringify(
|
|
405
|
-
activeFilters.map((f) => ({
|
|
446
|
+
// Serialize activeFilters + givenValues to detect actual value changes
|
|
447
|
+
const serialized = JSON.stringify({
|
|
448
|
+
filters: activeFilters.map((f) => ({
|
|
406
449
|
dim: f.dimensionName,
|
|
407
450
|
type: f.matchType,
|
|
408
451
|
val: f.value,
|
|
409
452
|
val2: f.value2,
|
|
410
453
|
})),
|
|
411
|
-
|
|
454
|
+
givens: Array.from(givenValues.entries()).sort(([a], [b]) =>
|
|
455
|
+
a.localeCompare(b),
|
|
456
|
+
),
|
|
457
|
+
});
|
|
412
458
|
|
|
413
|
-
// Skip if no actual change
|
|
414
|
-
if (serialized ===
|
|
459
|
+
// Skip if no actual change
|
|
460
|
+
if (serialized === prevInputsRef.current) {
|
|
415
461
|
return;
|
|
416
462
|
}
|
|
417
463
|
|
|
418
|
-
// Skip the initial render
|
|
419
|
-
if (
|
|
420
|
-
|
|
464
|
+
// Skip the initial render when both inputs are empty
|
|
465
|
+
if (
|
|
466
|
+
prevInputsRef.current === "" &&
|
|
467
|
+
activeFilters.length === 0 &&
|
|
468
|
+
givenValues.size === 0
|
|
469
|
+
) {
|
|
470
|
+
prevInputsRef.current = serialized;
|
|
421
471
|
return;
|
|
422
472
|
}
|
|
423
473
|
|
|
424
|
-
|
|
474
|
+
prevInputsRef.current = serialized;
|
|
425
475
|
|
|
426
|
-
// Re-execute with current filters (or
|
|
476
|
+
// Re-execute with current filters and givens (or empty if cleared)
|
|
427
477
|
if (!isExecuting) {
|
|
428
|
-
executeCells(activeFilters);
|
|
478
|
+
executeCells(activeFilters, givenValues);
|
|
429
479
|
}
|
|
430
|
-
}, [activeFilters, isExecuting, executeCells]);
|
|
480
|
+
}, [activeFilters, givenValues, isExecuting, executeCells]);
|
|
431
481
|
|
|
432
482
|
// Handle filter change using composite key
|
|
433
483
|
const handleFilterChange = useCallback(
|
|
@@ -447,6 +497,14 @@ export default function Notebook({
|
|
|
447
497
|
<CleanNotebookContainer>
|
|
448
498
|
<CleanNotebookSection>
|
|
449
499
|
<Stack spacing={3} component="section">
|
|
500
|
+
{/* Givens Panel — runtime parameters declared via `given:` */}
|
|
501
|
+
<GivensPanel
|
|
502
|
+
givens={declaredGivens}
|
|
503
|
+
values={givenValues}
|
|
504
|
+
onChange={updateGiven}
|
|
505
|
+
onClearAll={clearAllGivens}
|
|
506
|
+
/>
|
|
507
|
+
|
|
450
508
|
{/* Filter Panel */}
|
|
451
509
|
{dimensionSpecs.length > 0 && filterValuesData && (
|
|
452
510
|
<Paper
|
|
@@ -27,9 +27,6 @@ import { useServer } from "../ServerProvider";
|
|
|
27
27
|
import { encodeResourceUri, parseResourceUri } from "../../utils/formatting";
|
|
28
28
|
import { MALLOY_BRAND, MONO_FONT_FAMILY } from "../styles";
|
|
29
29
|
import ContentTypeIcon from "./ContentTypeIcon";
|
|
30
|
-
// TODO(redesign-followup): port the Connections section into the redesigned
|
|
31
|
-
// flat-row aesthetic (currently rendered with its original card/table styling).
|
|
32
|
-
import Connections from "./Connections";
|
|
33
30
|
|
|
34
31
|
const README_NOTEBOOK = "README.malloynb";
|
|
35
32
|
|
|
@@ -213,10 +210,6 @@ export default function Package({
|
|
|
213
210
|
{databases.length === 0 && <EmptyRow label="No data files" />}
|
|
214
211
|
</PackageSection>
|
|
215
212
|
|
|
216
|
-
<Box sx={{ mb: 4 }}>
|
|
217
|
-
<Connections resourceUri={resourceUri} />
|
|
218
|
-
</Box>
|
|
219
|
-
|
|
220
213
|
{hasReadme && (
|
|
221
214
|
<Box sx={{ mt: 6 }}>
|
|
222
215
|
<Notebook
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import ClearIcon from "@mui/icons-material/Clear";
|
|
2
|
+
import {
|
|
3
|
+
Autocomplete,
|
|
4
|
+
Checkbox,
|
|
5
|
+
FormControlLabel,
|
|
6
|
+
IconButton,
|
|
7
|
+
InputAdornment,
|
|
8
|
+
TextField,
|
|
9
|
+
} from "@mui/material";
|
|
10
|
+
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
|
11
|
+
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
|
|
12
|
+
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
|
|
13
|
+
import dayjs from "dayjs";
|
|
14
|
+
import utc from "dayjs/plugin/utc";
|
|
15
|
+
import { Given } from "../../client";
|
|
16
|
+
import { GivenValue } from "../../hooks/useGivensForm";
|
|
17
|
+
|
|
18
|
+
dayjs.extend(utc);
|
|
19
|
+
|
|
20
|
+
export interface GivenInputProps {
|
|
21
|
+
given: Given;
|
|
22
|
+
value: GivenValue | undefined;
|
|
23
|
+
onChange: (next: GivenValue) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Distill a given's `#(...)` annotation list into helper text for the UI.
|
|
28
|
+
* If an annotation includes `description="..."` (a Malloy convention), the
|
|
29
|
+
* quoted value is surfaced verbatim. Otherwise the annotation contents
|
|
30
|
+
* inside `#(...)` are joined as-is so model authors still see something
|
|
31
|
+
* recognizable. Returns undefined when nothing is renderable.
|
|
32
|
+
*/
|
|
33
|
+
function annotationHelperText(given: Given): string | undefined {
|
|
34
|
+
const visible = (given.annotations ?? []).filter((a) =>
|
|
35
|
+
a.trim().startsWith("#("),
|
|
36
|
+
);
|
|
37
|
+
if (visible.length === 0) return undefined;
|
|
38
|
+
|
|
39
|
+
const rendered: string[] = [];
|
|
40
|
+
for (const raw of visible) {
|
|
41
|
+
const trimmed = raw.trim();
|
|
42
|
+
const descriptionMatch = trimmed.match(/description="([^"]*)"/);
|
|
43
|
+
if (descriptionMatch) {
|
|
44
|
+
rendered.push(descriptionMatch[1]);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// Strip leading `#(` and trailing `)`, then push the inner content
|
|
48
|
+
const inner = trimmed
|
|
49
|
+
.replace(/^#\(/, "")
|
|
50
|
+
.replace(/\)\s*$/, "")
|
|
51
|
+
.trim();
|
|
52
|
+
if (inner) rendered.push(inner);
|
|
53
|
+
}
|
|
54
|
+
return rendered.length > 0 ? rendered.join("\n") : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Renders an input widget appropriate for the declared given type.
|
|
59
|
+
* Unknown / unrecognized types fall back to a plain text input.
|
|
60
|
+
*
|
|
61
|
+
* For text-based inputs (string, number, filter, default), a clear (×)
|
|
62
|
+
* adornment appears when the field has a value. DatePicker, Checkbox, and
|
|
63
|
+
* multi-Autocomplete have their own native clear affordances.
|
|
64
|
+
*/
|
|
65
|
+
export function GivenInput({ given, value, onChange }: GivenInputProps) {
|
|
66
|
+
const label = given.name ?? "";
|
|
67
|
+
const type = given.type ?? "string";
|
|
68
|
+
const helperText = annotationHelperText(given);
|
|
69
|
+
|
|
70
|
+
if (type === "boolean") {
|
|
71
|
+
const checked = value === true;
|
|
72
|
+
// Checkbox wrapped in FormControlLabel — no helperText slot available.
|
|
73
|
+
return (
|
|
74
|
+
<FormControlLabel
|
|
75
|
+
control={
|
|
76
|
+
<Checkbox
|
|
77
|
+
checked={checked}
|
|
78
|
+
onChange={(e) => onChange(e.target.checked)}
|
|
79
|
+
/>
|
|
80
|
+
}
|
|
81
|
+
label={label}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (type === "number") {
|
|
87
|
+
const num = typeof value === "number" ? value : "";
|
|
88
|
+
return (
|
|
89
|
+
<TextField
|
|
90
|
+
label={label}
|
|
91
|
+
type="number"
|
|
92
|
+
value={num}
|
|
93
|
+
onChange={(e) => {
|
|
94
|
+
const v = e.target.value;
|
|
95
|
+
onChange(v === "" ? null : Number(v));
|
|
96
|
+
}}
|
|
97
|
+
helperText={helperText}
|
|
98
|
+
slotProps={{
|
|
99
|
+
input: {
|
|
100
|
+
endAdornment: num !== "" && (
|
|
101
|
+
<ClearAdornment onClear={() => onChange(null)} />
|
|
102
|
+
),
|
|
103
|
+
},
|
|
104
|
+
}}
|
|
105
|
+
fullWidth
|
|
106
|
+
size="small"
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (type === "date" || type === "timestamp" || type === "timestamptz") {
|
|
112
|
+
const dateValue = value instanceof Date ? dayjs.utc(value) : null;
|
|
113
|
+
return (
|
|
114
|
+
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
|
115
|
+
<DatePicker
|
|
116
|
+
label={label}
|
|
117
|
+
value={dateValue}
|
|
118
|
+
onChange={(next) => onChange(next ? next.toDate() : null)}
|
|
119
|
+
slotProps={{
|
|
120
|
+
textField: { fullWidth: true, size: "small", helperText },
|
|
121
|
+
field: { clearable: true, onClear: () => onChange(null) },
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
</LocalizationProvider>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (type.startsWith("array<")) {
|
|
129
|
+
const list = Array.isArray(value) ? value.map(String) : [];
|
|
130
|
+
return (
|
|
131
|
+
<Autocomplete
|
|
132
|
+
multiple
|
|
133
|
+
freeSolo
|
|
134
|
+
options={[]}
|
|
135
|
+
value={list}
|
|
136
|
+
onChange={(_event, next) =>
|
|
137
|
+
onChange(next.length === 0 ? null : (next as string[]))
|
|
138
|
+
}
|
|
139
|
+
renderInput={(params) => (
|
|
140
|
+
<TextField
|
|
141
|
+
{...params}
|
|
142
|
+
label={label}
|
|
143
|
+
size="small"
|
|
144
|
+
helperText={helperText}
|
|
145
|
+
/>
|
|
146
|
+
)}
|
|
147
|
+
fullWidth
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Default: string, filter<...>, or unknown types — plain text input
|
|
153
|
+
const str = typeof value === "string" ? value : "";
|
|
154
|
+
return (
|
|
155
|
+
<TextField
|
|
156
|
+
label={label}
|
|
157
|
+
value={str}
|
|
158
|
+
onChange={(e) => {
|
|
159
|
+
const v = e.target.value;
|
|
160
|
+
onChange(v === "" ? null : v);
|
|
161
|
+
}}
|
|
162
|
+
placeholder={type.startsWith("filter<") ? type : undefined}
|
|
163
|
+
helperText={helperText}
|
|
164
|
+
slotProps={{
|
|
165
|
+
input: {
|
|
166
|
+
endAdornment: str !== "" && (
|
|
167
|
+
<ClearAdornment onClear={() => onChange(null)} />
|
|
168
|
+
),
|
|
169
|
+
},
|
|
170
|
+
}}
|
|
171
|
+
fullWidth
|
|
172
|
+
size="small"
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function ClearAdornment({ onClear }: { onClear: () => void }) {
|
|
178
|
+
return (
|
|
179
|
+
<InputAdornment position="end">
|
|
180
|
+
<IconButton
|
|
181
|
+
size="small"
|
|
182
|
+
aria-label="clear value"
|
|
183
|
+
onClick={onClear}
|
|
184
|
+
edge="end"
|
|
185
|
+
>
|
|
186
|
+
<ClearIcon fontSize="small" />
|
|
187
|
+
</IconButton>
|
|
188
|
+
</InputAdornment>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Box, Button, Paper, Stack, Typography } from "@mui/material";
|
|
2
|
+
import { Given } from "../../client";
|
|
3
|
+
import { GivenValue } from "../../hooks/useGivensForm";
|
|
4
|
+
import { GivenInput } from "./GivenInput";
|
|
5
|
+
|
|
6
|
+
export interface GivensPanelProps {
|
|
7
|
+
givens: Given[];
|
|
8
|
+
values: Map<string, GivenValue>;
|
|
9
|
+
onChange: (name: string, value: GivenValue) => void;
|
|
10
|
+
onClearAll: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Renders the "Parameters" panel — one input per declared `given:`.
|
|
15
|
+
* Returns null when the model declares no givens, so callers can drop
|
|
16
|
+
* `<GivensPanel ... />` unconditionally without a length guard.
|
|
17
|
+
*
|
|
18
|
+
* A "Reset" button appears in the panel header when at least one value
|
|
19
|
+
* is set; it fires `onClearAll` which the parent should wire to
|
|
20
|
+
* `useGivensForm.clearAll`.
|
|
21
|
+
*/
|
|
22
|
+
export function GivensPanel({
|
|
23
|
+
givens,
|
|
24
|
+
values,
|
|
25
|
+
onChange,
|
|
26
|
+
onClearAll,
|
|
27
|
+
}: GivensPanelProps) {
|
|
28
|
+
if (givens.length === 0) return null;
|
|
29
|
+
const hasValues = values.size > 0;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Paper
|
|
33
|
+
elevation={0}
|
|
34
|
+
sx={{
|
|
35
|
+
p: 3,
|
|
36
|
+
backgroundColor: "transparent",
|
|
37
|
+
border: "none",
|
|
38
|
+
boxShadow: "none",
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<Stack
|
|
42
|
+
direction="row"
|
|
43
|
+
alignItems="center"
|
|
44
|
+
justifyContent="space-between"
|
|
45
|
+
sx={{ mb: 2 }}
|
|
46
|
+
>
|
|
47
|
+
<Typography
|
|
48
|
+
variant="subtitle2"
|
|
49
|
+
sx={{ fontWeight: 600, color: "#333" }}
|
|
50
|
+
>
|
|
51
|
+
Parameters
|
|
52
|
+
</Typography>
|
|
53
|
+
{hasValues && (
|
|
54
|
+
<Button
|
|
55
|
+
variant="text"
|
|
56
|
+
size="small"
|
|
57
|
+
onClick={onClearAll}
|
|
58
|
+
sx={{ textTransform: "none" }}
|
|
59
|
+
>
|
|
60
|
+
Reset
|
|
61
|
+
</Button>
|
|
62
|
+
)}
|
|
63
|
+
</Stack>
|
|
64
|
+
<Box
|
|
65
|
+
sx={{
|
|
66
|
+
display: "grid",
|
|
67
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
|
|
68
|
+
gap: 3,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{givens.map((given) => (
|
|
72
|
+
<Box key={given.name}>
|
|
73
|
+
<GivenInput
|
|
74
|
+
given={given}
|
|
75
|
+
value={given.name ? values.get(given.name) : undefined}
|
|
76
|
+
onChange={(next) =>
|
|
77
|
+
given.name && onChange(given.name, next)
|
|
78
|
+
}
|
|
79
|
+
/>
|
|
80
|
+
</Box>
|
|
81
|
+
))}
|
|
82
|
+
</Box>
|
|
83
|
+
</Paper>
|
|
84
|
+
);
|
|
85
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -31,3 +31,11 @@ export {
|
|
|
31
31
|
type FilterType,
|
|
32
32
|
type UseDimensionalFilterRangeDataParams,
|
|
33
33
|
} from "./useDimensionalFilterRangeData";
|
|
34
|
+
|
|
35
|
+
// Givens hooks and types
|
|
36
|
+
export { useModelGivens } from "./useModelGivens";
|
|
37
|
+
export {
|
|
38
|
+
useGivensForm,
|
|
39
|
+
type GivenValue,
|
|
40
|
+
type UseGivensFormResult,
|
|
41
|
+
} from "./useGivensForm";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { Given } from "../client";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* UI-side value for a given. Mirrors the JS shapes the server accepts
|
|
6
|
+
* for `givens` runtime values, plus `Date` (serialized to ISO before send).
|
|
7
|
+
*/
|
|
8
|
+
export type GivenValue =
|
|
9
|
+
| string
|
|
10
|
+
| number
|
|
11
|
+
| boolean
|
|
12
|
+
| Date
|
|
13
|
+
| string[]
|
|
14
|
+
| number[]
|
|
15
|
+
| null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Result from the useGivensForm hook
|
|
19
|
+
*/
|
|
20
|
+
export interface UseGivensFormResult {
|
|
21
|
+
/** Current value for each given, keyed by given name. Missing keys mean "use the model default". */
|
|
22
|
+
givenValues: Map<string, GivenValue>;
|
|
23
|
+
/** Update a given's value. Pass `null` to revert to model default. */
|
|
24
|
+
updateGiven: (name: string, value: GivenValue) => void;
|
|
25
|
+
/** Remove a single given override. */
|
|
26
|
+
clearGiven: (name: string) => void;
|
|
27
|
+
/** Remove all overrides. */
|
|
28
|
+
clearAll: () => void;
|
|
29
|
+
/** Map of just the givens that have a user-supplied (non-null) override. */
|
|
30
|
+
getActiveGivens: () => Map<string, Exclude<GivenValue, null>>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Manages user-supplied values for model `given:` parameters.
|
|
35
|
+
*
|
|
36
|
+
* Mirrors the shape of [`useDimensionFilters`](./useDimensionFilters.ts):
|
|
37
|
+
* one entry per declared given, keyed by name (no source qualifier — givens
|
|
38
|
+
* are model-level, not per-source).
|
|
39
|
+
*
|
|
40
|
+
* When `givens` (the introspected list) changes (e.g., after the notebook
|
|
41
|
+
* loads), existing user overrides are preserved for matching names and new
|
|
42
|
+
* givens are added as empty.
|
|
43
|
+
*/
|
|
44
|
+
export function useGivensForm(givens: Given[]): UseGivensFormResult {
|
|
45
|
+
const [givenValues, setGivenValues] = useState<Map<string, GivenValue>>(
|
|
46
|
+
() => new Map(),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Sync the form when the declared givens list changes (e.g., model reloaded).
|
|
50
|
+
// Preserve existing values for givens that still exist; drop values for givens
|
|
51
|
+
// that no longer exist.
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
setGivenValues((prev) => {
|
|
54
|
+
const declared = new Set(
|
|
55
|
+
givens.map((g) => g.name).filter((n): n is string => !!n),
|
|
56
|
+
);
|
|
57
|
+
const next = new Map<string, GivenValue>();
|
|
58
|
+
prev.forEach((value, name) => {
|
|
59
|
+
if (declared.has(name)) next.set(name, value);
|
|
60
|
+
});
|
|
61
|
+
return next;
|
|
62
|
+
});
|
|
63
|
+
}, [givens]);
|
|
64
|
+
|
|
65
|
+
const updateGiven = useCallback((name: string, value: GivenValue) => {
|
|
66
|
+
setGivenValues((prev) => {
|
|
67
|
+
const next = new Map(prev);
|
|
68
|
+
if (value === null) {
|
|
69
|
+
next.delete(name);
|
|
70
|
+
} else {
|
|
71
|
+
next.set(name, value);
|
|
72
|
+
}
|
|
73
|
+
return next;
|
|
74
|
+
});
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const clearGiven = useCallback((name: string) => {
|
|
78
|
+
setGivenValues((prev) => {
|
|
79
|
+
if (!prev.has(name)) return prev;
|
|
80
|
+
const next = new Map(prev);
|
|
81
|
+
next.delete(name);
|
|
82
|
+
return next;
|
|
83
|
+
});
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
const clearAll = useCallback(() => {
|
|
87
|
+
setGivenValues((prev) => (prev.size === 0 ? prev : new Map()));
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
const getActiveGivens = useCallback((): Map<
|
|
91
|
+
string,
|
|
92
|
+
Exclude<GivenValue, null>
|
|
93
|
+
> => {
|
|
94
|
+
const active = new Map<string, Exclude<GivenValue, null>>();
|
|
95
|
+
givenValues.forEach((value, name) => {
|
|
96
|
+
if (value !== null && value !== undefined) {
|
|
97
|
+
active.set(name, value);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return active;
|
|
101
|
+
}, [givenValues]);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
givenValues,
|
|
105
|
+
updateGiven,
|
|
106
|
+
clearGiven,
|
|
107
|
+
clearAll,
|
|
108
|
+
getActiveGivens,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { Given, RawNotebook } from "../client";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extracts the deduplicated list of model-level `given:` declarations from
|
|
6
|
+
* a notebook's sources. The server attaches the same model-level givens to
|
|
7
|
+
* every `Source` for SDK ergonomics (see #761); this hook collapses them
|
|
8
|
+
* back to one entry per name so callers render a single input per given.
|
|
9
|
+
*
|
|
10
|
+
* Returns an empty array when no givens are declared on any source.
|
|
11
|
+
*/
|
|
12
|
+
export function useModelGivens(notebook: RawNotebook | undefined): Given[] {
|
|
13
|
+
return useMemo(() => {
|
|
14
|
+
if (!notebook?.sources?.length) return [];
|
|
15
|
+
const byName = new Map<string, Given>();
|
|
16
|
+
for (const source of notebook.sources) {
|
|
17
|
+
if (!source.givens?.length) continue;
|
|
18
|
+
for (const given of source.givens) {
|
|
19
|
+
if (given.name && !byName.has(given.name)) {
|
|
20
|
+
byName.set(given.name, given);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return Array.from(byName.values());
|
|
25
|
+
}, [notebook]);
|
|
26
|
+
}
|