@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,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactively resolve `$param` and `{{udf}}` placeholders in a template.
|
|
3
|
+
*
|
|
4
|
+
* Pure-`$param` templates are handled in-SDK with a simple regex
|
|
5
|
+
* substitution. Templates that reference `{{udf}}` (potentially with
|
|
6
|
+
* `?overrides`, column/index access, or HTML template node recursion)
|
|
7
|
+
* delegate to `bridge.template.render` so the host can use its rich UDF
|
|
8
|
+
* machinery. The SDK orchestrates state, cancellation, and re-runs.
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useMemo, useRef, useState, useSyncExternalStore, useCallback, } from "react";
|
|
11
|
+
import { useFusedWidgetBridge } from "../bridge";
|
|
12
|
+
import { useFormParams } from "../form";
|
|
13
|
+
import { useCanvasParams } from "./use-canvas-params";
|
|
14
|
+
const PARAM_TOKEN_RE = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
|
|
15
|
+
const UDF_PLACEHOLDER_RE = /\{\{[\s\S]*?\}\}/;
|
|
16
|
+
// Mirrors `parseInlineUdfPlaceholders` for the purpose of finding which UDF
|
|
17
|
+
// names a template references. We only need the leading identifier — full
|
|
18
|
+
// access-path / override parsing is the host's concern.
|
|
19
|
+
const UDF_NAME_EXTRACT_RE = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\b/g;
|
|
20
|
+
/**
|
|
21
|
+
* Reactively resolve `$param_name` and `{{udf_name}}` placeholders.
|
|
22
|
+
*
|
|
23
|
+
* - `$param_name` is substituted from canvas params (edge-filtered) and
|
|
24
|
+
* form-scoped values when inside a form.
|
|
25
|
+
* - `{{udf_name}}` (and variants like `{{udf_name.col}}`, `{{udf_name?k=v}}`,
|
|
26
|
+
* HTML template node refs) is resolved by the host via the template bridge.
|
|
27
|
+
*
|
|
28
|
+
* Returns `{ value, loading }`. `loading` is `true` only while `{{udf}}`
|
|
29
|
+
* data is being fetched; pure `$param` templates never trigger loading.
|
|
30
|
+
*
|
|
31
|
+
* @example — dynamic label
|
|
32
|
+
* const { value } = useParamSubstitution("Selected region: $region");
|
|
33
|
+
*
|
|
34
|
+
* @example — SQL fragment with missing-param preservation
|
|
35
|
+
* const { value: where } = useParamSubstitution(
|
|
36
|
+
* "WHERE city = '$city'",
|
|
37
|
+
* { preserveMissingParams: true }
|
|
38
|
+
* );
|
|
39
|
+
*/
|
|
40
|
+
export function useParamSubstitution(template, options = {}) {
|
|
41
|
+
const safeTemplate = template ?? "";
|
|
42
|
+
const preserveMissingParams = options.preserveMissingParams ?? false;
|
|
43
|
+
const bridge = useFusedWidgetBridge();
|
|
44
|
+
const paramNames = useMemo(() => extractParamNames(safeTemplate), [safeTemplate]);
|
|
45
|
+
const canvasValues = useCanvasParams(paramNames);
|
|
46
|
+
const { inForm, values: formValues } = useFormParams(paramNames);
|
|
47
|
+
const paramValues = useMemo(() => (inForm ? { ...canvasValues, ...formValues } : canvasValues), [canvasValues, formValues, inForm]);
|
|
48
|
+
const hasUdfRefs = useMemo(() => UDF_PLACEHOLDER_RE.test(safeTemplate), [safeTemplate]);
|
|
49
|
+
// ── Fast path: no UDF refs ─────────────────────────────────────────────
|
|
50
|
+
const pureParamValue = useMemo(() => {
|
|
51
|
+
if (hasUdfRefs)
|
|
52
|
+
return "";
|
|
53
|
+
return substituteParams(safeTemplate, paramValues, preserveMissingParams);
|
|
54
|
+
}, [safeTemplate, paramValues, preserveMissingParams, hasUdfRefs]);
|
|
55
|
+
// ── Slow path: UDF refs ────────────────────────────────────────────────
|
|
56
|
+
// Extract referenced UDF names so we can subscribe to their outputs.
|
|
57
|
+
// When any of them re-executes we bump a tick and the effect below
|
|
58
|
+
// re-runs `bridge.template.render` with fresh data.
|
|
59
|
+
const referencedUdfNames = useMemo(() => {
|
|
60
|
+
if (!hasUdfRefs)
|
|
61
|
+
return [];
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const out = [];
|
|
64
|
+
UDF_NAME_EXTRACT_RE.lastIndex = 0;
|
|
65
|
+
let m;
|
|
66
|
+
while ((m = UDF_NAME_EXTRACT_RE.exec(safeTemplate)) !== null) {
|
|
67
|
+
if (!seen.has(m[1])) {
|
|
68
|
+
seen.add(m[1]);
|
|
69
|
+
out.push(m[1]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}, [safeTemplate, hasUdfRefs]);
|
|
74
|
+
// Subscribe to host re-render triggers (UDF outputs + topology) so that
|
|
75
|
+
// the async render re-runs when an upstream UDF re-executes or edges shift.
|
|
76
|
+
const templateChangeKey = useTemplateBridgeChangeKey(bridge, referencedUdfNames);
|
|
77
|
+
const [resolved, setResolved] = useState(() => ({ key: "", value: "" }));
|
|
78
|
+
const [loading, setLoading] = useState(false);
|
|
79
|
+
// Render key includes template + paramValues + the host change ticker.
|
|
80
|
+
const renderKey = useMemo(() => {
|
|
81
|
+
return JSON.stringify({
|
|
82
|
+
template: safeTemplate,
|
|
83
|
+
paramValues,
|
|
84
|
+
preserveMissingParams,
|
|
85
|
+
tick: templateChangeKey,
|
|
86
|
+
});
|
|
87
|
+
}, [safeTemplate, paramValues, preserveMissingParams, templateChangeKey]);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!hasUdfRefs) {
|
|
90
|
+
setLoading(false);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
let cancelled = false;
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
setLoading(true);
|
|
96
|
+
bridge.template
|
|
97
|
+
.render(safeTemplate, paramValues, {
|
|
98
|
+
preserveMissingParams,
|
|
99
|
+
signal: controller.signal,
|
|
100
|
+
})
|
|
101
|
+
.then((result) => {
|
|
102
|
+
if (cancelled)
|
|
103
|
+
return;
|
|
104
|
+
setResolved((prev) => prev.key === renderKey && prev.value === result.value
|
|
105
|
+
? prev
|
|
106
|
+
: { key: renderKey, value: result.value });
|
|
107
|
+
setLoading(result.loading);
|
|
108
|
+
}, (err) => {
|
|
109
|
+
if (cancelled)
|
|
110
|
+
return;
|
|
111
|
+
if (err?.name === "AbortError")
|
|
112
|
+
return;
|
|
113
|
+
setLoading(false);
|
|
114
|
+
});
|
|
115
|
+
return () => {
|
|
116
|
+
cancelled = true;
|
|
117
|
+
controller.abort();
|
|
118
|
+
};
|
|
119
|
+
}, [
|
|
120
|
+
bridge,
|
|
121
|
+
hasUdfRefs,
|
|
122
|
+
safeTemplate,
|
|
123
|
+
paramValues,
|
|
124
|
+
preserveMissingParams,
|
|
125
|
+
renderKey,
|
|
126
|
+
]);
|
|
127
|
+
const value = useMemo(() => {
|
|
128
|
+
if (!hasUdfRefs)
|
|
129
|
+
return pureParamValue;
|
|
130
|
+
if (resolved.key === renderKey)
|
|
131
|
+
return resolved.value;
|
|
132
|
+
// Loading placeholder: ask host for best-effort sync render.
|
|
133
|
+
try {
|
|
134
|
+
return bridge.template.renderLoading(safeTemplate, paramValues, {
|
|
135
|
+
preserveMissingParams,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return safeTemplate;
|
|
140
|
+
}
|
|
141
|
+
}, [
|
|
142
|
+
bridge,
|
|
143
|
+
hasUdfRefs,
|
|
144
|
+
pureParamValue,
|
|
145
|
+
resolved,
|
|
146
|
+
renderKey,
|
|
147
|
+
safeTemplate,
|
|
148
|
+
paramValues,
|
|
149
|
+
preserveMissingParams,
|
|
150
|
+
]);
|
|
151
|
+
return { value, loading: hasUdfRefs ? loading : false };
|
|
152
|
+
}
|
|
153
|
+
function extractParamNames(template) {
|
|
154
|
+
const names = [];
|
|
155
|
+
const seen = new Set();
|
|
156
|
+
for (const match of template.matchAll(PARAM_TOKEN_RE)) {
|
|
157
|
+
const name = match[1];
|
|
158
|
+
if (!seen.has(name)) {
|
|
159
|
+
seen.add(name);
|
|
160
|
+
names.push(name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return names;
|
|
164
|
+
}
|
|
165
|
+
function substituteParams(template, values, preserveMissingParams) {
|
|
166
|
+
return template.replace(PARAM_TOKEN_RE, (match, name) => {
|
|
167
|
+
const v = values[name];
|
|
168
|
+
if (v === undefined || v === null) {
|
|
169
|
+
return preserveMissingParams ? match : "";
|
|
170
|
+
}
|
|
171
|
+
return stringifyParamValue(v);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function stringifyParamValue(value) {
|
|
175
|
+
if (value === undefined || value === null)
|
|
176
|
+
return "";
|
|
177
|
+
if (typeof value === "string")
|
|
178
|
+
return value;
|
|
179
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
180
|
+
return String(value);
|
|
181
|
+
const serialized = JSON.stringify(value);
|
|
182
|
+
return serialized ?? "";
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Subscribe to the template bridge's change signal + every referenced UDF's
|
|
186
|
+
* output via `bridge.udfs.subscribeOutput`. The returned numeric key
|
|
187
|
+
* changes whenever a re-render could produce a different result.
|
|
188
|
+
*/
|
|
189
|
+
function useTemplateBridgeChangeKey(bridge, referencedUdfNames) {
|
|
190
|
+
const tickRef = useRef(0);
|
|
191
|
+
const namesKey = referencedUdfNames.slice().sort().join("|");
|
|
192
|
+
const subscribe = useCallback((cb) => {
|
|
193
|
+
const fire = () => {
|
|
194
|
+
tickRef.current += 1;
|
|
195
|
+
cb();
|
|
196
|
+
};
|
|
197
|
+
const unsubs = [bridge.template.subscribe(fire)];
|
|
198
|
+
for (const name of referencedUdfNames) {
|
|
199
|
+
unsubs.push(bridge.udfs.subscribeOutput(name, fire));
|
|
200
|
+
}
|
|
201
|
+
return () => unsubs.forEach((u) => u());
|
|
202
|
+
},
|
|
203
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
204
|
+
[bridge, namesKey]);
|
|
205
|
+
const getSnapshot = useCallback(() => tickRef.current, []);
|
|
206
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
207
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type UdfOutputSnapshot } from "../bridge";
|
|
2
|
+
/**
|
|
3
|
+
* Subscribe to a UDF's output by name. Returns the current `UdfOutputSnapshot`
|
|
4
|
+
* (data + execution status + optional error + VFS filename) or `undefined`
|
|
5
|
+
* if the UDF is not present in the canvas.
|
|
6
|
+
*
|
|
7
|
+
* Re-renders when the UDF's results change (after re-execution) or when its
|
|
8
|
+
* execution status flips.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const out = useUdfOutputByName("cities_udf");
|
|
12
|
+
* if (!out) return <div>UDF not found</div>;
|
|
13
|
+
* if (out.isExecutionInProgress) return <div>Loading…</div>;
|
|
14
|
+
* if (out.error) return <div>Error: {out.error}</div>;
|
|
15
|
+
*/
|
|
16
|
+
export declare function useUdfOutputByName(udfName: string | undefined): UdfOutputSnapshot | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Request the workbench to re-execute a named UDF (e.g. after the user
|
|
19
|
+
* clicks a "Refresh" button). No-op in test harnesses without re-execution.
|
|
20
|
+
*/
|
|
21
|
+
export declare function useRequestUdfReexecute(): (udfName: string) => void;
|
|
22
|
+
export type ParsedUdfQuery = {
|
|
23
|
+
udfName: string;
|
|
24
|
+
columnName: string;
|
|
25
|
+
index?: number;
|
|
26
|
+
} | null;
|
|
27
|
+
/**
|
|
28
|
+
* Fast check whether a string is `{{udf.col}}` or `{{udf.col[idx]}}`.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* isUdfQuery("{{my_udf.city}}") // true
|
|
32
|
+
* isUdfQuery("{{my_udf.city[0]}}") // true
|
|
33
|
+
* isUdfQuery("not a query") // false
|
|
34
|
+
*/
|
|
35
|
+
export declare function isUdfQuery(query?: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Parse a UDF column query string. Returns `null` for unrecognised input.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* parseUdfColumnQuery("{{my_udf.city}}") // { udfName: "my_udf", columnName: "city" }
|
|
41
|
+
* parseUdfColumnQuery("{{my_udf.city[0]}}") // { ..., index: 0 }
|
|
42
|
+
*/
|
|
43
|
+
export declare function parseUdfColumnQuery(query?: string): ParsedUdfQuery;
|
|
44
|
+
export interface UseUdfDataFrameSampleOptions {
|
|
45
|
+
udfName?: string;
|
|
46
|
+
sampleSize?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface UseUdfDataFrameSampleResult {
|
|
49
|
+
loading: boolean;
|
|
50
|
+
errorMessage: string | null;
|
|
51
|
+
isError: boolean;
|
|
52
|
+
columns: string[];
|
|
53
|
+
rows: Record<string, unknown>[];
|
|
54
|
+
requestReexecute: () => void;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Pull a small sample of rows from a UDF's DataFrame output and derive
|
|
58
|
+
* column names. Used by dropdowns and galleries that populate options
|
|
59
|
+
* from UDF data.
|
|
60
|
+
*/
|
|
61
|
+
export declare function useUdfDataFrameSample({ udfName, sampleSize, }: UseUdfDataFrameSampleOptions): UseUdfDataFrameSampleResult;
|
|
62
|
+
export interface UseUdfColumnValuesResult {
|
|
63
|
+
values: unknown[];
|
|
64
|
+
loading: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Read all values from a UDF column using `{{udf.col}}` query syntax.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* const { values, loading } = useUdfColumnValues("{{my_udf.city}}");
|
|
71
|
+
* // values = ["NYC", "LA", "SF", ...]
|
|
72
|
+
*/
|
|
73
|
+
export declare function useUdfColumnValues(query?: string, sampleSize?: number): UseUdfColumnValuesResult;
|
|
74
|
+
export interface UseUdfColumnValueResult {
|
|
75
|
+
value: unknown | null;
|
|
76
|
+
loading: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Read a single value from a UDF column at a specific index.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* const { value, loading } = useUdfColumnValue("{{my_udf.city[0]}}");
|
|
83
|
+
* // value = "NYC"
|
|
84
|
+
*/
|
|
85
|
+
export declare function useUdfColumnValue(query?: string, sampleSize?: number): UseUdfColumnValueResult;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
2
|
+
import { useFusedWidgetBridge } from "../bridge";
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe to a UDF's output by name. Returns the current `UdfOutputSnapshot`
|
|
5
|
+
* (data + execution status + optional error + VFS filename) or `undefined`
|
|
6
|
+
* if the UDF is not present in the canvas.
|
|
7
|
+
*
|
|
8
|
+
* Re-renders when the UDF's results change (after re-execution) or when its
|
|
9
|
+
* execution status flips.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const out = useUdfOutputByName("cities_udf");
|
|
13
|
+
* if (!out) return <div>UDF not found</div>;
|
|
14
|
+
* if (out.isExecutionInProgress) return <div>Loading…</div>;
|
|
15
|
+
* if (out.error) return <div>Error: {out.error}</div>;
|
|
16
|
+
*/
|
|
17
|
+
export function useUdfOutputByName(udfName) {
|
|
18
|
+
const bridge = useFusedWidgetBridge();
|
|
19
|
+
const subscribe = useCallback((cb) => {
|
|
20
|
+
if (!udfName)
|
|
21
|
+
return () => { };
|
|
22
|
+
return bridge.udfs.subscribeOutput(udfName, cb);
|
|
23
|
+
}, [bridge, udfName]);
|
|
24
|
+
const snapshotRef = useRef(undefined);
|
|
25
|
+
const getSnapshot = useCallback(() => {
|
|
26
|
+
if (!udfName)
|
|
27
|
+
return undefined;
|
|
28
|
+
const next = bridge.udfs.getOutputSnapshot(udfName);
|
|
29
|
+
const prev = snapshotRef.current;
|
|
30
|
+
if (snapshotsEqual(prev, next))
|
|
31
|
+
return prev;
|
|
32
|
+
snapshotRef.current = next;
|
|
33
|
+
return next;
|
|
34
|
+
}, [bridge, udfName]);
|
|
35
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Request the workbench to re-execute a named UDF (e.g. after the user
|
|
39
|
+
* clicks a "Refresh" button). No-op in test harnesses without re-execution.
|
|
40
|
+
*/
|
|
41
|
+
export function useRequestUdfReexecute() {
|
|
42
|
+
const bridge = useFusedWidgetBridge();
|
|
43
|
+
return useCallback((udfName) => bridge.udfs.requestReexecute(udfName), [bridge]);
|
|
44
|
+
}
|
|
45
|
+
function snapshotsEqual(a, b) {
|
|
46
|
+
if (a === b)
|
|
47
|
+
return true;
|
|
48
|
+
if (!a || !b)
|
|
49
|
+
return false;
|
|
50
|
+
return (a.data === b.data &&
|
|
51
|
+
a.isExecutionInProgress === b.isExecutionInProgress &&
|
|
52
|
+
a.error === b.error &&
|
|
53
|
+
a.vfsFilename === b.vfsFilename);
|
|
54
|
+
}
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// UDF column query convenience hooks
|
|
57
|
+
// ============================================================================
|
|
58
|
+
const UDF_QUERY_REGEX = /^\{\{(\w+)\.(\w+)(?:\[(\d+)\])?\}\}$/;
|
|
59
|
+
/**
|
|
60
|
+
* Fast check whether a string is `{{udf.col}}` or `{{udf.col[idx]}}`.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* isUdfQuery("{{my_udf.city}}") // true
|
|
64
|
+
* isUdfQuery("{{my_udf.city[0]}}") // true
|
|
65
|
+
* isUdfQuery("not a query") // false
|
|
66
|
+
*/
|
|
67
|
+
export function isUdfQuery(query) {
|
|
68
|
+
if (!query || typeof query !== "string")
|
|
69
|
+
return false;
|
|
70
|
+
return UDF_QUERY_REGEX.test(query);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Parse a UDF column query string. Returns `null` for unrecognised input.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* parseUdfColumnQuery("{{my_udf.city}}") // { udfName: "my_udf", columnName: "city" }
|
|
77
|
+
* parseUdfColumnQuery("{{my_udf.city[0]}}") // { ..., index: 0 }
|
|
78
|
+
*/
|
|
79
|
+
export function parseUdfColumnQuery(query) {
|
|
80
|
+
if (!isUdfQuery(query))
|
|
81
|
+
return null;
|
|
82
|
+
const match = query.match(UDF_QUERY_REGEX);
|
|
83
|
+
if (!match)
|
|
84
|
+
return null;
|
|
85
|
+
const [, udfName, columnName, indexStr] = match;
|
|
86
|
+
const index = indexStr !== undefined ? parseInt(indexStr, 10) : undefined;
|
|
87
|
+
return { udfName, columnName, index };
|
|
88
|
+
}
|
|
89
|
+
function isDataSourceLike(value) {
|
|
90
|
+
return (!!value &&
|
|
91
|
+
typeof value === "object" &&
|
|
92
|
+
typeof value.getRows === "function");
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Pull a small sample of rows from a UDF's DataFrame output and derive
|
|
96
|
+
* column names. Used by dropdowns and galleries that populate options
|
|
97
|
+
* from UDF data.
|
|
98
|
+
*/
|
|
99
|
+
export function useUdfDataFrameSample({ udfName, sampleSize = 200, }) {
|
|
100
|
+
const snapshot = useUdfOutputByName(udfName);
|
|
101
|
+
const reexecute = useRequestUdfReexecute();
|
|
102
|
+
const [rows, setRows] = useState([]);
|
|
103
|
+
const [columns, setColumns] = useState([]);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
let cancelled = false;
|
|
106
|
+
const data = snapshot?.data;
|
|
107
|
+
if (!data || !isDataSourceLike(data)) {
|
|
108
|
+
setRows([]);
|
|
109
|
+
setColumns([]);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
(async () => {
|
|
113
|
+
try {
|
|
114
|
+
const rawRows = await data.getRows(0, Math.max(0, sampleSize));
|
|
115
|
+
if (cancelled)
|
|
116
|
+
return;
|
|
117
|
+
const normalized = rawRows.map((r) => {
|
|
118
|
+
const obj = r;
|
|
119
|
+
if (obj &&
|
|
120
|
+
typeof obj === "object" &&
|
|
121
|
+
obj.properties &&
|
|
122
|
+
typeof obj.properties === "object") {
|
|
123
|
+
return obj.properties;
|
|
124
|
+
}
|
|
125
|
+
return r;
|
|
126
|
+
});
|
|
127
|
+
setRows(normalized);
|
|
128
|
+
const cols = Array.from(new Set(normalized.flatMap((r) => Object.keys(r ?? {}))));
|
|
129
|
+
setColumns(cols);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
if (cancelled)
|
|
133
|
+
return;
|
|
134
|
+
setRows([]);
|
|
135
|
+
setColumns([]);
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
return () => {
|
|
139
|
+
cancelled = true;
|
|
140
|
+
};
|
|
141
|
+
}, [snapshot?.data, sampleSize]);
|
|
142
|
+
const requestReexecute = useCallback(() => {
|
|
143
|
+
if (udfName)
|
|
144
|
+
reexecute(udfName);
|
|
145
|
+
}, [reexecute, udfName]);
|
|
146
|
+
return {
|
|
147
|
+
loading: snapshot?.isExecutionInProgress ?? false,
|
|
148
|
+
errorMessage: snapshot?.error ?? null,
|
|
149
|
+
isError: Boolean(snapshot?.error),
|
|
150
|
+
columns,
|
|
151
|
+
rows,
|
|
152
|
+
requestReexecute,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Read all values from a UDF column using `{{udf.col}}` query syntax.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* const { values, loading } = useUdfColumnValues("{{my_udf.city}}");
|
|
160
|
+
* // values = ["NYC", "LA", "SF", ...]
|
|
161
|
+
*/
|
|
162
|
+
export function useUdfColumnValues(query, sampleSize = 200) {
|
|
163
|
+
const isValid = isUdfQuery(query);
|
|
164
|
+
const parsed = useMemo(() => (isValid ? parseUdfColumnQuery(query) : null), [isValid, query]);
|
|
165
|
+
const { rows, loading } = useUdfDataFrameSample({
|
|
166
|
+
udfName: parsed?.udfName,
|
|
167
|
+
sampleSize,
|
|
168
|
+
});
|
|
169
|
+
const values = useMemo(() => {
|
|
170
|
+
if (!parsed || !parsed.columnName)
|
|
171
|
+
return [];
|
|
172
|
+
return rows
|
|
173
|
+
.map((row) => row?.[parsed.columnName])
|
|
174
|
+
.filter((v) => v !== undefined && v !== null);
|
|
175
|
+
}, [rows, parsed]);
|
|
176
|
+
return { values, loading: isValid ? loading : false };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Read a single value from a UDF column at a specific index.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* const { value, loading } = useUdfColumnValue("{{my_udf.city[0]}}");
|
|
183
|
+
* // value = "NYC"
|
|
184
|
+
*/
|
|
185
|
+
export function useUdfColumnValue(query, sampleSize = 200) {
|
|
186
|
+
const isValid = isUdfQuery(query);
|
|
187
|
+
const parsed = useMemo(() => (isValid ? parseUdfColumnQuery(query) : null), [isValid, query]);
|
|
188
|
+
const { rows, loading } = useUdfDataFrameSample({
|
|
189
|
+
udfName: parsed?.udfName,
|
|
190
|
+
sampleSize,
|
|
191
|
+
});
|
|
192
|
+
const value = useMemo(() => {
|
|
193
|
+
if (!parsed || !parsed.columnName || parsed.index === undefined)
|
|
194
|
+
return null;
|
|
195
|
+
const row = rows[parsed.index];
|
|
196
|
+
if (!row)
|
|
197
|
+
return null;
|
|
198
|
+
const v = row[parsed.columnName];
|
|
199
|
+
return v !== undefined ? v : null;
|
|
200
|
+
}, [rows, parsed]);
|
|
201
|
+
return { value, loading: isValid ? loading : false };
|
|
202
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type UploadAccessState = {
|
|
2
|
+
status: "idle";
|
|
3
|
+
} | {
|
|
4
|
+
status: "checking";
|
|
5
|
+
} | {
|
|
6
|
+
status: "allowed";
|
|
7
|
+
} | {
|
|
8
|
+
status: "denied";
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Check whether the current user has write access to a destination path
|
|
13
|
+
* (S3, GCS, etc.). Used by the `file-upload` widget to surface a clear
|
|
14
|
+
* error before the user attempts to upload.
|
|
15
|
+
*
|
|
16
|
+
* Returns a state machine value `{ status: "idle" | "checking" | "allowed" |
|
|
17
|
+
* "denied" }` that you can branch on directly in render.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useUploadAccessCheck(destinationPath: string | undefined, enabled: boolean): UploadAccessState;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useFusedWidgetBridge } from "../bridge";
|
|
3
|
+
/**
|
|
4
|
+
* Check whether the current user has write access to a destination path
|
|
5
|
+
* (S3, GCS, etc.). Used by the `file-upload` widget to surface a clear
|
|
6
|
+
* error before the user attempts to upload.
|
|
7
|
+
*
|
|
8
|
+
* Returns a state machine value `{ status: "idle" | "checking" | "allowed" |
|
|
9
|
+
* "denied" }` that you can branch on directly in render.
|
|
10
|
+
*/
|
|
11
|
+
export function useUploadAccessCheck(destinationPath, enabled) {
|
|
12
|
+
const bridge = useFusedWidgetBridge();
|
|
13
|
+
const [state, setState] = useState({ status: "idle" });
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!enabled || !destinationPath?.trim()) {
|
|
16
|
+
setState({ status: "idle" });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
let cancelled = false;
|
|
20
|
+
setState({ status: "checking" });
|
|
21
|
+
bridge.uploads.checkAccess(destinationPath).then((result) => {
|
|
22
|
+
if (cancelled)
|
|
23
|
+
return;
|
|
24
|
+
if (result.ok) {
|
|
25
|
+
setState({ status: "allowed" });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
setState({
|
|
29
|
+
status: "denied",
|
|
30
|
+
message: result.message ?? "Upload access denied.",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return () => {
|
|
35
|
+
cancelled = true;
|
|
36
|
+
};
|
|
37
|
+
}, [bridge, destinationPath, enabled]);
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type SignUrlResult } from "../bridge";
|
|
2
|
+
/** URL schemes that require signing before fetch. */
|
|
3
|
+
export declare const SIGNED_URL_SCHEMES: readonly ["s3://", "gs://", "fd://"];
|
|
4
|
+
/**
|
|
5
|
+
* Manual URL signing — returns a stable `signUrl` callback you can call
|
|
6
|
+
* with any URL. Useful when your component needs to sign URLs imperatively
|
|
7
|
+
* (e.g. on user click). For reactive media src resolution, use `useMediaSrc`.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const { signUrl } = useUrlSigning();
|
|
11
|
+
* const handleDownload = async () => {
|
|
12
|
+
* const { signed } = await signUrl("s3://my-bucket/file.csv");
|
|
13
|
+
* window.location.href = signed;
|
|
14
|
+
* };
|
|
15
|
+
*/
|
|
16
|
+
export declare function useUrlSigning(): {
|
|
17
|
+
signUrl: (url: string) => Promise<SignUrlResult>;
|
|
18
|
+
};
|
|
19
|
+
export interface UseMediaSrcResult {
|
|
20
|
+
/** The resolved `src` URL (signed if needed, with `$param`s substituted). */
|
|
21
|
+
src: string | null;
|
|
22
|
+
/** True while substitution or signing is in flight. */
|
|
23
|
+
loading: boolean;
|
|
24
|
+
error: string | null;
|
|
25
|
+
/** Re-sign — call to refresh an expired URL. */
|
|
26
|
+
refreshSignedUrl: () => Promise<string | null>;
|
|
27
|
+
/** The substituted source URL prior to signing (useful for diagnostics). */
|
|
28
|
+
resolvedSrc: string;
|
|
29
|
+
/** True if the resolved URL requires signing. */
|
|
30
|
+
needsSigning: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Reactively resolve a media `src` URL. Substitutes `$param` tokens first,
|
|
34
|
+
* then signs S3/GCS/FD URLs. Other URL schemes pass through unchanged.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* const { src, loading, error } = useMediaSrc(props.imageUrl);
|
|
38
|
+
* if (loading) return <Spinner />;
|
|
39
|
+
* if (error) return <div>Failed: {error}</div>;
|
|
40
|
+
* return <img src={src ?? ""} alt="" />;
|
|
41
|
+
*/
|
|
42
|
+
export declare function useMediaSrc(srcInput: string | undefined): UseMediaSrcResult;
|