@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
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# @fusedio/widget-sdk
|
|
2
|
+
|
|
3
|
+
React hooks and types for building custom **json-ui components** that run
|
|
4
|
+
inside the [Fused](https://fused.io) workbench canvas.
|
|
5
|
+
|
|
6
|
+
If you want to build a 3rd-party component catalog that the Fused workbench
|
|
7
|
+
can load — a custom chart, a custom input, a domain-specific widget — this
|
|
8
|
+
SDK is the contract your components depend on.
|
|
9
|
+
|
|
10
|
+
> **Status:** pre-1.0. The public hook surface is stabilising but minor
|
|
11
|
+
> breaking changes may still happen between `0.x` releases. The bridge
|
|
12
|
+
> interface (for host implementers) is more volatile and may grow.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @fusedio/widget-sdk
|
|
18
|
+
# or
|
|
19
|
+
bun add @fusedio/widget-sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
React ≥18 is a peer dependency.
|
|
23
|
+
|
|
24
|
+
## The shape of a component
|
|
25
|
+
|
|
26
|
+
Every component receives a single `element` prop. Read your props out of
|
|
27
|
+
`element.props`, return JSX.
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { useFusedParam, type ComponentRenderProps } from "@fusedio/widget-sdk";
|
|
31
|
+
|
|
32
|
+
interface CounterProps {
|
|
33
|
+
param: string;
|
|
34
|
+
label?: string;
|
|
35
|
+
step?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function Counter({ element }: ComponentRenderProps<CounterProps>) {
|
|
39
|
+
const { param, label = "Count", step = 1 } = element.props;
|
|
40
|
+
const { value, setValue } = useFusedParam({ param, defaultValue: 0 });
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<button onClick={() => setValue(value + step)}>
|
|
44
|
+
{label}: {value}
|
|
45
|
+
</button>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
When the user clicks, `setValue` broadcasts the new value to the canvas;
|
|
51
|
+
any connected UDF re-runs with the updated parameter.
|
|
52
|
+
|
|
53
|
+
## What the hooks let you do
|
|
54
|
+
|
|
55
|
+
| Hook | What it does |
|
|
56
|
+
| ------------------------------ | ------------------------------------------------------------------- |
|
|
57
|
+
| `useFusedParam` | Two-way bind a component to a canvas parameter (debounced). |
|
|
58
|
+
| `useCanvasParams` | Read multiple canvas parameter values at once (edge-filtered). |
|
|
59
|
+
| `useParamSubstitution` | Resolve `$param` and `{{udf}}` placeholders inside a template. |
|
|
60
|
+
| `useUdfOutputByName` | Subscribe to a UDF's output, status, error. |
|
|
61
|
+
| `useUdfColumnValue` / `Values` | Pull values out of `{{udf.col}}` / `{{udf.col[idx]}}` queries. |
|
|
62
|
+
| `useUdfDataFrameSample` | Sample rows from a UDF's DataFrame output. |
|
|
63
|
+
| `useDuckDbSqlQuery` | Run a DuckDB-WASM query against UDF parquet outputs in the browser. |
|
|
64
|
+
| `useUrlSigning` / `useMediaSrc` | Sign `s3://`, `gs://`, `fd://` URLs and resolve media sources. |
|
|
65
|
+
| `useUploadAccessCheck` | Pre-flight an upload destination for write access. |
|
|
66
|
+
| `useAllowedSources` | Which UDFs are allowed to broadcast to this node? |
|
|
67
|
+
| `useAllowedUdfNames` | Set of UDF names this node may reference. |
|
|
68
|
+
| `useJsonUiEdgeAnimation` | Animate the canvas edge pellet around custom async work. |
|
|
69
|
+
| `useJsonUiLog` | Write entries to the runtime logs panel. |
|
|
70
|
+
| `useJsonUiUdfInfo` | Current node identity (`udfName`, `udfUniqueId`, `configHash`). |
|
|
71
|
+
|
|
72
|
+
Every hook ships with `@example` blocks in its TypeScript declarations —
|
|
73
|
+
hover any import in your editor for the full signature, defaults, and
|
|
74
|
+
usage notes.
|
|
75
|
+
|
|
76
|
+
## Param flow at a glance
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Component A Component B
|
|
80
|
+
useFusedParam("city") useFusedParam("city")
|
|
81
|
+
│ ▲
|
|
82
|
+
▼ │
|
|
83
|
+
setValue("NYC") │
|
|
84
|
+
│ │
|
|
85
|
+
▼ │
|
|
86
|
+
Fused workbench │
|
|
87
|
+
─ broadcasts on BroadcastChannel ─► edge-filtered routing
|
|
88
|
+
│
|
|
89
|
+
▼
|
|
90
|
+
value = "NYC"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- Values broadcast over a same-origin
|
|
94
|
+
`BroadcastChannel("parameter-updates")`.
|
|
95
|
+
- The workbench filters by canvas edges: a component only **receives**
|
|
96
|
+
values from upstream-connected nodes.
|
|
97
|
+
- `setValue` debounces (300ms default). Use `broadcastNow` to flush
|
|
98
|
+
immediately (e.g. `onMouseUp` of a slider). Use `clearValue` to reset
|
|
99
|
+
and notify the canvas.
|
|
100
|
+
|
|
101
|
+
## Running against the workbench
|
|
102
|
+
|
|
103
|
+
In the deployed Fused workbench your component is loaded as part of a
|
|
104
|
+
**catalog bundle**: a single ESM file built with `esbuild` that the user
|
|
105
|
+
adds to the workbench via *Settings → Custom Catalogs*. A minimal build
|
|
106
|
+
looks like:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
esbuild src/index.ts --bundle --format=esm --outfile=dist/catalog.esm.js \
|
|
110
|
+
--external:react --external:react/jsx-runtime \
|
|
111
|
+
--external:@fusedio/widget-sdk --external:zod
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
These four specifiers are marked `external` because the workbench injects
|
|
115
|
+
a runtime import map that resolves them to its own already-loaded React,
|
|
116
|
+
SDK, and Zod instances — guaranteeing one React instance, one SDK
|
|
117
|
+
instance, one Zod instance across host + every loaded catalog. Without
|
|
118
|
+
that, hook calls hit a different React instance ("invalid hook call"),
|
|
119
|
+
and Zod schemas fail `instanceof` checks inside the workbench's
|
|
120
|
+
`z.toJSONSchema(...)` pass.
|
|
121
|
+
|
|
122
|
+
A starter template (with a local sandbox, hot reload, and a GitHub Action
|
|
123
|
+
that publishes the built bundle) will be linked here once it's published.
|
|
124
|
+
|
|
125
|
+
## Architecture
|
|
126
|
+
|
|
127
|
+
This SDK is a thin shell:
|
|
128
|
+
|
|
129
|
+
- **`FusedWidgetBridge`** is the interface the host implements (canvas
|
|
130
|
+
params, UDF outputs, SQL execution, URL signing, …).
|
|
131
|
+
- **Hooks** read the bridge from `FusedWidgetBridgeContext` and adapt it to
|
|
132
|
+
React via `useSyncExternalStore`.
|
|
133
|
+
- **Pure utilities** (`utils/sql-placeholders.ts`) parse SQL templates with
|
|
134
|
+
no host dependencies.
|
|
135
|
+
|
|
136
|
+
The SDK itself does **no I/O, no fetches, no auth, no storage**. Every
|
|
137
|
+
side effect goes through the bridge — which the host (the Fused workbench
|
|
138
|
+
or your test harness) provides. This is why the same component code runs
|
|
139
|
+
unchanged in the workbench, in a local sandbox, and in any future host.
|
|
140
|
+
|
|
141
|
+
## Implementing a custom host (advanced)
|
|
142
|
+
|
|
143
|
+
Hosting json-ui components outside the Fused workbench:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
import {
|
|
147
|
+
FusedWidgetBridgeContext,
|
|
148
|
+
type FusedWidgetBridge,
|
|
149
|
+
} from "@fusedio/widget-sdk";
|
|
150
|
+
|
|
151
|
+
const myBridge: FusedWidgetBridge = {
|
|
152
|
+
params: { /* … */ },
|
|
153
|
+
udfs: { /* … */ },
|
|
154
|
+
// … everything else
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
<FusedWidgetBridgeContext.Provider value={myBridge}>
|
|
158
|
+
{/* render json-ui components here */}
|
|
159
|
+
</FusedWidgetBridgeContext.Provider>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
You'll need to implement every sub-bridge (`params`, `udfs`, `routing`,
|
|
163
|
+
`sql`, `template`, `uploads`, `edges`, `log`, plus `signUrl` and `node`).
|
|
164
|
+
The exhaustive interface is exported as `FusedWidgetBridge` — hover it in
|
|
165
|
+
your editor for the full type.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
Apache-2.0
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { ParameterMessageType } from "./protocol";
|
|
2
|
+
export interface FusedWidgetBridge {
|
|
3
|
+
/** Canvas parameter state (edge-filtered, two-way). */
|
|
4
|
+
params: ParamBridge;
|
|
5
|
+
/** UDF output data and re-execution. */
|
|
6
|
+
udfs: UdfBridge;
|
|
7
|
+
/** Identity of the current canvas node. */
|
|
8
|
+
node: NodeIdentity;
|
|
9
|
+
/** Edge animation control. */
|
|
10
|
+
edges: EdgeAnimationBridge;
|
|
11
|
+
/** Edge-based routing capabilities. */
|
|
12
|
+
routing: RoutingBridge;
|
|
13
|
+
/** DuckDB SQL execution against UDF Parquet outputs. */
|
|
14
|
+
sql: SqlBridge;
|
|
15
|
+
/** Template rendering for `$param` + `{{udf}}` substitution. */
|
|
16
|
+
template: TemplateBridge;
|
|
17
|
+
/** File-upload access checks. */
|
|
18
|
+
uploads: UploadBridge;
|
|
19
|
+
/** Sign an S3/GCS/FD URL with the current user's access token. */
|
|
20
|
+
signUrl(url: string): Promise<SignUrlResult>;
|
|
21
|
+
/** Per-node logging (visible in the runtime logs panel). */
|
|
22
|
+
log: LogBridge;
|
|
23
|
+
}
|
|
24
|
+
export interface ParamBridge {
|
|
25
|
+
/**
|
|
26
|
+
* Subscribe to changes for a single canvas parameter.
|
|
27
|
+
* Designed for use with React.useSyncExternalStore — callback takes no args.
|
|
28
|
+
* Returns an unsubscribe function.
|
|
29
|
+
*/
|
|
30
|
+
subscribe(param: string, cb: () => void): () => void;
|
|
31
|
+
/** Synchronously read the current edge-filtered value for a param. */
|
|
32
|
+
getSnapshot(param: string): unknown;
|
|
33
|
+
/** Subscribe to changes for *any* of a list of params. */
|
|
34
|
+
subscribeMany(params: readonly string[], cb: () => void): () => void;
|
|
35
|
+
/** Read snapshot for many params at once (edge-filtered). */
|
|
36
|
+
getSnapshotMany(params: readonly string[]): Record<string, unknown>;
|
|
37
|
+
/** Broadcast a parameter value to the canvas (typed). */
|
|
38
|
+
set(param: string, value: unknown, type?: ParameterMessageType): void;
|
|
39
|
+
/** Clear a parameter: send CLEAR for this source. */
|
|
40
|
+
clear(param: string): void;
|
|
41
|
+
}
|
|
42
|
+
export interface UdfBridge {
|
|
43
|
+
/** Subscribe to changes in a UDF's results (output data + execution status). */
|
|
44
|
+
subscribeOutput(udfName: string, cb: () => void): () => void;
|
|
45
|
+
/** Get the current snapshot of a UDF's results. */
|
|
46
|
+
getOutputSnapshot(udfName: string): UdfOutputSnapshot | undefined;
|
|
47
|
+
/** Request the workbench to re-execute a UDF. */
|
|
48
|
+
requestReexecute(udfName: string): void;
|
|
49
|
+
}
|
|
50
|
+
export interface UdfOutputSnapshot {
|
|
51
|
+
/** The UDF result data — TableDataSource, HTML blob, array, etc. */
|
|
52
|
+
data: unknown;
|
|
53
|
+
/** True while the UDF is currently executing. */
|
|
54
|
+
isExecutionInProgress: boolean;
|
|
55
|
+
/** Error message if the most recent execution failed. */
|
|
56
|
+
error?: string;
|
|
57
|
+
/** VFS filename for DuckDB queries (e.g. `"<udfName>.parquet"`). */
|
|
58
|
+
vfsFilename?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface NodeIdentity {
|
|
61
|
+
/** Unique ID of the current canvas node (regenerated on page reload). */
|
|
62
|
+
udfUniqueId?: string;
|
|
63
|
+
/** Human-readable name of the current node (stable across reloads). */
|
|
64
|
+
udfName?: string;
|
|
65
|
+
/** Hash of the current widget JSON config. Changes when the JSON is edited. */
|
|
66
|
+
configHash?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface EdgeAnimationBridge {
|
|
69
|
+
/** Start the edge-animating loading state for the current node. */
|
|
70
|
+
startLoading(): void;
|
|
71
|
+
/** End the loading state — fires the edge pellet on the true→false transition. */
|
|
72
|
+
stopLoading(): void;
|
|
73
|
+
}
|
|
74
|
+
/** Identity of a UDF allowed to broadcast params to the current node. */
|
|
75
|
+
export interface AllowedSource {
|
|
76
|
+
udfUniqueId?: string;
|
|
77
|
+
udfName?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface RoutingBridge {
|
|
80
|
+
/** Subscribe to changes in allowed sources (canvas topology changes). */
|
|
81
|
+
subscribeAllowedSources(cb: () => void): () => void;
|
|
82
|
+
/** Get the set of UDF names this node may reference. `null` = no filtering. */
|
|
83
|
+
getAllowedUdfNames(): Set<string> | null;
|
|
84
|
+
/** Get the allowed source identities for this node. `null` = no filtering. */
|
|
85
|
+
getAllowedSources(): ReadonlyArray<AllowedSource> | null;
|
|
86
|
+
}
|
|
87
|
+
export interface SqlQueryOptions {
|
|
88
|
+
defaultLimit?: number;
|
|
89
|
+
signal?: AbortSignal;
|
|
90
|
+
}
|
|
91
|
+
export interface SqlQueryResult {
|
|
92
|
+
rows: ReadonlyArray<Record<string, unknown>>;
|
|
93
|
+
columns: readonly string[];
|
|
94
|
+
error?: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A reference to a `{{udf}}` or `{{udf?k=v}}` placeholder, used by
|
|
98
|
+
* `bridge.sql.resolveVfsFilenames` to register UDFs (including overrides).
|
|
99
|
+
*
|
|
100
|
+
* `key` is the canonical registry key that consumers should use to look up
|
|
101
|
+
* the resolved filename in the returned Map. Bare `{{udf}}` references use
|
|
102
|
+
* `key === name`; override references use `computePlaceholderKey(name, overrides)`.
|
|
103
|
+
*/
|
|
104
|
+
export interface VfsResolveRef {
|
|
105
|
+
name: string;
|
|
106
|
+
key: string;
|
|
107
|
+
overrides?: Record<string, string>;
|
|
108
|
+
}
|
|
109
|
+
export interface VfsResolveResult {
|
|
110
|
+
/** Map of `ref.key` → resolved VFS filename (e.g. `"my_udf.parquet"`). */
|
|
111
|
+
filenames: Map<string, string>;
|
|
112
|
+
/** Per-key error messages for refs that failed to register. */
|
|
113
|
+
errors?: Map<string, string>;
|
|
114
|
+
}
|
|
115
|
+
export interface SqlBridge {
|
|
116
|
+
/** Execute a SQL query against UDF Parquet outputs via DuckDB. */
|
|
117
|
+
query(sql: string, options?: SqlQueryOptions): Promise<SqlQueryResult>;
|
|
118
|
+
/**
|
|
119
|
+
* Resolve UDF references to VFS filenames; registers any UDFs (and
|
|
120
|
+
* override variants) not yet in VFS. Returns a map keyed by `ref.key`.
|
|
121
|
+
*
|
|
122
|
+
* When called with bare string names, the legacy `Map<name, filename>`
|
|
123
|
+
* shape is preserved for backward compatibility with the previous bridge
|
|
124
|
+
* surface — but new callers should pass `VfsResolveRef[]`.
|
|
125
|
+
*/
|
|
126
|
+
resolveVfsFilenames(refs: readonly VfsResolveRef[] | readonly string[]): Promise<VfsResolveResult | Map<string, string>>;
|
|
127
|
+
}
|
|
128
|
+
export interface TemplateRenderOptions {
|
|
129
|
+
/** When true, leave unresolved `$param` tokens intact rather than replacing with empty string. */
|
|
130
|
+
preserveMissingParams?: boolean;
|
|
131
|
+
/** Optional cancellation signal. */
|
|
132
|
+
signal?: AbortSignal;
|
|
133
|
+
}
|
|
134
|
+
export interface TemplateRenderResult {
|
|
135
|
+
/** The rendered string with `$param` and `{{udf}}` placeholders replaced. */
|
|
136
|
+
value: string;
|
|
137
|
+
/** True if any UDF placeholder is still loading (data not yet available). */
|
|
138
|
+
loading: boolean;
|
|
139
|
+
}
|
|
140
|
+
export interface TemplateBridge {
|
|
141
|
+
/**
|
|
142
|
+
* Asynchronously render a template containing `$param` and `{{udf}}`
|
|
143
|
+
* placeholders. Resolves UDF dependencies, fetches override variants if
|
|
144
|
+
* needed, and stringifies the result.
|
|
145
|
+
*
|
|
146
|
+
* The host owns the rendering machinery (UDF result access, allowed-UDF
|
|
147
|
+
* routing, HTML template node recursion, override fetching). The SDK
|
|
148
|
+
* orchestrates re-runs when params change.
|
|
149
|
+
*/
|
|
150
|
+
render(template: string, paramValues: Record<string, unknown>, options?: TemplateRenderOptions): Promise<TemplateRenderResult>;
|
|
151
|
+
/**
|
|
152
|
+
* Synchronously render a loading placeholder for the template (used to
|
|
153
|
+
* keep the UI populated while async render is in flight). Should not
|
|
154
|
+
* touch any UDFs — only `$param` substitution and best-effort HTML
|
|
155
|
+
* template node substitution from already-available data.
|
|
156
|
+
*/
|
|
157
|
+
renderLoading(template: string, paramValues: Record<string, unknown>, options?: TemplateRenderOptions): string;
|
|
158
|
+
/**
|
|
159
|
+
* Subscribe to events that should cause a re-render: UDF outputs changing,
|
|
160
|
+
* topology shifts, etc. The callback should be invoked any time
|
|
161
|
+
* `render()` could now produce a different result for the *same* inputs.
|
|
162
|
+
*/
|
|
163
|
+
subscribe(cb: () => void): () => void;
|
|
164
|
+
}
|
|
165
|
+
export interface UploadAccessResult {
|
|
166
|
+
ok: boolean;
|
|
167
|
+
/** When ok=false, a human-readable message; otherwise omitted. */
|
|
168
|
+
message?: string;
|
|
169
|
+
}
|
|
170
|
+
export interface UploadBridge {
|
|
171
|
+
/**
|
|
172
|
+
* Check whether the current user has write access to a destination path
|
|
173
|
+
* (S3, GCS, etc.). Used by the `file-upload` widget to surface a clear
|
|
174
|
+
* error before the user attempts to upload.
|
|
175
|
+
*/
|
|
176
|
+
checkAccess(destinationPath: string): Promise<UploadAccessResult>;
|
|
177
|
+
}
|
|
178
|
+
export interface SignUrlResult {
|
|
179
|
+
/** The signed URL (or the original URL if signing was not needed). */
|
|
180
|
+
signed: string;
|
|
181
|
+
/** True if the URL needed signing (false for non-S3/GCS/FD URLs). */
|
|
182
|
+
needsSigning: boolean;
|
|
183
|
+
}
|
|
184
|
+
export type JsonUiLogLevel = "info" | "warn" | "error";
|
|
185
|
+
export interface LogEntry {
|
|
186
|
+
/** Epoch millis of when this entry was created. */
|
|
187
|
+
timestamp: number;
|
|
188
|
+
level: JsonUiLogLevel;
|
|
189
|
+
message: string;
|
|
190
|
+
/** Hash of the widget config that produced this entry (for staleness detection). */
|
|
191
|
+
configHash?: string;
|
|
192
|
+
}
|
|
193
|
+
export interface LogBridge {
|
|
194
|
+
/**
|
|
195
|
+
* Append a log entry for the current node.
|
|
196
|
+
*
|
|
197
|
+
* `configHash` is optional — when provided (by the SDK's `useJsonUiLog`,
|
|
198
|
+
* which reads it from `JsonUiNodeOverrideContext`), the entry is tagged
|
|
199
|
+
* with that hash so nested `JsonUiConfigHashOverride` subtrees emit
|
|
200
|
+
* correctly-scoped entries without rebuilding the bridge. When omitted,
|
|
201
|
+
* the bridge falls back to its own node identity.
|
|
202
|
+
*/
|
|
203
|
+
log(message: string, level?: JsonUiLogLevel, configHash?: string): void;
|
|
204
|
+
/** Subscribe to log changes for a node. */
|
|
205
|
+
subscribeLogs(nodeId: string, cb: () => void): () => void;
|
|
206
|
+
/** Get the log entries snapshot for a node. */
|
|
207
|
+
getLogsSnapshot(nodeId: string): readonly LogEntry[];
|
|
208
|
+
/** Clear all log entries for a node. */
|
|
209
|
+
clearLogs(nodeId: string): void;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Context that carries the FusedWidgetBridge instance.
|
|
213
|
+
* Provided by the workbench's `<JsonUiProvider>` and by the test harness.
|
|
214
|
+
* Catalog authors never interact with this directly — use the hooks.
|
|
215
|
+
*/
|
|
216
|
+
export declare const FusedWidgetBridgeContext: import("react").Context<FusedWidgetBridge | null>;
|
|
217
|
+
/**
|
|
218
|
+
* Internal helper used by all SDK hooks. Throws if used outside a
|
|
219
|
+
* `<FusedWidgetBridgeContext.Provider>` so misconfiguration fails loudly.
|
|
220
|
+
*/
|
|
221
|
+
export declare function useFusedWidgetBridge(): FusedWidgetBridge;
|
|
222
|
+
/**
|
|
223
|
+
* Per-subtree override of the bridge's node identity. When present, SDK hooks
|
|
224
|
+
* like `useJsonUiUdfInfo` and `useJsonUiLog` read these fields instead of
|
|
225
|
+
* `bridge.node`. Lets nested `JsonUiConfigHashOverride` providers tag log
|
|
226
|
+
* entries with their own `configHash` without rebuilding the entire bridge,
|
|
227
|
+
* which would otherwise cascade through every `useSyncExternalStore`
|
|
228
|
+
* subscription in the subtree.
|
|
229
|
+
*
|
|
230
|
+
* Hosts populate this from their JsonUiProvider props.
|
|
231
|
+
*/
|
|
232
|
+
export interface JsonUiNodeOverride {
|
|
233
|
+
udfUniqueId?: string;
|
|
234
|
+
udfName?: string;
|
|
235
|
+
configHash?: string;
|
|
236
|
+
}
|
|
237
|
+
export declare const JsonUiNodeOverrideContext: import("react").Context<JsonUiNodeOverride | null>;
|
|
238
|
+
/**
|
|
239
|
+
* Internal helper that resolves the effective node identity for SDK hooks.
|
|
240
|
+
* Reads `JsonUiNodeOverrideContext` first (per-subtree override), falls back
|
|
241
|
+
* to `bridge.node` (per-provider identity).
|
|
242
|
+
*/
|
|
243
|
+
export declare function useJsonUiNode(): {
|
|
244
|
+
udfUniqueId: string | undefined;
|
|
245
|
+
udfName: string | undefined;
|
|
246
|
+
configHash: string | undefined;
|
|
247
|
+
};
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FusedWidgetBridge — the dependency-injection interface between SDK hooks
|
|
3
|
+
* and the host environment (workbench, test harness, mobile app, etc).
|
|
4
|
+
*
|
|
5
|
+
* The SDK hooks (`useFusedParam`, `useParamSubstitution`, `useUdfOutputByName`,
|
|
6
|
+
* `useDuckDbSqlQuery`, …) read this bridge from `FusedWidgetBridgeContext`
|
|
7
|
+
* and delegate all state management to it. Catalog component authors only
|
|
8
|
+
* call hooks; they never touch the bridge directly.
|
|
9
|
+
*
|
|
10
|
+
* Hosts implement this interface to inject their own state stores:
|
|
11
|
+
* - The Fused workbench wires Jotai atoms, DuckDB, fetcher, log atom.
|
|
12
|
+
* - The catalog-template test harness uses in-memory Map storage.
|
|
13
|
+
* - Any future host (mobile, embedded) can implement their own.
|
|
14
|
+
*/
|
|
15
|
+
import { createContext, useContext } from "react";
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// React context
|
|
18
|
+
// ============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Context that carries the FusedWidgetBridge instance.
|
|
21
|
+
* Provided by the workbench's `<JsonUiProvider>` and by the test harness.
|
|
22
|
+
* Catalog authors never interact with this directly — use the hooks.
|
|
23
|
+
*/
|
|
24
|
+
export const FusedWidgetBridgeContext = createContext(null);
|
|
25
|
+
FusedWidgetBridgeContext.displayName = "FusedWidgetBridgeContext";
|
|
26
|
+
/**
|
|
27
|
+
* Internal helper used by all SDK hooks. Throws if used outside a
|
|
28
|
+
* `<FusedWidgetBridgeContext.Provider>` so misconfiguration fails loudly.
|
|
29
|
+
*/
|
|
30
|
+
export function useFusedWidgetBridge() {
|
|
31
|
+
const bridge = useContext(FusedWidgetBridgeContext);
|
|
32
|
+
if (!bridge) {
|
|
33
|
+
throw new Error("useFusedWidgetBridge: no FusedWidgetBridgeContext provider in the tree. " +
|
|
34
|
+
"Wrap your components with the workbench's <JsonUiProvider> or the " +
|
|
35
|
+
"test harness's <FusedWidgetBridgeContext.Provider value={createTestBridge()}>.");
|
|
36
|
+
}
|
|
37
|
+
return bridge;
|
|
38
|
+
}
|
|
39
|
+
export const JsonUiNodeOverrideContext = createContext(null);
|
|
40
|
+
JsonUiNodeOverrideContext.displayName = "JsonUiNodeOverrideContext";
|
|
41
|
+
/**
|
|
42
|
+
* Internal helper that resolves the effective node identity for SDK hooks.
|
|
43
|
+
* Reads `JsonUiNodeOverrideContext` first (per-subtree override), falls back
|
|
44
|
+
* to `bridge.node` (per-provider identity).
|
|
45
|
+
*/
|
|
46
|
+
export function useJsonUiNode() {
|
|
47
|
+
const bridge = useFusedWidgetBridge();
|
|
48
|
+
const override = useContext(JsonUiNodeOverrideContext);
|
|
49
|
+
if (override) {
|
|
50
|
+
return {
|
|
51
|
+
udfUniqueId: override.udfUniqueId ?? bridge.node.udfUniqueId,
|
|
52
|
+
udfName: override.udfName ?? bridge.node.udfName,
|
|
53
|
+
configHash: override.configHash ?? bridge.node.configHash,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
udfUniqueId: bridge.node.udfUniqueId,
|
|
58
|
+
udfName: bridge.node.udfName,
|
|
59
|
+
configHash: bridge.node.configHash,
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var ir="parameter-updates",ae=(o=>(o.PARAM="param",o.RANGE="range",o.VIEWPORT="viewport",o.CLEAR="clear",o))(ae||{});function ur(e){if(typeof e!="object"||e===null)return!1;let n=e;return typeof n.type=="string"&&Object.values(ae).includes(n.type)&&typeof n.parameter=="string"&&"values"in n}import{createContext as ye,useContext as Re}from"react";var be=ye(null);be.displayName="FusedWidgetBridgeContext";function y(){let e=Re(be);if(!e)throw new Error("useFusedWidgetBridge: no FusedWidgetBridgeContext provider in the tree. Wrap your components with the workbench's <JsonUiProvider> or the test harness's <FusedWidgetBridgeContext.Provider value={createTestBridge()}>.");return e}var he=ye(null);he.displayName="JsonUiNodeOverrideContext";function V(){let e=y(),n=Re(he);return n?{udfUniqueId:n.udfUniqueId??e.node.udfUniqueId,udfName:n.udfName??e.node.udfName,configHash:n.configHash??e.node.configHash}:{udfUniqueId:e.node.udfUniqueId,udfName:e.node.udfName,configHash:e.node.configHash}}import{createContext as nn,useCallback as ve,useContext as rn,useRef as we,useSyncExternalStore as tn}from"react";function dr(){let e=new Map,n=new Set,r=t=>{n.forEach(o=>{o.names.has(t)&&o.cb()})};return{get(t){return e.get(t)},getSnapshot(t){let o={};for(let i of t)e.has(i)&&(o[i]=e.get(i));return o},getAll(){let t={};return e.forEach((o,i)=>{t[i]=o}),t},setField(t,o){e.has(t)&&Object.is(e.get(t),o)||(e.set(t,o),r(t))},removeField(t){e.has(t)&&(e.delete(t),r(t))},subscribe(t,o){let i={names:new Set(t),cb:o};return n.add(i),()=>{n.delete(i)}}}}var Ue=nn({store:null,isInForm:!1});Ue.displayName="JsonUiFormContext";function on(){return rn(Ue)}var xe=Object.freeze({});function j(e){let{store:n,isInForm:r}=on(),t=un(e),o=ve(f=>n?n.subscribe(t,f):()=>{},[n,t]),i=we(xe),l=ve(()=>{if(!n)return xe;let f=n.getSnapshot(t),a=i.current;return sn(a,f)?a:(i.current=f,f)},[n,t]),s=tn(o,l,l);return{inForm:r,values:s}}function sn(e,n){if(e===n)return!0;let r=Object.keys(e),t=Object.keys(n);if(r.length!==t.length)return!1;for(let o of r)if(!Object.is(e[o],n[o]))return!1;return!0}function un(e){let n=we(e),r=n.current;return r!==e&&(r.length!==e.length||r.some((t,o)=>t!==e[o]))&&(n.current=e),n.current}function an(e){return e}function ln(e){return e}import{useCallback as X,useEffect as J,useMemo as Y,useRef as I,useState as cn}from"react";function fn({param:e,debounceMs:n=300,readOnly:r=!1,defaultValue:t,broadcastDefaultValue:o=!0,validate:i,preprocess:l}){let s=y(),{configHash:f}=V(),a=!!e,R=(u,c)=>{if(!e)return;if(u==="Cleared"){s.log.log(`Cleared param "${e}"`,"info",f);return}let d=JSON.stringify(c),N=d&&d.length>100?d.slice(0,100)+"\u2026":d;s.log.log(`${u} param "${e}" = ${N}`,"info",f)},p=Y(()=>l??gn(t),[l]),C=Y(()=>i??dn(t),[i]),g=Y(()=>e?[e]:[],[e]),{inForm:b,values:h}=j(g),P=e?h[e]:void 0,T=Y(()=>{if(!(!a||!e))return s.params.getSnapshot(e)},[s,a,e]),v=()=>{let u=b&&P!==void 0?P:T;if(u==null)return t;let c=p(u);return C(c)?c:t},[D,w]=cn(v),x=I(D);x.current=D;let m=I(null),A=I(!1),L=I(!1),F=I({enabled:a,param:e});F.current={enabled:a,param:e};let E=I(s);E.current=s;let Q=I(e);J(()=>{let u=Q.current;Q.current=e,u&&u!==e&&s.params.clear(u)},[s,e]),J(()=>{if(!a||!e||A.current)return;let u=b&&P!==void 0?P:s.params.getSnapshot(e);if(u==null)return;let c=p(u);c!==x.current&&C(c)&&(w(c),R("Received",u))},[s,e,a,b,P,p,C]),J(()=>!a||!e?void 0:s.params.subscribe(e,()=>{if(A.current)return;let c=s.params.getSnapshot(e);if(c==null)return;let d=p(c);d!==x.current&&C(d)&&(w(d),R("Received",c))}),[s,e,a,p,C]);let M=X(u=>{!a||!e||(s.params.set(e,u,"param"),s.edges.stopLoading(),R("Broadcast",u))},[s,a,e]),q=X(()=>{!a||r||(m.current&&(clearTimeout(m.current),m.current=null),s.edges.startLoading(),M(x.current),A.current=!1)},[s,M,a,r]),U=X(u=>{m.current&&(clearTimeout(m.current),m.current=null),w(u),x.current=u,A.current=!1,!(!a||!e||r)&&(s.params.clear(e),s.edges.stopLoading(),R("Cleared",null))},[s,a,e,r]);J(()=>{L.current=!1},[e,a]),J(()=>{if(!a||!e||r||!o||L.current)return;let u=s.params.getSnapshot(e);if(u!=null){L.current=!0;return}if(x.current===""){L.current=!0;return}M(x.current),L.current=!0},[s,a,e,r,o,M]);let _=X(u=>{w(u),!(!a||r)&&(A.current=!0,s.edges.startLoading(),m.current&&clearTimeout(m.current),m.current=setTimeout(()=>{M(u),A.current=!1},n))},[s,M,n,a,r]);return J(()=>()=>{m.current&&clearTimeout(m.current);let{enabled:u,param:c}=F.current;!u||!c||E.current.params.clear(c)},[]),{value:D,setValue:_,broadcastNow:q,clearValue:U}}function dn(e){return typeof e=="string"?(n=>typeof n=="string"):typeof e=="number"?(n=>typeof n=="number"):typeof e=="boolean"?(n=>typeof n=="boolean"):Array.isArray(e)?(n=>Array.isArray(n)):e!==null&&typeof e=="object"?(n=>n!==null&&typeof n=="object"&&!Array.isArray(n)):(n=>!0)}function gn(e){return typeof e=="string"?n=>typeof n=="string"?n:Array.isArray(n)?n.join(","):n!==null&&typeof n=="object"?JSON.stringify(n):String(n):n=>n}import{useCallback as ke,useRef as Pe,useSyncExternalStore as pn}from"react";var Ce=Object.freeze({});function Z(e){let n=y(),r=Sn(e),t=ke(l=>r.length===0?()=>{}:n.params.subscribeMany(r,l),[n,r]),o=Pe(Ce),i=ke(()=>{if(r.length===0)return Ce;let l=n.params.getSnapshotMany(r),s=o.current;return mn(s,l)?s:(o.current=l,l)},[n,r]);return pn(t,i,i)}function mn(e,n){if(e===n)return!0;let r=Object.keys(e),t=Object.keys(n);if(r.length!==t.length)return!1;for(let o of r)if(!Object.is(e[o],n[o]))return!1;return!0}function Sn(e){let n=Pe(e),r=n.current;return r!==e&&(r.length!==e.length||r.some((t,o)=>t!==e[o]))&&(n.current=e),n.current}import{useCallback as le,useRef as yn,useSyncExternalStore as Rn}from"react";function bn(){let e=y(),n=le(l=>e.routing.subscribeAllowedSources(l),[e]),r=yn(null),t=le(()=>{let l=e.routing.getAllowedSources(),s=r.current;return hn(s,l)?s:(r.current=l,l)},[e]),o=Rn(n,t,t),i=le((l,s)=>!o||o.length===0||!l&&!s?!0:o.some(f=>f.udfUniqueId&&f.udfUniqueId===l||f.udfName&&f.udfName===s),[o]);return{allowedSources:o,isAllowedSource:i}}function hn(e,n){return e===n?!0:e===null||n===null||e.length!==n.length?!1:e.every((r,t)=>r.udfUniqueId===n[t].udfUniqueId&&r.udfName===n[t].udfName)}import{useCallback as Ee,useRef as vn,useSyncExternalStore as xn}from"react";function wn(){let e=y(),n=Ee(o=>e.routing.subscribeAllowedSources(o),[e]),r=vn(null),t=Ee(()=>{let o=e.routing.getAllowedUdfNames(),i=r.current;return Un(i,o)?i:(r.current=o,o)},[e]);return xn(n,t,t)}function Un(e,n){if(e===n)return!0;if(e===null||n===null||e.size!==n.size)return!1;for(let r of e)if(!n.has(r))return!1;return!0}import{useEffect as kn,useMemo as $,useRef as Cn,useState as Ae,useSyncExternalStore as Pn,useCallback as Le}from"react";var Fe=/\$([a-zA-Z_][a-zA-Z0-9_]*)/g,En=/\{\{[\s\S]*?\}\}/,Te=/\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\b/g;function ce(e,n={}){let r=e??"",t=n.preserveMissingParams??!1,o=y(),i=$(()=>An(r),[r]),l=Z(i),{inForm:s,values:f}=j(i),a=$(()=>s?{...l,...f}:l,[l,f,s]),R=$(()=>En.test(r),[r]),p=$(()=>R?"":Ln(r,a,t),[r,a,t,R]),C=$(()=>{if(!R)return[];let w=new Set,x=[];Te.lastIndex=0;let m;for(;(m=Te.exec(r))!==null;)w.has(m[1])||(w.add(m[1]),x.push(m[1]));return x},[r,R]),g=Fn(o,C),[b,h]=Ae(()=>({key:"",value:""})),[P,T]=Ae(!1),v=$(()=>JSON.stringify({template:r,paramValues:a,preserveMissingParams:t,tick:g}),[r,a,t,g]);return kn(()=>{if(!R){T(!1);return}let w=!1,x=new AbortController;return T(!0),o.template.render(r,a,{preserveMissingParams:t,signal:x.signal}).then(m=>{w||(h(A=>A.key===v&&A.value===m.value?A:{key:v,value:m.value}),T(m.loading))},m=>{w||m?.name!=="AbortError"&&T(!1)}),()=>{w=!0,x.abort()}},[o,R,r,a,t,v]),{value:$(()=>{if(!R)return p;if(b.key===v)return b.value;try{return o.template.renderLoading(r,a,{preserveMissingParams:t})}catch{return r}},[o,R,p,b,v,r,a,t]),loading:R?P:!1}}function An(e){let n=[],r=new Set;for(let t of e.matchAll(Fe)){let o=t[1];r.has(o)||(r.add(o),n.push(o))}return n}function Ln(e,n,r){return e.replace(Fe,(t,o)=>{let i=n[o];return i==null?r?t:"":Tn(i)})}function Tn(e){return e==null?"":typeof e=="string"?e:typeof e=="number"||typeof e=="boolean"?String(e):JSON.stringify(e)??""}function Fn(e,n){let r=Cn(0),t=n.slice().sort().join("|"),o=Le(l=>{let s=()=>{r.current+=1,l()},f=[e.template.subscribe(s)];for(let a of n)f.push(e.udfs.subscribeOutput(a,s));return()=>f.forEach(a=>a())},[e,t]),i=Le(()=>r.current,[]);return Pn(o,i,i)}import{useCallback as ee,useEffect as Dn,useMemo as ne,useRef as On,useState as De,useSyncExternalStore as Nn}from"react";function Oe(e){let n=y(),r=ee(i=>e?n.udfs.subscribeOutput(e,i):()=>{},[n,e]),t=On(void 0),o=ee(()=>{if(!e)return;let i=n.udfs.getOutputSnapshot(e),l=t.current;return Mn(l,i)?l:(t.current=i,i)},[n,e]);return Nn(r,o,o)}function Ne(){let e=y();return ee(n=>e.udfs.requestReexecute(n),[e])}function Mn(e,n){return e===n?!0:!e||!n?!1:e.data===n.data&&e.isExecutionInProgress===n.isExecutionInProgress&&e.error===n.error&&e.vfsFilename===n.vfsFilename}var Me=/^\{\{(\w+)\.(\w+)(?:\[(\d+)\])?\}\}$/;function re(e){return!e||typeof e!="string"?!1:Me.test(e)}function fe(e){if(!re(e))return null;let n=e.match(Me);if(!n)return null;let[,r,t,o]=n,i=o!==void 0?parseInt(o,10):void 0;return{udfName:r,columnName:t,index:i}}function Bn(e){return!!e&&typeof e=="object"&&typeof e.getRows=="function"}function de({udfName:e,sampleSize:n=200}){let r=Oe(e),t=Ne(),[o,i]=De([]),[l,s]=De([]);Dn(()=>{let a=!1,R=r?.data;if(!R||!Bn(R)){i([]),s([]);return}return(async()=>{try{let p=await R.getRows(0,Math.max(0,n));if(a)return;let C=p.map(b=>{let h=b;return h&&typeof h=="object"&&h.properties&&typeof h.properties=="object"?h.properties:b});i(C);let g=Array.from(new Set(C.flatMap(b=>Object.keys(b??{}))));s(g)}catch{if(a)return;i([]),s([])}})(),()=>{a=!0}},[r?.data,n]);let f=ee(()=>{e&&t(e)},[t,e]);return{loading:r?.isExecutionInProgress??!1,errorMessage:r?.error??null,isError:!!r?.error,columns:l,rows:o,requestReexecute:f}}function qn(e,n=200){let r=re(e),t=ne(()=>r?fe(e):null,[r,e]),{rows:o,loading:i}=de({udfName:t?.udfName,sampleSize:n});return{values:ne(()=>!t||!t.columnName?[]:o.map(s=>s?.[t.columnName]).filter(s=>s!=null),[o,t]),loading:r?i:!1}}function _n(e,n=200){let r=re(e),t=ne(()=>r?fe(e):null,[r,e]),{rows:o,loading:i}=de({udfName:t?.udfName,sampleSize:n});return{value:ne(()=>{if(!t||!t.columnName||t.index===void 0)return null;let s=o[t.index];if(!s)return null;let f=s[t.columnName];return f!==void 0?f:null},[o,t]),loading:r?i:!1}}import{useCallback as Ke,useEffect as z,useMemo as O,useRef as Hn,useState as B}from"react";import{useCallback as te,useMemo as Qn,useRef as ge,useSyncExternalStore as In}from"react";var Be=Object.freeze([]);function oe(){let e=y(),{configHash:n}=V(),r=ge(e);r.current=e;let t=ge(n);t.current=n;let o=te((i,l="info")=>{r.current.log.log(i,l,t.current)},[]);return Qn(()=>({log:o}),[o])}function $n(e){let n=y(),r=te(i=>e?n.log.subscribeLogs(e,i):()=>{},[n,e]),t=ge(Be),o=te(()=>{if(!e)return Be;let i=n.log.getLogsSnapshot(e);return i===t.current?t.current:(t.current=i,i)},[n,e]);return In(r,o,o)}function Vn(e){let n=y();return te(()=>{e&&n.log.clearLogs(e)},[n,e])}function se(){let e=y();return{startLoading:e.edges.startLoading,stopLoading:e.edges.stopLoading}}var pe=/\$([a-zA-Z_][a-zA-Z0-9_]*)/g,qe=/\{\{(\w+)(?:\?([^}]*))?\}\}/g,me=/'((?:s3|gs|fd):\/\/[^'\n]+)'/g;function jn(e){return e==null?"''":typeof e=="number"&&!Number.isNaN(e)?String(e):typeof e=="boolean"?e?"TRUE":"FALSE":`'${String(e).replace(/'/g,"''")}'`}function Jn(e,n){let r=!1;for(let t=0;t<n;t++)if(e[t]==="'"){if(r&&e[t+1]==="'"){t++;continue}r=!r}return r}function _e(e,n){return e.replace(pe,(r,t,o)=>{let i=n[t],l=i==null?"":String(i);return Jn(e,o)?l.replace(/'/g,"''"):jn(i)})}function Qe(e){let n=new Set,r=[];pe.lastIndex=0;for(let t of e.matchAll(pe)){let o=t[1];n.has(o)||(n.add(o),r.push(o))}return r}function Wn(e){if(!e)return null;let n={},r=!1;for(let t of e.split(/[&,]/)){if(!t)continue;let o=t.indexOf("=");if(o===-1)continue;let i=t.slice(0,o),l=t.slice(o+1),s,f;try{s=decodeURIComponent(i),f=decodeURIComponent(l)}catch{s=i,f=l}s&&(n[s]=f,r=!0)}return r?n:null}function Ie(e){let n=[],r;for(qe.lastIndex=0;(r=qe.exec(e))!==null;){let[t,o,i]=r;n.push({match:t,name:o,overrides:Wn(i),start:r.index,end:r.index+t.length})}return n}function $e(e){if(!e)return[];let n=new Set,r=[];me.lastIndex=0;let t;for(;(t=me.exec(e))!==null;){let o=t[1];n.has(o)||(n.add(o),r.push(o))}return r}function Ve(e,n){return e&&e.replace(me,(r,t)=>{let o=n[t];return o?`'${o}'`:r})}var je=/^\$([a-zA-Z_][a-zA-Z0-9_]*)$/;function Je(e){let n=e.trim(),r=je.exec(n);return r?r[1]:null}function We(e,n){let r=je.exec(e);if(!r)return{value:e,unresolved:!1};let t=r[1];if(!(t in n))return{value:e,unresolved:!0};let o=n[t];return o==null?{value:"",unresolved:!1}:{value:String(o),unresolved:!1}}function zn(e){return Object.keys(e).sort().map(n=>`${n}=${e[n]}`).join("&")}function ze(e,n){return n?`${e}#${zn(n)}`:e}var Ze=500,W=Object.freeze([]),G=Object.freeze([]);function He(e,n){if(/\bLIMIT\b/i.test(e))return e;let r=e.trimEnd();return`${r.endsWith(";")?r.slice(0,-1):r} LIMIT ${n}`}function Kn(e){return`"${e.replace(/"/g,'""')}"`}function Zn(e,n,r,t,o,i){let l=e.match(/^\$([a-zA-Z_][a-zA-Z0-9_]*)$/);if(l){let f=o[l[1]];return f==null?"":He(String(f),i)}let s=e;for(let f=n.length-1;f>=0;f--){let{raw:a,key:R,resolvedOverrides:p}=n[f],g=p===null&&t?t[a.name]:void 0,b;g?b=Kn(g.relationName):b=`'${r.get(R)??`${a.name}.parquet`}'`,s=s.slice(0,a.start)+b+s.slice(a.end)}return s=He(s,i),_e(s,o)}function Ge({sql:e,enabled:n=!0,maxRows:r=Ze,sourceOverrides:t}){let o=y(),{startLoading:i,stopLoading:l}=se(),{log:s}=oe(),[f,a]=B(""),[R,p]=B(!1),[C,g]=B(null),[b,h]=B(0),P=O(()=>e?Ie(e):[],[e]),T=O(()=>t?P.filter(u=>u.overrides!==null||!t[u.name]):P,[P,t]),v=O(()=>{let u=new Set;for(let c of T)if(c.overrides)for(let d of Object.values(c.overrides)){let N=Je(d);N&&u.add(N)}return Array.from(u)},[T]),D=O(()=>e?Qe(e):[],[e]),w=O(()=>{let u=new Set,c=[];for(let d of D)u.has(d)||(u.add(d),c.push(d));for(let d of v)u.has(d)||(u.add(d),c.push(d));return c},[D,v]),x=Z(w),{inForm:m,values:A}=j(w),L=O(()=>m?{...x,...A}:x,[m,x,A]),F=O(()=>T.map(u=>{if(!u.overrides)return{raw:u,key:u.name,resolvedOverrides:null,unresolved:!1};let c={},d=!1;for(let[N,H]of Object.entries(u.overrides)){let S=We(H,L);S.unresolved&&(d=!0),c[N]=S.value}return{raw:u,key:ze(u.name,c),resolvedOverrides:c,unresolved:d}}),[T,L]),E=O(()=>{let u=new Set,c=[];for(let d of F)d.unresolved||u.has(d.key)||(u.add(d.key),c.push({name:d.raw.name,key:d.key,overrides:d.resolvedOverrides??void 0}));return c},[F]),Q=O(()=>E.map(u=>`${u.key}|${u.name}|${u.overrides?Object.entries(u.overrides).sort(([c],[d])=>c.localeCompare(d)).map(([c,d])=>`${c}=${d}`).join(","):""}`).join(`
|
|
2
|
+
`),[E]),M=Ke(()=>{h(u=>u+1)},[]);z(()=>{if(!n||E.length===0)return;let u=E.map(c=>o.udfs.subscribeOutput(c.name,()=>{h(d=>d+1)}));return()=>{u.forEach(c=>c())}},[o,n,E]),z(()=>{R?i():l()},[R,i,l]);let q=O(()=>{if(!t)return null;for(let u of P){if(u.overrides!==null)continue;let c=t[u.name];if(c?.error)return c.error}return null},[P,t]),U=O(()=>t?P.some(u=>u.overrides===null&&t[u.name]?.loading):!1,[P,t]),_=F.some(u=>u.unresolved);return z(()=>{if(!n||!e){a(""),p(!1),g(null);return}if(q){a(""),g(q),p(!1),s(`SQL preprocessing: ${q}`,"error");return}if(U||_){a(""),g(null),p(!0);return}let u=!1;return p(!0),g(null),(async()=>{let c=new Map,d;if(E.length>0)try{let S=await o.sql.resolveVfsFilenames(E);if(u)return;if(S instanceof Map)for(let k of F){if(k.resolvedOverrides)continue;let K=S.get(k.raw.name);K&&c.set(k.key,K)}else c=S.filenames,d=S.errors}catch(S){if(u)return;let k=S instanceof Error?S.message:typeof S=="string"?S:"VFS registration failed";a(""),g(k),p(!1),s(`SQL preprocessing: ${k}`,"error");return}if(d)for(let S of F){let k=d.get(S.key);if(k){if(u)return;a(""),g(k),p(!1),s(`SQL preprocessing: ${k}`,"error");return}}for(let S of F)if(!(S.unresolved||S.resolvedOverrides===null&&t?.[S.raw.name]!==void 0)&&!c.has(S.key)){if(u)return;a(""),g(null),p(!0);return}let N;try{N=Zn(e,F,c,t,L,r)}catch(S){if(u)return;let k=S instanceof Error?S.message:typeof S=="string"?S:"SQL preprocessing failed";a(""),g(k),p(!1),s(`SQL preprocessing failed: ${k}`,"error");return}let H=$e(N);if(H.length===0){if(u)return;a(N),g(null),p(!1),s("SQL preprocessing completed");return}try{let S={},k=await Promise.all(H.map(ue=>o.signUrl(ue)));if(u)return;H.forEach((ue,en)=>{S[ue]=k[en].signed});let K=Ve(N,S);a(K),g(null),p(!1),s("SQL preprocessing completed")}catch(S){if(u)return;let k=S instanceof Error?S.message:typeof S=="string"?S:"URL signing failed";a(""),g(k),p(!1),s(`SQL preprocessing failed: ${k}`,"error")}})(),()=>{u=!0}},[o,n,e,Q,F,L,t,q,U,_,r,b,s,E]),{processedSql:f,loading:R,error:C,refetch:M}}function Gn({sql:e,enabled:n=!0,maxRows:r=Ze,sourceOverrides:t}){let o=y(),{startLoading:i,stopLoading:l}=se(),{log:s}=oe(),[f,a]=B(W),[R,p]=B(G),[C,g]=B(!1),[b,h]=B(null),[P,T]=B(0),{processedSql:v,loading:D,error:w,refetch:x}=Ge({sql:e,enabled:n,maxRows:r,sourceOverrides:t}),m=Hn(""),A=n&&!!e&&!!v&&!D&&!w&&v!==m.current,L=D||C||A;z(()=>{L?i():l()},[L,i,l]);let F=Ke(()=>{m.current="",x(),T(E=>E+1)},[x]);return z(()=>{if(!n||!e){m.current="",a(W),p(G),g(!1),h(null);return}if(w){m.current="",h(w),a(W),p(G),g(!1);return}if(D||!v){m.current="",g(!1);return}let E=!1,Q=new AbortController;m.current=v,g(!0),h(null);let M=v.length>120?v.slice(0,120)+"\u2026":v;s(`SQL query started: ${M}`);let q=performance.now();return o.sql.query(v,{signal:Q.signal}).then(U=>{if(E)return;let _=Math.round(performance.now()-q);if(U.error){a(W),p(G),h(U.error),g(!1),s(`SQL failed (${_}ms): ${U.error}`,"error");return}a(U.rows.length===0?W:U.rows),p(U.columns),h(null),g(!1),s(`SQL completed: ${U.rows.length} row${U.rows.length!==1?"s":""} in ${_}ms`)},U=>{if(E||U?.name==="AbortError")return;let _=Math.round(performance.now()-q),u=U instanceof Error?U.message:typeof U=="string"?U:"SQL query failed";h(u),a(W),p(G),g(!1),s(`SQL failed (${_}ms): ${u}`,"error")}),()=>{E=!0,Q.abort()}},[o,n,e,v,D,w,P,s]),{rows:f,columns:R,loading:L,error:b,refetch:F}}function Xn(e,n=!0){let r=y(),[t,o]=B({filenames:new Map,loading:!1}),i=O(()=>e.slice().sort().join("|"),[e]);return z(()=>{if(!n||e.length===0){o({filenames:new Map,loading:!1});return}let l=!1;return o(s=>({...s,loading:!0,error:void 0})),r.sql.resolveVfsFilenames(e).then(s=>{if(l)return;let f=s instanceof Map?s:s.filenames;o({filenames:f,loading:!1})},s=>{l||o({filenames:new Map,loading:!1,error:s instanceof Error?s.message:String(s)})}),()=>{l=!0}},[r,i,n]),t}import{useCallback as Xe,useEffect as Yn,useState as ie}from"react";var Ye=["s3://","gs://","fd://"];function Se(e){return Ye.some(n=>e.startsWith(n))}function er(){let e=y();return{signUrl:Xe(r=>e.signUrl(r),[e])}}function nr(e){let n=y(),{value:r,loading:t}=ce(e),[o,i]=ie(null),[l,s]=ie(null),[f,a]=ie(!1),[R,p]=ie(0);Yn(()=>{if(t)return;if(!r){i(null),s(null),a(!1);return}if(!Se(r)){i(r),s(null),a(!1);return}let g=!1;return a(!0),s(null),n.signUrl(r).then(({signed:b})=>{g||i(b??r)}).catch(b=>{g||(s(b instanceof Error?b.message:"Failed to load media"),i(null))}).finally(()=>{g||a(!1)}),()=>{g=!0}},[n,t,r,R]);let C=Xe(async()=>{if(!r||!Se(r))return r??null;let{signed:g}=await n.signUrl(r),b=g??r;return i(b),s(null),p(h=>h+1),b},[n,r]);return{src:o,loading:t||f,error:l,refreshSignedUrl:C,resolvedSrc:r,needsSigning:!!(r&&Se(r))}}import{useEffect as rr,useState as tr}from"react";function or(e,n){let r=y(),[t,o]=tr({status:"idle"});return rr(()=>{if(!n||!e?.trim()){o({status:"idle"});return}let i=!1;return o({status:"checking"}),r.uploads.checkAccess(e).then(l=>{i||(l.ok?o({status:"allowed"}):o({status:"denied",message:l.message??"Upload access denied."}))}),()=>{i=!0}},[r,e,n]),t}function sr(){return V()}export{Ue as FormContext,be as FusedWidgetBridgeContext,he as JsonUiNodeOverrideContext,ir as PARAMETER_BROADCAST_CHANNEL,ae as ParameterMessageType,me as SIGNABLE_URL_LITERAL_REGEX,Ye as SIGNED_URL_SCHEMES,pe as SQL_PARAM_REGEX,qe as SQL_SOURCE_PLACEHOLDER_REGEX,zn as canonicalOverrideKey,ze as computePlaceholderKey,dr as createFormParamsStore,ln as defineCatalog,an as defineComponent,jn as escapeSqlValue,$e as extractSignableUrls,Qe as extractSqlParams,Je as getDollarRefName,ur as isStandardMessage,re as isUdfQuery,Wn as parseOverridesString,Ie as parseSqlUdfPlaceholders,fe as parseUdfColumnQuery,We as resolveOverrideValue,Ve as rewriteSignedUrls,_e as substituteSqlParams,bn as useAllowedSources,wn as useAllowedUdfNames,Z as useCanvasParams,Gn as useDuckDbSqlQuery,Ge as useDuckDbSqlQueryPreprocessing,on as useFormContext,j as useFormParams,fn as useFusedParam,y as useFusedWidgetBridge,se as useJsonUiEdgeAnimation,oe as useJsonUiLog,Vn as useJsonUiLogClear,$n as useJsonUiLogs,V as useJsonUiNode,sr as useJsonUiUdfInfo,nr as useMediaSrc,ce as useParamSubstitution,Ne as useRequestUdfReexecute,_n as useUdfColumnValue,qn as useUdfColumnValues,de as useUdfDataFrameSample,Oe as useUdfOutputByName,or as useUploadAccessCheck,er as useUrlSigning,Xn as useVfsRegistration};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CatalogComponentDefinition } from "./define-component";
|
|
2
|
+
/**
|
|
3
|
+
* The single default export every catalog bundle ships:
|
|
4
|
+
*
|
|
5
|
+
* export default defineCatalog({
|
|
6
|
+
* components: { "kebab-key": defineComponent({...}), ... },
|
|
7
|
+
* skill, // string imported from "../SKILL.md" (esbuild text loader)
|
|
8
|
+
* summary, // ≤120-char headline; REQUIRED when skill is present
|
|
9
|
+
* });
|
|
10
|
+
*
|
|
11
|
+
* `skill` and `summary` are co-required at the **type level** via function
|
|
12
|
+
* overloads — TypeScript rejects `{components, skill}` and `{components,
|
|
13
|
+
* summary}` because neither overload matches. There is intentionally NO
|
|
14
|
+
* runtime validation here:
|
|
15
|
+
*
|
|
16
|
+
* - Bundle-time throws would block the entire catalog load over a UI
|
|
17
|
+
* concern (e.g. a too-long summary). The workbench loader instead
|
|
18
|
+
* validates the loaded module and surfaces structured errors in the
|
|
19
|
+
* Custom Catalogs UI so the canvas can keep working.
|
|
20
|
+
* - The `/build` slash-command in catalog-template owns the
|
|
21
|
+
* SKILL.md ↔ defineCatalog wiring symmetry check.
|
|
22
|
+
*
|
|
23
|
+
* `defineCatalog` is therefore a pure type-narrowing identity, matching the
|
|
24
|
+
* style of `defineComponent`.
|
|
25
|
+
*/
|
|
26
|
+
export interface CatalogDefinitionBase {
|
|
27
|
+
components: Record<string, CatalogComponentDefinition<any>>;
|
|
28
|
+
}
|
|
29
|
+
export interface CatalogDefinitionWithSkill extends CatalogDefinitionBase {
|
|
30
|
+
/**
|
|
31
|
+
* Free-form markdown — author-supplied cross-component guidance the AI
|
|
32
|
+
* fetches lazily via `get_catalog_skill`. Do **not** restate per-component
|
|
33
|
+
* prop info here; that reaches the AI through each component's Zod schema
|
|
34
|
+
* and the existing `get_json_ui_component_schemas` tool, and any
|
|
35
|
+
* duplication will drift.
|
|
36
|
+
*/
|
|
37
|
+
skill: string;
|
|
38
|
+
/**
|
|
39
|
+
* ≤120-char headline shown in the system prompt's
|
|
40
|
+
* `<available_catalog_skills>` block so the AI can decide whether the
|
|
41
|
+
* catalog is relevant before fetching the full skill.
|
|
42
|
+
*/
|
|
43
|
+
summary: string;
|
|
44
|
+
}
|
|
45
|
+
export type CatalogDefinition = CatalogDefinitionBase | CatalogDefinitionWithSkill;
|
|
46
|
+
export declare function defineCatalog<C extends Record<string, CatalogComponentDefinition<any>>>(def: {
|
|
47
|
+
components: C;
|
|
48
|
+
skill: string;
|
|
49
|
+
summary: string;
|
|
50
|
+
}): CatalogDefinitionWithSkill;
|
|
51
|
+
export declare function defineCatalog<C extends Record<string, CatalogComponentDefinition<any>>>(def: {
|
|
52
|
+
components: C;
|
|
53
|
+
}): CatalogDefinitionBase;
|