@fusedio/widget-sdk 0.1.0
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 +169 -0
- package/dist/bridge.d.ts +247 -0
- package/dist/bridge.js +61 -0
- package/dist/bundle.js +2 -0
- package/dist/define-catalog.d.ts +53 -0
- package/dist/define-catalog.js +3 -0
- package/dist/define-component.d.ts +93 -0
- package/dist/define-component.js +43 -0
- package/dist/form.d.ts +49 -0
- package/dist/form.js +136 -0
- package/dist/hooks/use-allowed-sources.d.ts +20 -0
- package/dist/hooks/use-allowed-sources.js +49 -0
- package/dist/hooks/use-allowed-udf-names.d.ts +15 -0
- package/dist/hooks/use-allowed-udf-names.js +42 -0
- package/dist/hooks/use-canvas-params.d.ts +13 -0
- package/dist/hooks/use-canvas-params.js +58 -0
- package/dist/hooks/use-duckdb-sql.d.ts +61 -0
- package/dist/hooks/use-duckdb-sql.js +558 -0
- package/dist/hooks/use-fused-param.d.ts +40 -0
- package/dist/hooks/use-fused-param.js +283 -0
- package/dist/hooks/use-json-ui-edge-animation.d.ts +22 -0
- package/dist/hooks/use-json-ui-edge-animation.js +26 -0
- package/dist/hooks/use-json-ui-log.d.ts +33 -0
- package/dist/hooks/use-json-ui-log.js +74 -0
- package/dist/hooks/use-json-ui-udf-info.d.ts +24 -0
- package/dist/hooks/use-json-ui-udf-info.js +23 -0
- package/dist/hooks/use-param-substitution.d.ts +22 -0
- package/dist/hooks/use-param-substitution.js +207 -0
- package/dist/hooks/use-udf-output.d.ts +85 -0
- package/dist/hooks/use-udf-output.js +202 -0
- package/dist/hooks/use-upload-access-check.d.ts +19 -0
- package/dist/hooks/use-upload-access-check.js +39 -0
- package/dist/hooks/use-url-signing.d.ts +42 -0
- package/dist/hooks/use-url-signing.js +101 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +40 -0
- package/dist/protocol.d.ts +39 -0
- package/dist/protocol.js +32 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.js +1 -0
- package/dist/utils/sql-placeholders.d.ts +80 -0
- package/dist/utils/sql-placeholders.js +204 -0
- package/package.json +36 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useFusedWidgetBridge, useJsonUiNode } from "../bridge";
|
|
3
|
+
import { useFormParams } from "../form";
|
|
4
|
+
import { ParameterMessageType } from "../protocol";
|
|
5
|
+
/**
|
|
6
|
+
* Two-way bind a component to a named canvas parameter.
|
|
7
|
+
*
|
|
8
|
+
* When another node broadcasts a param value (e.g. a map click, a dropdown
|
|
9
|
+
* selection from upstream), this hook receives it and updates `value`. When
|
|
10
|
+
* the user interacts locally, `setValue()` debounces and broadcasts back so
|
|
11
|
+
* connected UDF nodes re-run with the new value.
|
|
12
|
+
*
|
|
13
|
+
* Form integration: when this component is rendered inside a built-in Form,
|
|
14
|
+
* the live form-field value shadows the canvas value so siblings react
|
|
15
|
+
* before the form is submitted (form values are never broadcast to the
|
|
16
|
+
* canvas until submit).
|
|
17
|
+
*
|
|
18
|
+
* Works as plain local state if `param` is undefined or empty.
|
|
19
|
+
*
|
|
20
|
+
* Requires: a `FusedWidgetBridgeContext.Provider` ancestor. The workbench's
|
|
21
|
+
* `JsonUiProvider` provides one; tests use `createTestBridge()`.
|
|
22
|
+
*
|
|
23
|
+
* @example — number counter
|
|
24
|
+
* const { value, setValue } = useFusedParam({ param: "count", defaultValue: 0 });
|
|
25
|
+
* return <button onClick={() => setValue(value + 1)}>{value}</button>;
|
|
26
|
+
*
|
|
27
|
+
* @example — typed array param
|
|
28
|
+
* const { value, setValue } = useFusedParam({
|
|
29
|
+
* param: "bounds",
|
|
30
|
+
* defaultValue: [-74, 40, -73, 41] as [number, number, number, number],
|
|
31
|
+
* validate: (v): v is [number, number, number, number] =>
|
|
32
|
+
* Array.isArray(v) && v.length === 4 && v.every(n => typeof n === "number"),
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* @example — broadcast immediately on mouseup (bypass debounce)
|
|
36
|
+
* const { setValue, broadcastNow } = useFusedParam({ param: "hue", defaultValue: 0 });
|
|
37
|
+
* <input type="range" onChange={e => setValue(+e.target.value)} onMouseUp={broadcastNow} />
|
|
38
|
+
*
|
|
39
|
+
* @example — clear button
|
|
40
|
+
* const { clearValue } = useFusedParam({ param: "selection", defaultValue: null });
|
|
41
|
+
* <button onClick={() => clearValue(null)}>Reset</button>
|
|
42
|
+
*/
|
|
43
|
+
export function useFusedParam({ param, debounceMs = 300, readOnly = false, defaultValue, broadcastDefaultValue = true, validate, preprocess, }) {
|
|
44
|
+
const bridge = useFusedWidgetBridge();
|
|
45
|
+
const { configHash } = useJsonUiNode();
|
|
46
|
+
const enabled = !!param;
|
|
47
|
+
// Stable log helper — entries appear in the workbench's runtime logs panel.
|
|
48
|
+
// configHash is passed through so nested JsonUiConfigHashOverride subtrees
|
|
49
|
+
// tag entries correctly without requiring a bridge rebuild.
|
|
50
|
+
const logPreview = (action, raw) => {
|
|
51
|
+
if (!param)
|
|
52
|
+
return;
|
|
53
|
+
if (action === "Cleared") {
|
|
54
|
+
bridge.log.log(`Cleared param "${param}"`, "info", configHash);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const preview = JSON.stringify(raw);
|
|
58
|
+
const trimmed = preview && preview.length > 100 ? preview.slice(0, 100) + "…" : preview;
|
|
59
|
+
bridge.log.log(`${action} param "${param}" = ${trimmed}`, "info", configHash);
|
|
60
|
+
};
|
|
61
|
+
// Stable validator and preprocessor. We deliberately omit `defaultValue`
|
|
62
|
+
// from the deps so a parent re-render passing a new defaultValue literal
|
|
63
|
+
// doesn't churn these. The type semantics only depend on the original.
|
|
64
|
+
const preprocessor = useMemo(() => preprocess ?? createDefaultPreprocessor(defaultValue),
|
|
65
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
+
[preprocess]);
|
|
67
|
+
const validator = useMemo(() => validate ?? createDefaultValidator(defaultValue),
|
|
68
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
69
|
+
[validate]);
|
|
70
|
+
// Form-scoped value shadows canvas value when inside a form.
|
|
71
|
+
const paramNamesForForm = useMemo(() => (param ? [param] : []), [param]);
|
|
72
|
+
const { inForm, values: formParams } = useFormParams(paramNamesForForm);
|
|
73
|
+
const formValue = param ? formParams[param] : undefined;
|
|
74
|
+
// ── Initial value resolution ────────────────────────────────────────────
|
|
75
|
+
// Read snapshot once to seed local state — subsequent updates come via the
|
|
76
|
+
// sync effect below. Reading the bridge here is safe (synchronous).
|
|
77
|
+
const initialCanvasValue = useMemo(() => {
|
|
78
|
+
if (!enabled || !param)
|
|
79
|
+
return undefined;
|
|
80
|
+
return bridge.params.getSnapshot(param);
|
|
81
|
+
}, [bridge, enabled, param]);
|
|
82
|
+
const computeInitialValue = () => {
|
|
83
|
+
const raw = inForm && formValue !== undefined ? formValue : initialCanvasValue;
|
|
84
|
+
if (raw === undefined || raw === null)
|
|
85
|
+
return defaultValue;
|
|
86
|
+
const processed = preprocessor(raw);
|
|
87
|
+
return validator(processed) ? processed : defaultValue;
|
|
88
|
+
};
|
|
89
|
+
const [value, setValueState] = useState(computeInitialValue);
|
|
90
|
+
const valueRef = useRef(value);
|
|
91
|
+
valueRef.current = value;
|
|
92
|
+
const debounceRef = useRef(null);
|
|
93
|
+
const isLocalChangeRef = useRef(false);
|
|
94
|
+
const didBroadcastOnMountRef = useRef(false);
|
|
95
|
+
// Stable closure over the latest binding for the unmount cleanup.
|
|
96
|
+
// bridge is captured by ref too so the cleanup effect's deps can stay `[]`
|
|
97
|
+
// — otherwise any bridge identity flip would re-run the effect and
|
|
98
|
+
// broadcast a stray CLEAR for every bound input, cascading through the
|
|
99
|
+
// canvas-param listener fan-out.
|
|
100
|
+
const latestBindingRef = useRef({ enabled, param });
|
|
101
|
+
latestBindingRef.current = { enabled, param };
|
|
102
|
+
const bridgeRef = useRef(bridge);
|
|
103
|
+
bridgeRef.current = bridge;
|
|
104
|
+
// ── Clear bound param when name changes (carry-over avoidance) ──────────
|
|
105
|
+
const prevParamRef = useRef(param);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const prev = prevParamRef.current;
|
|
108
|
+
prevParamRef.current = param;
|
|
109
|
+
if (prev && prev !== param) {
|
|
110
|
+
bridge.params.clear(prev);
|
|
111
|
+
}
|
|
112
|
+
}, [bridge, param]);
|
|
113
|
+
// ── Subscribe to canvas (and form) updates ──────────────────────────────
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (!enabled || !param)
|
|
116
|
+
return;
|
|
117
|
+
if (isLocalChangeRef.current)
|
|
118
|
+
return;
|
|
119
|
+
const raw = inForm && formValue !== undefined
|
|
120
|
+
? formValue
|
|
121
|
+
: bridge.params.getSnapshot(param);
|
|
122
|
+
// Do not reset to defaultValue on transient null/undefined — would wipe
|
|
123
|
+
// the user's local selection during share-mode filtering or fast clicks.
|
|
124
|
+
if (raw === undefined || raw === null)
|
|
125
|
+
return;
|
|
126
|
+
const processed = preprocessor(raw);
|
|
127
|
+
if (processed !== valueRef.current && validator(processed)) {
|
|
128
|
+
setValueState(processed);
|
|
129
|
+
logPreview("Received", raw);
|
|
130
|
+
}
|
|
131
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
132
|
+
}, [bridge, param, enabled, inForm, formValue, preprocessor, validator]);
|
|
133
|
+
// Subscribe to bridge canvas updates so changes from other nodes flow in.
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (!enabled || !param)
|
|
136
|
+
return;
|
|
137
|
+
const unsub = bridge.params.subscribe(param, () => {
|
|
138
|
+
if (isLocalChangeRef.current)
|
|
139
|
+
return;
|
|
140
|
+
const raw = bridge.params.getSnapshot(param);
|
|
141
|
+
if (raw === undefined || raw === null)
|
|
142
|
+
return;
|
|
143
|
+
const processed = preprocessor(raw);
|
|
144
|
+
if (processed !== valueRef.current && validator(processed)) {
|
|
145
|
+
setValueState(processed);
|
|
146
|
+
logPreview("Received", raw);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return unsub;
|
|
150
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
151
|
+
}, [bridge, param, enabled, preprocessor, validator]);
|
|
152
|
+
// ── Broadcast plumbing ──────────────────────────────────────────────────
|
|
153
|
+
const broadcast = useCallback((newValue) => {
|
|
154
|
+
if (!enabled || !param)
|
|
155
|
+
return;
|
|
156
|
+
bridge.params.set(param, newValue, ParameterMessageType.PARAM);
|
|
157
|
+
bridge.edges.stopLoading();
|
|
158
|
+
logPreview("Broadcast", newValue);
|
|
159
|
+
},
|
|
160
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
161
|
+
[bridge, enabled, param]);
|
|
162
|
+
const broadcastNow = useCallback(() => {
|
|
163
|
+
if (!enabled || readOnly)
|
|
164
|
+
return;
|
|
165
|
+
if (debounceRef.current) {
|
|
166
|
+
clearTimeout(debounceRef.current);
|
|
167
|
+
debounceRef.current = null;
|
|
168
|
+
}
|
|
169
|
+
bridge.edges.startLoading();
|
|
170
|
+
broadcast(valueRef.current);
|
|
171
|
+
isLocalChangeRef.current = false;
|
|
172
|
+
}, [bridge, broadcast, enabled, readOnly]);
|
|
173
|
+
const clearValue = useCallback((newValue) => {
|
|
174
|
+
if (debounceRef.current) {
|
|
175
|
+
clearTimeout(debounceRef.current);
|
|
176
|
+
debounceRef.current = null;
|
|
177
|
+
}
|
|
178
|
+
setValueState(newValue);
|
|
179
|
+
valueRef.current = newValue;
|
|
180
|
+
isLocalChangeRef.current = false;
|
|
181
|
+
if (!enabled || !param || readOnly)
|
|
182
|
+
return;
|
|
183
|
+
bridge.params.clear(param);
|
|
184
|
+
bridge.edges.stopLoading();
|
|
185
|
+
logPreview("Cleared", null);
|
|
186
|
+
},
|
|
187
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
188
|
+
[bridge, enabled, param, readOnly]);
|
|
189
|
+
// Reset one-time mount broadcast guard when binding changes.
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
didBroadcastOnMountRef.current = false;
|
|
192
|
+
}, [param, enabled]);
|
|
193
|
+
// Broadcast default value on mount when no canvas value exists.
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (!enabled || !param || readOnly)
|
|
196
|
+
return;
|
|
197
|
+
if (!broadcastDefaultValue)
|
|
198
|
+
return;
|
|
199
|
+
if (didBroadcastOnMountRef.current)
|
|
200
|
+
return;
|
|
201
|
+
const canvasValue = bridge.params.getSnapshot(param);
|
|
202
|
+
if (canvasValue !== undefined && canvasValue !== null) {
|
|
203
|
+
didBroadcastOnMountRef.current = true;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Empty-string defaults are noisy — skip.
|
|
207
|
+
if (valueRef.current === "") {
|
|
208
|
+
didBroadcastOnMountRef.current = true;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
broadcast(valueRef.current);
|
|
212
|
+
didBroadcastOnMountRef.current = true;
|
|
213
|
+
}, [bridge, enabled, param, readOnly, broadcastDefaultValue, broadcast]);
|
|
214
|
+
const setValue = useCallback((newValue) => {
|
|
215
|
+
setValueState(newValue);
|
|
216
|
+
if (!enabled || readOnly)
|
|
217
|
+
return;
|
|
218
|
+
isLocalChangeRef.current = true;
|
|
219
|
+
bridge.edges.startLoading();
|
|
220
|
+
if (debounceRef.current)
|
|
221
|
+
clearTimeout(debounceRef.current);
|
|
222
|
+
debounceRef.current = setTimeout(() => {
|
|
223
|
+
broadcast(newValue);
|
|
224
|
+
isLocalChangeRef.current = false;
|
|
225
|
+
}, debounceMs);
|
|
226
|
+
}, [bridge, broadcast, debounceMs, enabled, readOnly]);
|
|
227
|
+
// Cleanup: cancel pending debounce, and clear the bound param so preset
|
|
228
|
+
// type-swaps don't leave stale canvas state behind. Deps are `[]` so this
|
|
229
|
+
// runs on unmount only — never on bridge identity flips. bridge is read
|
|
230
|
+
// from the ref at cleanup time.
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
return () => {
|
|
233
|
+
if (debounceRef.current)
|
|
234
|
+
clearTimeout(debounceRef.current);
|
|
235
|
+
const { enabled, param } = latestBindingRef.current;
|
|
236
|
+
if (!enabled || !param)
|
|
237
|
+
return;
|
|
238
|
+
bridgeRef.current.params.clear(param);
|
|
239
|
+
};
|
|
240
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
241
|
+
}, []);
|
|
242
|
+
return { value, setValue, broadcastNow, clearValue };
|
|
243
|
+
}
|
|
244
|
+
// ============================================================================
|
|
245
|
+
// Default validator + preprocessor
|
|
246
|
+
// ============================================================================
|
|
247
|
+
function createDefaultValidator(defaultValue) {
|
|
248
|
+
if (typeof defaultValue === "string") {
|
|
249
|
+
return ((v) => typeof v === "string");
|
|
250
|
+
}
|
|
251
|
+
if (typeof defaultValue === "number") {
|
|
252
|
+
return ((v) => typeof v === "number");
|
|
253
|
+
}
|
|
254
|
+
if (typeof defaultValue === "boolean") {
|
|
255
|
+
return ((v) => typeof v === "boolean");
|
|
256
|
+
}
|
|
257
|
+
if (Array.isArray(defaultValue)) {
|
|
258
|
+
return ((v) => Array.isArray(v));
|
|
259
|
+
}
|
|
260
|
+
if (defaultValue !== null && typeof defaultValue === "object") {
|
|
261
|
+
return ((v) => v !== null && typeof v === "object" && !Array.isArray(v));
|
|
262
|
+
}
|
|
263
|
+
return ((_v) => true);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Coerces raw canvas values to T before validation. For string T this
|
|
267
|
+
* prevents the strict type guard from silently dropping numbers/booleans/
|
|
268
|
+
* arrays/objects that arrive as the param value. For other types, passthrough.
|
|
269
|
+
*/
|
|
270
|
+
function createDefaultPreprocessor(defaultValue) {
|
|
271
|
+
if (typeof defaultValue === "string") {
|
|
272
|
+
return (v) => {
|
|
273
|
+
if (typeof v === "string")
|
|
274
|
+
return v;
|
|
275
|
+
if (Array.isArray(v))
|
|
276
|
+
return v.join(",");
|
|
277
|
+
if (v !== null && typeof v === "object")
|
|
278
|
+
return JSON.stringify(v);
|
|
279
|
+
return String(v);
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return (v) => v;
|
|
283
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controls the edge-animation pellet for the current canvas node.
|
|
3
|
+
*
|
|
4
|
+
* Call `startLoading()` when beginning async work and `stopLoading()` on
|
|
5
|
+
* completion — the `true → false` transition is what fires the visual
|
|
6
|
+
* pellet on connected outgoing canvas edges.
|
|
7
|
+
*
|
|
8
|
+
* `useFusedParam` calls these automatically around each broadcast, so use
|
|
9
|
+
* this hook only when you need to animate edges for non-param async work
|
|
10
|
+
* (e.g. a `fetch()` your component runs directly).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const { startLoading, stopLoading } = useJsonUiEdgeAnimation();
|
|
14
|
+
* async function fetchData() {
|
|
15
|
+
* startLoading();
|
|
16
|
+
* try { await heavyCompute(); } finally { stopLoading(); }
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
export declare function useJsonUiEdgeAnimation(): {
|
|
20
|
+
startLoading: () => void;
|
|
21
|
+
stopLoading: () => void;
|
|
22
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useFusedWidgetBridge } from "../bridge";
|
|
2
|
+
/**
|
|
3
|
+
* Controls the edge-animation pellet for the current canvas node.
|
|
4
|
+
*
|
|
5
|
+
* Call `startLoading()` when beginning async work and `stopLoading()` on
|
|
6
|
+
* completion — the `true → false` transition is what fires the visual
|
|
7
|
+
* pellet on connected outgoing canvas edges.
|
|
8
|
+
*
|
|
9
|
+
* `useFusedParam` calls these automatically around each broadcast, so use
|
|
10
|
+
* this hook only when you need to animate edges for non-param async work
|
|
11
|
+
* (e.g. a `fetch()` your component runs directly).
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const { startLoading, stopLoading } = useJsonUiEdgeAnimation();
|
|
15
|
+
* async function fetchData() {
|
|
16
|
+
* startLoading();
|
|
17
|
+
* try { await heavyCompute(); } finally { stopLoading(); }
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
export function useJsonUiEdgeAnimation() {
|
|
21
|
+
const bridge = useFusedWidgetBridge();
|
|
22
|
+
return {
|
|
23
|
+
startLoading: bridge.edges.startLoading,
|
|
24
|
+
stopLoading: bridge.edges.stopLoading,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type JsonUiLogLevel, type LogEntry } from "../bridge";
|
|
2
|
+
/**
|
|
3
|
+
* Logging hook for json-ui components. Entries appear in the workbench's
|
|
4
|
+
* runtime logs panel for the current node.
|
|
5
|
+
*
|
|
6
|
+
* Entries are automatically tagged with the current widget's `configHash`
|
|
7
|
+
* (resolved through `JsonUiNodeOverrideContext` so nested
|
|
8
|
+
* `JsonUiConfigHashOverride` subtrees emit correctly-scoped entries),
|
|
9
|
+
* which lets the workbench discard stale entries when a widget's JSON is
|
|
10
|
+
* edited.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const { log } = useJsonUiLog();
|
|
14
|
+
* log("Selected city: " + city);
|
|
15
|
+
* log("Failed to load chart data", "error");
|
|
16
|
+
*/
|
|
17
|
+
export declare function useJsonUiLog(): {
|
|
18
|
+
log: (message: string, level?: JsonUiLogLevel) => void;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Read-only hook returning the current log entries for a given node.
|
|
22
|
+
* Returns an empty array if `nodeId` is undefined or the node has no logs.
|
|
23
|
+
*
|
|
24
|
+
* Primarily used by the workbench's runtime logs panel — catalog components
|
|
25
|
+
* rarely need to read logs they wrote, but the hook is exposed for
|
|
26
|
+
* completeness (e.g. building a debug view component).
|
|
27
|
+
*/
|
|
28
|
+
export declare function useJsonUiLogs(nodeId: string | undefined): readonly LogEntry[];
|
|
29
|
+
/**
|
|
30
|
+
* Returns a stable callback that clears all log entries for the given node.
|
|
31
|
+
* Returns a no-op function when `nodeId` is undefined.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useJsonUiLogClear(nodeId: string | undefined): () => void;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
import { useFusedWidgetBridge, useJsonUiNode, } from "../bridge";
|
|
3
|
+
const EMPTY_LOGS = Object.freeze([]);
|
|
4
|
+
/**
|
|
5
|
+
* Logging hook for json-ui components. Entries appear in the workbench's
|
|
6
|
+
* runtime logs panel for the current node.
|
|
7
|
+
*
|
|
8
|
+
* Entries are automatically tagged with the current widget's `configHash`
|
|
9
|
+
* (resolved through `JsonUiNodeOverrideContext` so nested
|
|
10
|
+
* `JsonUiConfigHashOverride` subtrees emit correctly-scoped entries),
|
|
11
|
+
* which lets the workbench discard stale entries when a widget's JSON is
|
|
12
|
+
* edited.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const { log } = useJsonUiLog();
|
|
16
|
+
* log("Selected city: " + city);
|
|
17
|
+
* log("Failed to load chart data", "error");
|
|
18
|
+
*/
|
|
19
|
+
export function useJsonUiLog() {
|
|
20
|
+
const bridge = useFusedWidgetBridge();
|
|
21
|
+
const { configHash } = useJsonUiNode();
|
|
22
|
+
// bridge and configHash are read from refs so the returned `log` callback
|
|
23
|
+
// identity is stable across re-renders. Downstream effects with `log` in
|
|
24
|
+
// their deps (e.g. use-duckdb-sql.ts's preprocessing effect) would
|
|
25
|
+
// otherwise re-fire on every bridge flip / configHash change and cascade
|
|
26
|
+
// setState across every SQL widget in the dashboard.
|
|
27
|
+
const bridgeRef = useRef(bridge);
|
|
28
|
+
bridgeRef.current = bridge;
|
|
29
|
+
const configHashRef = useRef(configHash);
|
|
30
|
+
configHashRef.current = configHash;
|
|
31
|
+
const log = useCallback((message, level = "info") => {
|
|
32
|
+
bridgeRef.current.log.log(message, level, configHashRef.current);
|
|
33
|
+
}, []);
|
|
34
|
+
return useMemo(() => ({ log }), [log]);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read-only hook returning the current log entries for a given node.
|
|
38
|
+
* Returns an empty array if `nodeId` is undefined or the node has no logs.
|
|
39
|
+
*
|
|
40
|
+
* Primarily used by the workbench's runtime logs panel — catalog components
|
|
41
|
+
* rarely need to read logs they wrote, but the hook is exposed for
|
|
42
|
+
* completeness (e.g. building a debug view component).
|
|
43
|
+
*/
|
|
44
|
+
export function useJsonUiLogs(nodeId) {
|
|
45
|
+
const bridge = useFusedWidgetBridge();
|
|
46
|
+
const subscribe = useCallback((cb) => {
|
|
47
|
+
if (!nodeId)
|
|
48
|
+
return () => { };
|
|
49
|
+
return bridge.log.subscribeLogs(nodeId, cb);
|
|
50
|
+
}, [bridge, nodeId]);
|
|
51
|
+
const snapshotRef = useRef(EMPTY_LOGS);
|
|
52
|
+
const getSnapshot = useCallback(() => {
|
|
53
|
+
if (!nodeId)
|
|
54
|
+
return EMPTY_LOGS;
|
|
55
|
+
const next = bridge.log.getLogsSnapshot(nodeId);
|
|
56
|
+
if (next === snapshotRef.current)
|
|
57
|
+
return snapshotRef.current;
|
|
58
|
+
snapshotRef.current = next;
|
|
59
|
+
return next;
|
|
60
|
+
}, [bridge, nodeId]);
|
|
61
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Returns a stable callback that clears all log entries for the given node.
|
|
65
|
+
* Returns a no-op function when `nodeId` is undefined.
|
|
66
|
+
*/
|
|
67
|
+
export function useJsonUiLogClear(nodeId) {
|
|
68
|
+
const bridge = useFusedWidgetBridge();
|
|
69
|
+
return useCallback(() => {
|
|
70
|
+
if (!nodeId)
|
|
71
|
+
return;
|
|
72
|
+
bridge.log.clearLogs(nodeId);
|
|
73
|
+
}, [bridge, nodeId]);
|
|
74
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the identity of the current canvas node.
|
|
3
|
+
*
|
|
4
|
+
* `udfUniqueId` and `udfName` are used internally by `useFusedParam` for
|
|
5
|
+
* edge-based param routing. Catalog components rarely need to read these
|
|
6
|
+
* directly — they're surfaced primarily so logs and debugging can be
|
|
7
|
+
* scoped to a node.
|
|
8
|
+
*
|
|
9
|
+
* Reads from `JsonUiNodeOverrideContext` first (so nested
|
|
10
|
+
* `JsonUiConfigHashOverride` subtrees see their override), falling back to
|
|
11
|
+
* the bridge's node identity.
|
|
12
|
+
*
|
|
13
|
+
* Returns `{ udfUniqueId: undefined, udfName: undefined, configHash: undefined }`
|
|
14
|
+
* when the surrounding bridge has not populated them.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const { udfName } = useJsonUiUdfInfo();
|
|
18
|
+
* console.log("I am node:", udfName);
|
|
19
|
+
*/
|
|
20
|
+
export declare function useJsonUiUdfInfo(): {
|
|
21
|
+
udfUniqueId: string | undefined;
|
|
22
|
+
udfName: string | undefined;
|
|
23
|
+
configHash: string | undefined;
|
|
24
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useJsonUiNode } from "../bridge";
|
|
2
|
+
/**
|
|
3
|
+
* Returns the identity of the current canvas node.
|
|
4
|
+
*
|
|
5
|
+
* `udfUniqueId` and `udfName` are used internally by `useFusedParam` for
|
|
6
|
+
* edge-based param routing. Catalog components rarely need to read these
|
|
7
|
+
* directly — they're surfaced primarily so logs and debugging can be
|
|
8
|
+
* scoped to a node.
|
|
9
|
+
*
|
|
10
|
+
* Reads from `JsonUiNodeOverrideContext` first (so nested
|
|
11
|
+
* `JsonUiConfigHashOverride` subtrees see their override), falling back to
|
|
12
|
+
* the bridge's node identity.
|
|
13
|
+
*
|
|
14
|
+
* Returns `{ udfUniqueId: undefined, udfName: undefined, configHash: undefined }`
|
|
15
|
+
* when the surrounding bridge has not populated them.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const { udfName } = useJsonUiUdfInfo();
|
|
19
|
+
* console.log("I am node:", udfName);
|
|
20
|
+
*/
|
|
21
|
+
export function useJsonUiUdfInfo() {
|
|
22
|
+
return useJsonUiNode();
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ParamSubstitutionOptions, ParamSubstitutionResult } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Reactively resolve `$param_name` and `{{udf_name}}` placeholders.
|
|
4
|
+
*
|
|
5
|
+
* - `$param_name` is substituted from canvas params (edge-filtered) and
|
|
6
|
+
* form-scoped values when inside a form.
|
|
7
|
+
* - `{{udf_name}}` (and variants like `{{udf_name.col}}`, `{{udf_name?k=v}}`,
|
|
8
|
+
* HTML template node refs) is resolved by the host via the template bridge.
|
|
9
|
+
*
|
|
10
|
+
* Returns `{ value, loading }`. `loading` is `true` only while `{{udf}}`
|
|
11
|
+
* data is being fetched; pure `$param` templates never trigger loading.
|
|
12
|
+
*
|
|
13
|
+
* @example — dynamic label
|
|
14
|
+
* const { value } = useParamSubstitution("Selected region: $region");
|
|
15
|
+
*
|
|
16
|
+
* @example — SQL fragment with missing-param preservation
|
|
17
|
+
* const { value: where } = useParamSubstitution(
|
|
18
|
+
* "WHERE city = '$city'",
|
|
19
|
+
* { preserveMissingParams: true }
|
|
20
|
+
* );
|
|
21
|
+
*/
|
|
22
|
+
export declare function useParamSubstitution(template: string | undefined, options?: ParamSubstitutionOptions): ParamSubstitutionResult;
|