@altertable/data-app 0.64.0 → 0.66.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/AGENTS.md +40 -0
- package/CONTRIBUTING.md +37 -3
- package/README.md +2 -2
- package/dist/chunks/{contract-mwe7gnmh.js → contract-13j5zb3c.js} +110 -216
- package/dist/chunks/contract-13j5zb3c.js.map +13 -0
- package/dist/chunks/contract-19ckn3n7.js +78 -0
- package/dist/chunks/contract-19ckn3n7.js.map +10 -0
- package/dist/chunks/contract-awa2d5b1.js +272 -0
- package/dist/chunks/contract-awa2d5b1.js.map +12 -0
- package/dist/chunks/{contract-xtxza1fy.js → contract-e11m4y7f.js} +35 -71
- package/dist/chunks/contract-e11m4y7f.js.map +12 -0
- package/dist/chunks/{contract-zr3jd7mr.js → contract-h8a6f559.js} +100 -75
- package/dist/chunks/contract-h8a6f559.js.map +12 -0
- package/dist/chunks/contract-havnjmdr.js +13011 -0
- package/dist/chunks/contract-havnjmdr.js.map +99 -0
- package/dist/chunks/{contract-txjy0en2.js → contract-m6n8ctfc.js} +64 -72
- package/dist/chunks/contract-m6n8ctfc.js.map +10 -0
- package/dist/chunks/contract-pk603qj7.js +146 -0
- package/dist/chunks/contract-pk603qj7.js.map +10 -0
- package/dist/chunks/{contract-yxbjea23.js → contract-rfbakka4.js} +179 -2
- package/dist/chunks/contract-rfbakka4.js.map +13 -0
- package/dist/client/index.js +11 -6
- package/dist/client/index.js.map +1 -1
- package/dist/core/appearance.js +1 -1
- package/dist/core/contract.js +13 -3
- package/dist/core/contract.js.map +1 -1
- package/dist/embed/index.js +25 -13
- package/dist/embed/index.js.map +3 -3
- package/dist/local.js +1 -1
- package/dist/local.js.map +2 -2
- package/dist/react/embed/index.js +5 -2
- package/dist/react/embed/index.js.map +3 -3
- package/dist/react/index.js +2532 -11273
- package/dist/react/index.js.map +16 -88
- package/dist/react/ui/index.js +1614 -0
- package/dist/react/ui/index.js.map +22 -0
- package/dist/server.js +1 -1
- package/dist/server.js.map +2 -2
- package/dist/types/client/annotations.d.ts +23 -0
- package/dist/types/client/iframe.d.ts +2 -0
- package/dist/types/client/index.d.ts +2 -0
- package/dist/types/client/logger.d.ts +6 -0
- package/dist/types/core/annotations.d.ts +92 -0
- package/dist/types/core/appearance.d.ts +2 -2
- package/dist/types/core/bridge-endpoint.d.ts +21 -0
- package/dist/types/core/bridge.d.ts +137 -16
- package/dist/types/core/contract.d.ts +2 -0
- package/dist/types/core/logger.d.ts +12 -0
- package/dist/types/core/presentation.d.ts +2 -0
- package/dist/types/core/variables.d.ts +2 -5
- package/dist/types/embed/host.d.ts +6 -2
- package/dist/types/embed/index.d.ts +1 -0
- package/dist/types/embed/source.d.ts +1 -1
- package/dist/types/react/annotations/AnnotationBar.d.ts +24 -0
- package/dist/types/react/annotations/AnnotationControls.d.ts +10 -0
- package/dist/types/react/annotations/AnnotationEditor.d.ts +15 -0
- package/dist/types/react/annotations/AnnotationMarkers.d.ts +19 -0
- package/dist/types/react/annotations/AnnotationSelectionLayer.d.ts +12 -0
- package/dist/types/react/annotations/AnnotationTarget.d.ts +9 -0
- package/dist/types/react/annotations/AnnotationTooltip.d.ts +9 -0
- package/dist/types/react/annotations/AnnotationTrigger.d.ts +10 -0
- package/dist/types/react/annotations/annotation-editor-state.d.ts +56 -0
- package/dist/types/react/annotations/annotation-screenshot.d.ts +4 -0
- package/dist/types/react/annotations/annotation-targets.d.ts +33 -0
- package/dist/types/react/annotations/getAnnotationProps.d.ts +15 -0
- package/dist/types/react/annotations/styles.d.ts +3 -0
- package/dist/types/react/annotations/useAnnotationGeometry.d.ts +22 -0
- package/dist/types/react/annotations/useDataAppAnnotations.d.ts +19 -0
- package/dist/types/react/bindings.d.ts +110 -0
- package/dist/types/react/content.d.ts +24 -11
- package/dist/types/react/embed/bridge.d.ts +1 -1
- package/dist/types/react/hooks.d.ts +29 -787
- package/dist/types/react/index.d.ts +30 -104
- package/dist/types/react/source-owner.d.ts +2 -0
- package/dist/types/react/style-contract.d.ts +60 -0
- package/dist/types/react/style-validation.d.ts +2 -0
- package/dist/types/react/ui/AboutData.d.ts +9 -1
- package/dist/types/react/ui/AppHeader.d.ts +1 -2
- package/dist/types/react/ui/AppLayout.d.ts +3 -9
- package/dist/types/react/ui/AppToolbar.d.ts +6 -7
- package/dist/types/react/ui/AreaChart.d.ts +7 -0
- package/dist/types/react/ui/BarChart.d.ts +6 -0
- package/dist/types/react/ui/{ComparisonVisual.d.ts → Comparison.d.ts} +3 -3
- package/dist/types/react/ui/ContentSkeleton.d.ts +2 -0
- package/dist/types/react/ui/DataApp.d.ts +17 -53
- package/dist/types/react/ui/DataAppFrame.d.ts +42 -0
- package/dist/types/react/ui/DataBoundary.d.ts +4 -5
- package/dist/types/react/ui/DataSection.d.ts +6 -18
- package/dist/types/react/ui/DataSectionBoundary.d.ts +30 -0
- package/dist/types/react/ui/DataValue.d.ts +9 -0
- package/dist/types/react/ui/DataWidget.d.ts +2 -1
- package/dist/types/react/ui/DateTimeTooltip.d.ts +1 -1
- package/dist/types/react/ui/ExportControl.d.ts +4 -0
- package/dist/types/react/ui/HelpPopover.d.ts +3 -4
- package/dist/types/react/ui/InspectionContext.d.ts +2 -0
- package/dist/types/react/ui/InspectionProvider.d.ts +29 -0
- package/dist/types/react/ui/LineChart.d.ts +7 -0
- package/dist/types/react/ui/MetricWidget.d.ts +4 -3
- package/dist/types/react/ui/PieChart.d.ts +8 -0
- package/dist/types/react/ui/PresentStory.d.ts +2 -5
- package/dist/types/react/ui/RefreshControl.d.ts +1 -2
- package/dist/types/react/ui/ScatterChart.d.ts +17 -0
- package/dist/types/react/ui/SearchField.d.ts +1 -2
- package/dist/types/react/ui/SearchInput.d.ts +3 -1
- package/dist/types/react/ui/Sheet.d.ts +2 -1
- package/dist/types/react/ui/Skeleton.d.ts +5 -2
- package/dist/types/react/ui/StaticDataApp.d.ts +4 -0
- package/dist/types/react/ui/TableWidget.d.ts +19 -16
- package/dist/types/react/ui/Tabs.d.ts +6 -2
- package/dist/types/react/ui/TextWidget.d.ts +2 -1
- package/dist/types/react/ui/Toast.d.ts +8 -0
- package/dist/types/react/ui/Tooltip.d.ts +4 -3
- package/dist/types/react/ui/TrendChart.d.ts +5 -0
- package/dist/types/react/ui/UpdatedAt.d.ts +1 -1
- package/dist/types/react/ui/VisualizationWidget.d.ts +12 -13
- package/dist/types/react/ui/WidgetViewTabs.d.ts +1 -1
- package/dist/types/react/ui/chart-data.d.ts +25 -0
- package/dist/types/react/ui/csv-export.d.ts +19 -0
- package/dist/types/react/ui/data-context.d.ts +18 -7
- package/dist/types/react/ui/icons.d.ts +2 -0
- package/dist/types/react/ui/index.d.ts +74 -0
- package/dist/types/react/ui/metric.d.ts +1 -1
- package/dist/types/react/ui/presentation.d.ts +5 -1
- package/dist/types/react/ui/shortcuts.d.ts +7 -1
- package/dist/types/react/view-runtime.d.ts +29 -0
- package/dist/types/react/view.d.ts +21 -4
- package/dist/types/react/widgets.d.ts +79 -0
- package/dist/worker.js +381 -79
- package/docs/app-authoring.md +64 -30
- package/docs/data-context.md +89 -0
- package/docs/embed.md +1 -1
- package/docs/formatting-and-appearance.md +52 -0
- package/docs/hosted-apps.md +2 -15
- package/docs/layout.md +33 -0
- package/docs/local-data-apps.md +1 -1
- package/docs/react-embed.md +2 -2
- package/docs/react.md +20 -254
- package/docs/stories-and-export.md +52 -0
- package/docs/styling.md +55 -0
- package/docs/ui-quality.md +20 -0
- package/docs/ui.md +41 -0
- package/docs/variables.md +98 -0
- package/docs/views.md +127 -0
- package/docs/widgets.md +142 -0
- package/examples/starter-data-app/index.tsx +89 -44
- package/package.json +13 -4
- package/dist/chunks/contract-cxr9t12b.js +0 -18
- package/dist/chunks/contract-cxr9t12b.js.map +0 -10
- package/dist/chunks/contract-mwe7gnmh.js.map +0 -13
- package/dist/chunks/contract-txjy0en2.js.map +0 -10
- package/dist/chunks/contract-xtxza1fy.js.map +0 -12
- package/dist/chunks/contract-yxbjea23.js.map +0 -12
- package/dist/chunks/contract-zr3jd7mr.js.map +0 -12
- package/dist/types/react/ui/RefreshRegion.d.ts +0 -9
- package/dist/types/react/ui/SelectableBarChart.d.ts +0 -15
- package/docs/releasing.md +0 -79
- /package/dist/types/react/ui/{comparison.d.ts → metric-comparison.d.ts} +0 -0
package/dist/worker.js
CHANGED
|
@@ -48,19 +48,273 @@ function trustedParent(config, searchParams) {
|
|
|
48
48
|
var TOKEN_RE = /^(?=.{1,63}$)[a-z0-9]+(?:-[a-z0-9]+)+-app-[1-9][0-9]*$/;
|
|
49
49
|
var PAGE_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; connect-src 'none'; form-action 'none'; base-uri 'none'";
|
|
50
50
|
var inlineBootstrap = `(() => {
|
|
51
|
+
// src/core/logger.ts
|
|
52
|
+
function isLogEntry(value) {
|
|
53
|
+
if (!value || typeof value !== "object")
|
|
54
|
+
return false;
|
|
55
|
+
const entry = value;
|
|
56
|
+
return (entry.method === "log" || entry.method === "info" || entry.method === "warn" || entry.method === "error") && Array.isArray(entry.args) && entry.args.length <= 128;
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
// src/core/bridge.ts
|
|
52
60
|
var BRIDGE = "altertable:data-app";
|
|
53
61
|
var MAX_PENDING = 128;
|
|
54
62
|
var REQUEST_TIMEOUT_MS = 60000;
|
|
55
|
-
function isBridgeMessage(value) {
|
|
56
|
-
if (!value || typeof value !== "object")
|
|
57
|
-
return false;
|
|
58
|
-
const message = value;
|
|
59
|
-
return message.channel === BRIDGE && message.version === 1 && typeof message.type === "string" && typeof message.documentId === "string" && message.documentId.length > 0 && message.documentId.length <= 128;
|
|
60
|
-
}
|
|
61
63
|
function validId(value) {
|
|
62
64
|
return typeof value === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(value);
|
|
63
65
|
}
|
|
66
|
+
function event({
|
|
67
|
+
wire,
|
|
68
|
+
from,
|
|
69
|
+
session,
|
|
70
|
+
parse
|
|
71
|
+
}) {
|
|
72
|
+
return { wire, from, session, parse };
|
|
73
|
+
}
|
|
74
|
+
function id(fields) {
|
|
75
|
+
if (!validId(fields.id))
|
|
76
|
+
throw new Error("Invalid request ID.");
|
|
77
|
+
return fields.id;
|
|
78
|
+
}
|
|
79
|
+
function string(fields, key) {
|
|
80
|
+
const value = fields[key];
|
|
81
|
+
if (typeof value !== "string")
|
|
82
|
+
throw new Error(\`Invalid \${key}.\`);
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
function empty() {
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
var bridgeProtocol = {
|
|
89
|
+
channel: BRIDGE,
|
|
90
|
+
version: 1,
|
|
91
|
+
events: {
|
|
92
|
+
connect: event({
|
|
93
|
+
wire: "bridge:connect",
|
|
94
|
+
from: "host",
|
|
95
|
+
session: "none",
|
|
96
|
+
parse: empty
|
|
97
|
+
}),
|
|
98
|
+
ready: event({
|
|
99
|
+
wire: "bridge:ready",
|
|
100
|
+
from: "app",
|
|
101
|
+
session: "none",
|
|
102
|
+
parse: empty
|
|
103
|
+
}),
|
|
104
|
+
initialize: event({
|
|
105
|
+
wire: "bridge:initialize",
|
|
106
|
+
from: "host",
|
|
107
|
+
session: "establish",
|
|
108
|
+
parse: (fields) => ({ state: fields.state })
|
|
109
|
+
}),
|
|
110
|
+
request: event({
|
|
111
|
+
wire: "bridge:request",
|
|
112
|
+
from: "app",
|
|
113
|
+
session: "current",
|
|
114
|
+
parse: (fields) => {
|
|
115
|
+
const route = string(fields, "route");
|
|
116
|
+
if (!route || route.length > 256)
|
|
117
|
+
throw new Error("Invalid message route.");
|
|
118
|
+
return { id: id(fields), route, payload: fields.payload };
|
|
119
|
+
}
|
|
120
|
+
}),
|
|
121
|
+
result: event({
|
|
122
|
+
wire: "bridge:result",
|
|
123
|
+
from: "host",
|
|
124
|
+
session: "current",
|
|
125
|
+
parse: (fields) => {
|
|
126
|
+
if (!Object.hasOwn(fields, "response"))
|
|
127
|
+
throw new Error("Missing response.");
|
|
128
|
+
return { id: id(fields), response: fields.response };
|
|
129
|
+
}
|
|
130
|
+
}),
|
|
131
|
+
error: event({
|
|
132
|
+
wire: "bridge:error",
|
|
133
|
+
from: "host",
|
|
134
|
+
session: "current",
|
|
135
|
+
parse: (fields) => {
|
|
136
|
+
if (fields.requestId !== undefined && typeof fields.requestId !== "string")
|
|
137
|
+
throw new Error("Invalid requestId.");
|
|
138
|
+
return {
|
|
139
|
+
id: id(fields),
|
|
140
|
+
code: string(fields, "code"),
|
|
141
|
+
message: string(fields, "message"),
|
|
142
|
+
...fields.requestId === undefined ? {} : { requestId: fields.requestId }
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}),
|
|
146
|
+
cancel: event({
|
|
147
|
+
wire: "bridge:cancel",
|
|
148
|
+
from: "app",
|
|
149
|
+
session: "current",
|
|
150
|
+
parse: (fields) => ({ id: id(fields) })
|
|
151
|
+
}),
|
|
152
|
+
disconnect: event({
|
|
153
|
+
wire: "bridge:disconnect",
|
|
154
|
+
from: "app",
|
|
155
|
+
session: "current",
|
|
156
|
+
parse: empty
|
|
157
|
+
}),
|
|
158
|
+
scriptLoad: event({
|
|
159
|
+
wire: "script:load",
|
|
160
|
+
from: "host",
|
|
161
|
+
session: "current",
|
|
162
|
+
parse: (fields) => ({ javascript: string(fields, "javascript") })
|
|
163
|
+
}),
|
|
164
|
+
stateUpdate: event({
|
|
165
|
+
wire: "state:update",
|
|
166
|
+
from: "host",
|
|
167
|
+
session: "current",
|
|
168
|
+
parse: (fields) => ({ state: fields.state })
|
|
169
|
+
}),
|
|
170
|
+
runtimeLog: event({
|
|
171
|
+
wire: "runtime:log",
|
|
172
|
+
from: "app",
|
|
173
|
+
session: "current",
|
|
174
|
+
parse: (fields) => {
|
|
175
|
+
if (!isLogEntry(fields.payload))
|
|
176
|
+
throw new Error("Invalid log entry.");
|
|
177
|
+
return { payload: fields.payload };
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
runtimeReady: event({
|
|
181
|
+
wire: "runtime:ready",
|
|
182
|
+
from: "app",
|
|
183
|
+
session: "current",
|
|
184
|
+
parse: empty
|
|
185
|
+
}),
|
|
186
|
+
runtimeError: event({
|
|
187
|
+
wire: "runtime:error",
|
|
188
|
+
from: "app",
|
|
189
|
+
session: "current",
|
|
190
|
+
parse: empty
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
var byWire = new Map;
|
|
195
|
+
for (const name of Object.keys(bridgeProtocol.events)) {
|
|
196
|
+
const wire = bridgeProtocol.events[name].wire;
|
|
197
|
+
if (byWire.has(wire))
|
|
198
|
+
throw new Error(\`Duplicate bridge event: \${wire}\`);
|
|
199
|
+
byWire.set(wire, name);
|
|
200
|
+
}
|
|
201
|
+
function bridgeEventName(wire) {
|
|
202
|
+
return byWire.get(wire);
|
|
203
|
+
}
|
|
204
|
+
function parseBridgeEnvelope(value) {
|
|
205
|
+
if (!value || typeof value !== "object")
|
|
206
|
+
return;
|
|
207
|
+
const fields = value;
|
|
208
|
+
if (fields.channel !== BRIDGE || fields.version !== bridgeProtocol.version || typeof fields.type !== "string" || typeof fields.documentId !== "string" || !fields.documentId || fields.documentId.length > 128 || fields.sessionId !== undefined && !validId(fields.sessionId) || fields.token !== undefined && typeof fields.token !== "string")
|
|
209
|
+
return;
|
|
210
|
+
return fields;
|
|
211
|
+
}
|
|
212
|
+
function parseBridgePayload(envelope, definition) {
|
|
213
|
+
try {
|
|
214
|
+
return {
|
|
215
|
+
channel: BRIDGE,
|
|
216
|
+
version: bridgeProtocol.version,
|
|
217
|
+
documentId: envelope.documentId,
|
|
218
|
+
...envelope.sessionId === undefined ? {} : { sessionId: envelope.sessionId },
|
|
219
|
+
...envelope.token === undefined ? {} : { token: envelope.token },
|
|
220
|
+
type: definition.wire,
|
|
221
|
+
...definition.parse(envelope)
|
|
222
|
+
};
|
|
223
|
+
} catch {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function parseBridgeMessage(value) {
|
|
228
|
+
const envelope = parseBridgeEnvelope(value);
|
|
229
|
+
if (!envelope)
|
|
230
|
+
return;
|
|
231
|
+
const name = bridgeEventName(envelope.type);
|
|
232
|
+
if (!name)
|
|
233
|
+
return;
|
|
234
|
+
const definition = bridgeProtocol.events[name];
|
|
235
|
+
if (definition.session !== "none" && envelope.sessionId === undefined)
|
|
236
|
+
return;
|
|
237
|
+
return parseBridgePayload(envelope, definition);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/core/bridge-endpoint.ts
|
|
241
|
+
function createBridgeEndpoint({
|
|
242
|
+
role,
|
|
243
|
+
origin,
|
|
244
|
+
source,
|
|
245
|
+
opaque,
|
|
246
|
+
context,
|
|
247
|
+
post,
|
|
248
|
+
diagnostic,
|
|
249
|
+
invalidRequest,
|
|
250
|
+
handlers
|
|
251
|
+
}) {
|
|
252
|
+
let disposed = false;
|
|
253
|
+
function send(name, payload) {
|
|
254
|
+
if (disposed)
|
|
255
|
+
throw new Error("The bridge endpoint has closed.");
|
|
256
|
+
const definition = bridgeProtocol.events[name];
|
|
257
|
+
const current = context();
|
|
258
|
+
const message = parseBridgeMessage({
|
|
259
|
+
...payload,
|
|
260
|
+
channel: bridgeProtocol.channel,
|
|
261
|
+
version: bridgeProtocol.version,
|
|
262
|
+
type: definition.wire,
|
|
263
|
+
documentId: name === "connect" ? "host" : current.documentId,
|
|
264
|
+
sessionId: current.sessionId,
|
|
265
|
+
token: opaque ? current.token : undefined
|
|
266
|
+
});
|
|
267
|
+
if (!message || opaque && !validId(current.token))
|
|
268
|
+
throw new Error("The bridge is not ready to send this event.");
|
|
269
|
+
diagnostic?.("send", definition.wire);
|
|
270
|
+
if (disposed)
|
|
271
|
+
return;
|
|
272
|
+
post(message);
|
|
273
|
+
}
|
|
274
|
+
function receive(event) {
|
|
275
|
+
const peer = source();
|
|
276
|
+
if (disposed || !peer || event.origin !== origin || event.source !== peer)
|
|
277
|
+
return;
|
|
278
|
+
const envelope = parseBridgeEnvelope(event.data);
|
|
279
|
+
if (!envelope)
|
|
280
|
+
return;
|
|
281
|
+
const current = context();
|
|
282
|
+
const connecting = role === "app" && envelope.type === bridgeProtocol.events.connect.wire;
|
|
283
|
+
if (opaque && (connecting ? !validId(envelope.token) : !current.token || envelope.token !== current.token))
|
|
284
|
+
return;
|
|
285
|
+
const name = bridgeEventName(envelope.type);
|
|
286
|
+
if (!name)
|
|
287
|
+
return;
|
|
288
|
+
const definition = bridgeProtocol.events[name];
|
|
289
|
+
if (definition.from === role)
|
|
290
|
+
return;
|
|
291
|
+
if (definition.session === "establish") {
|
|
292
|
+
if (envelope.documentId !== current.documentId || envelope.sessionId === undefined)
|
|
293
|
+
return;
|
|
294
|
+
} else if (definition.session === "current") {
|
|
295
|
+
if (!current.sessionId || envelope.sessionId !== current.sessionId || envelope.documentId !== current.documentId)
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const message = parseBridgePayload(envelope, definition);
|
|
299
|
+
if (!message) {
|
|
300
|
+
if (role === "host" && name === "request" && validId(envelope.id))
|
|
301
|
+
invalidRequest?.(envelope.id);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
diagnostic?.("receive", definition.wire);
|
|
305
|
+
if (disposed)
|
|
306
|
+
return;
|
|
307
|
+
const handler = handlers[name];
|
|
308
|
+
handler(message);
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
send,
|
|
312
|
+
receive,
|
|
313
|
+
dispose() {
|
|
314
|
+
disposed = true;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
64
318
|
|
|
65
319
|
// src/core/messages.ts
|
|
66
320
|
var errorBrand = Symbol.for("@altertable/data-app/MessageRoutingError");
|
|
@@ -193,6 +447,46 @@ var inlineBootstrap = `(() => {
|
|
|
193
447
|
}
|
|
194
448
|
}
|
|
195
449
|
|
|
450
|
+
// src/client/logger.ts
|
|
451
|
+
function createBridgeLogger(enabled, send) {
|
|
452
|
+
function resolve(args) {
|
|
453
|
+
if (args.length === 1 && typeof args[0] === "function") {
|
|
454
|
+
const value = args[0]();
|
|
455
|
+
return Array.isArray(value) ? value : [value];
|
|
456
|
+
}
|
|
457
|
+
return args.map((arg) => typeof arg === "function" ? arg() : arg);
|
|
458
|
+
}
|
|
459
|
+
function write(method, args) {
|
|
460
|
+
if (!enabled())
|
|
461
|
+
return;
|
|
462
|
+
try {
|
|
463
|
+
send({ method, args: resolve(args) });
|
|
464
|
+
} catch {}
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
log: (...args) => write("log", args),
|
|
468
|
+
info: (...args) => write("info", args),
|
|
469
|
+
warn: (...args) => write("warn", args),
|
|
470
|
+
error: (...args) => write("error", args)
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function logBridgeMessage(logger, message) {
|
|
474
|
+
if (message.type === "runtime:log")
|
|
475
|
+
return;
|
|
476
|
+
logger.log(() => {
|
|
477
|
+
const metadata = { type: message.type };
|
|
478
|
+
if ("id" in message)
|
|
479
|
+
metadata.id = message.id;
|
|
480
|
+
if (message.type === "bridge:request") {
|
|
481
|
+
metadata.route = message.route;
|
|
482
|
+
const payload = message.payload;
|
|
483
|
+
if (message.route === "data:query" && payload && typeof payload === "object" && "operation" in payload && typeof payload.operation === "string")
|
|
484
|
+
metadata.operation = payload.operation;
|
|
485
|
+
}
|
|
486
|
+
return ["Sending message to parent", metadata];
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
196
490
|
// src/client/iframe.ts
|
|
197
491
|
function rethrowDataMessageError(error) {
|
|
198
492
|
if (error instanceof MessageRoutingError)
|
|
@@ -215,67 +509,61 @@ var inlineBootstrap = `(() => {
|
|
|
215
509
|
const pending = new Map;
|
|
216
510
|
let hostState;
|
|
217
511
|
const stateListeners = new Set;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
512
|
+
const logger = createBridgeLogger(() => !disposed && !!sessionId && !!hostState && typeof hostState === "object" && ("logging" in hostState) && hostState.logging === true, (entry) => send("runtimeLog", { payload: entry }));
|
|
513
|
+
const endpoint = createBridgeEndpoint({
|
|
514
|
+
role: "app",
|
|
515
|
+
origin: parentOrigin,
|
|
516
|
+
source: () => frame.parent,
|
|
517
|
+
opaque: mode === "bundle",
|
|
518
|
+
context: () => ({ documentId, sessionId, token }),
|
|
519
|
+
post(message) {
|
|
520
|
+
frame.parent.postMessage(message, parentOrigin);
|
|
521
|
+
logBridgeMessage(logger, message);
|
|
522
|
+
},
|
|
523
|
+
handlers: {
|
|
524
|
+
connect(message) {
|
|
525
|
+
if (mode === "bundle")
|
|
526
|
+
token = message.token;
|
|
527
|
+
send("ready", {});
|
|
528
|
+
},
|
|
529
|
+
initialize(message) {
|
|
530
|
+
if (sessionId && sessionId !== message.sessionId) {
|
|
531
|
+
for (const entry of pending.values())
|
|
532
|
+
entry.reject(new DataAppError("The preview reconnected. Retry the request.", "bridge_reset"));
|
|
533
|
+
}
|
|
534
|
+
const first = !sessionId;
|
|
535
|
+
sessionId = message.sessionId;
|
|
536
|
+
receiveState(message.state);
|
|
537
|
+
if (first)
|
|
538
|
+
for (const entry of pending.values())
|
|
539
|
+
entry.start();
|
|
540
|
+
if (disposed)
|
|
541
|
+
return;
|
|
542
|
+
if (mode === "url")
|
|
543
|
+
send("runtimeReady", {});
|
|
544
|
+
},
|
|
545
|
+
stateUpdate(message) {
|
|
546
|
+
receiveState(message.state);
|
|
547
|
+
},
|
|
548
|
+
scriptLoad(message) {
|
|
549
|
+
if (mode === "bundle")
|
|
550
|
+
loadScript?.(message.javascript);
|
|
551
|
+
},
|
|
552
|
+
result(message) {
|
|
553
|
+
pending.get(message.id)?.resolve(message.response);
|
|
554
|
+
},
|
|
555
|
+
error(message) {
|
|
556
|
+
pending.get(message.id)?.reject(new MessageRoutingError(message.code, message.message, message.requestId));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
});
|
|
560
|
+
const send = endpoint.send;
|
|
561
|
+
const receive = endpoint.receive;
|
|
221
562
|
function receiveState(state) {
|
|
222
563
|
hostState = state;
|
|
223
564
|
for (const listener of stateListeners)
|
|
224
565
|
listener(state);
|
|
225
566
|
}
|
|
226
|
-
function receive(event) {
|
|
227
|
-
if (event.origin !== parentOrigin || event.source !== frame.parent || !isBridgeMessage(event.data))
|
|
228
|
-
return;
|
|
229
|
-
const message = event.data;
|
|
230
|
-
if (message.type === "bridge:connect") {
|
|
231
|
-
if (mode === "bundle") {
|
|
232
|
-
if (!validId(message.token))
|
|
233
|
-
return;
|
|
234
|
-
token = message.token;
|
|
235
|
-
}
|
|
236
|
-
send({ type: "bridge:ready" });
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
if (mode === "bundle" && (!token || message.token !== token))
|
|
240
|
-
return;
|
|
241
|
-
if (message.documentId !== documentId)
|
|
242
|
-
return;
|
|
243
|
-
if (message.type === "bridge:initialize" && validId(message.sessionId)) {
|
|
244
|
-
if (sessionId && sessionId !== message.sessionId) {
|
|
245
|
-
for (const entry of pending.values())
|
|
246
|
-
entry.reject(new DataAppError("The preview reconnected. Retry the request.", "bridge_reset"));
|
|
247
|
-
}
|
|
248
|
-
const first = !sessionId;
|
|
249
|
-
sessionId = message.sessionId;
|
|
250
|
-
if (first)
|
|
251
|
-
for (const entry of pending.values())
|
|
252
|
-
entry.start();
|
|
253
|
-
receiveState(message.state);
|
|
254
|
-
if (mode === "url")
|
|
255
|
-
send({ type: "runtime:ready" });
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
if (!sessionId || message.sessionId !== sessionId)
|
|
259
|
-
return;
|
|
260
|
-
if (message.type === "state:update") {
|
|
261
|
-
receiveState(message.state);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
if (message.type === "script:load" && mode === "bundle" && typeof message.javascript === "string") {
|
|
265
|
-
loadScript?.(message.javascript);
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
if (typeof message.id !== "string")
|
|
269
|
-
return;
|
|
270
|
-
const entry = pending.get(message.id);
|
|
271
|
-
if (!entry)
|
|
272
|
-
return;
|
|
273
|
-
if (message.type === "bridge:result" && "response" in message) {
|
|
274
|
-
entry.resolve(message.response);
|
|
275
|
-
} else if (message.type === "bridge:error" && typeof message.code === "string" && typeof message.message === "string") {
|
|
276
|
-
entry.reject(new MessageRoutingError(message.code, message.message, typeof message.requestId === "string" ? message.requestId : undefined));
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
567
|
frame.addEventListener("message", receive);
|
|
280
568
|
function requestMessage(message, signal) {
|
|
281
569
|
return new Promise((resolve, reject) => {
|
|
@@ -294,13 +582,13 @@ var inlineBootstrap = `(() => {
|
|
|
294
582
|
}
|
|
295
583
|
function abort() {
|
|
296
584
|
if (sent)
|
|
297
|
-
send(
|
|
585
|
+
send("cancel", { id });
|
|
298
586
|
cleanup();
|
|
299
587
|
reject(signal?.reason);
|
|
300
588
|
}
|
|
301
589
|
const timer = setTimeout(() => {
|
|
302
590
|
if (sent)
|
|
303
|
-
send(
|
|
591
|
+
send("cancel", { id });
|
|
304
592
|
cleanup();
|
|
305
593
|
reject(new DataAppError(sessionId ? "The data request timed out." : "The preview shell is unavailable. Reload the preview.", sessionId ? "timeout" : "bridge_unavailable"));
|
|
306
594
|
}, timeoutMs);
|
|
@@ -309,8 +597,7 @@ var inlineBootstrap = `(() => {
|
|
|
309
597
|
if (sent)
|
|
310
598
|
return;
|
|
311
599
|
try {
|
|
312
|
-
send({
|
|
313
|
-
type: "bridge:request",
|
|
600
|
+
send("request", {
|
|
314
601
|
id,
|
|
315
602
|
route: message.route,
|
|
316
603
|
payload: message.payload
|
|
@@ -334,8 +621,8 @@ var inlineBootstrap = `(() => {
|
|
|
334
621
|
signal?.addEventListener("abort", abort, { once: true });
|
|
335
622
|
if (sessionId)
|
|
336
623
|
entry.start();
|
|
337
|
-
else
|
|
338
|
-
send(
|
|
624
|
+
else if (mode === "url" || token)
|
|
625
|
+
send("ready", {});
|
|
339
626
|
});
|
|
340
627
|
}
|
|
341
628
|
const messages = createMessageClient({ "data:query": defineDataQueryRoute(), "data:sql": sqlQueryRoute }, requestMessage);
|
|
@@ -343,20 +630,22 @@ var inlineBootstrap = `(() => {
|
|
|
343
630
|
return messages.request("data:query", { operation, input }, { signal }).catch(rethrowDataMessageError);
|
|
344
631
|
}
|
|
345
632
|
function disconnect() {
|
|
346
|
-
|
|
633
|
+
if (sessionId)
|
|
634
|
+
send("disconnect", {});
|
|
347
635
|
sessionId = undefined;
|
|
348
636
|
for (const entry of pending.values())
|
|
349
637
|
entry.reject(new DataAppError("The preview has closed.", "bridge_closed"));
|
|
350
638
|
}
|
|
351
639
|
function resume(event) {
|
|
352
|
-
if (event.persisted)
|
|
353
|
-
send(
|
|
640
|
+
if (event.persisted && (mode === "url" || token))
|
|
641
|
+
send("ready", {});
|
|
354
642
|
}
|
|
355
643
|
function dispose() {
|
|
356
644
|
if (disposed)
|
|
357
645
|
return;
|
|
358
646
|
disconnect();
|
|
359
647
|
disposed = true;
|
|
648
|
+
endpoint.dispose();
|
|
360
649
|
frame.removeEventListener("message", receive);
|
|
361
650
|
frame.removeEventListener("pagehide", disconnect);
|
|
362
651
|
frame.removeEventListener("pageshow", resume);
|
|
@@ -365,8 +654,9 @@ var inlineBootstrap = `(() => {
|
|
|
365
654
|
frame.addEventListener("pagehide", disconnect);
|
|
366
655
|
frame.addEventListener("pageshow", resume);
|
|
367
656
|
if (mode === "url")
|
|
368
|
-
send(
|
|
657
|
+
send("ready", {});
|
|
369
658
|
return {
|
|
659
|
+
logger,
|
|
370
660
|
request: requestMessage,
|
|
371
661
|
transport: queryOperation,
|
|
372
662
|
lakehouse: {
|
|
@@ -386,10 +676,12 @@ var inlineBootstrap = `(() => {
|
|
|
386
676
|
};
|
|
387
677
|
},
|
|
388
678
|
ready() {
|
|
389
|
-
|
|
679
|
+
if (!disposed && sessionId)
|
|
680
|
+
send("runtimeReady", {});
|
|
390
681
|
},
|
|
391
682
|
fail() {
|
|
392
|
-
|
|
683
|
+
if (!disposed && sessionId)
|
|
684
|
+
send("runtimeError", {});
|
|
393
685
|
}
|
|
394
686
|
};
|
|
395
687
|
}
|
|
@@ -417,6 +709,7 @@ var inlineBootstrap = `(() => {
|
|
|
417
709
|
}) {
|
|
418
710
|
let loaded;
|
|
419
711
|
let failed = false;
|
|
712
|
+
const bundleSource = \`altertable-data-app-\${crypto.randomUUID()}.js\`;
|
|
420
713
|
const bridge = createIframeTransport({
|
|
421
714
|
parentOrigin,
|
|
422
715
|
window: frame,
|
|
@@ -435,7 +728,8 @@ var inlineBootstrap = `(() => {
|
|
|
435
728
|
loaded = javascript;
|
|
436
729
|
try {
|
|
437
730
|
const script = frame.document.createElement("script");
|
|
438
|
-
script.textContent = javascript
|
|
731
|
+
script.textContent = \`\${javascript}
|
|
732
|
+
//# sourceURL=\${bundleSource}\`;
|
|
439
733
|
frame.document.body.append(script);
|
|
440
734
|
if (!failed)
|
|
441
735
|
bridge.ready();
|
|
@@ -448,12 +742,20 @@ var inlineBootstrap = `(() => {
|
|
|
448
742
|
failed = true;
|
|
449
743
|
bridge.fail();
|
|
450
744
|
}
|
|
451
|
-
|
|
452
|
-
|
|
745
|
+
function reportBundleError(event) {
|
|
746
|
+
if (event.filename === bundleSource)
|
|
747
|
+
reportError();
|
|
748
|
+
}
|
|
749
|
+
function reportBundleRejection(event) {
|
|
750
|
+
if (event.reason instanceof Error && event.reason.stack?.includes(bundleSource))
|
|
751
|
+
reportError();
|
|
752
|
+
}
|
|
753
|
+
frame.addEventListener("error", reportBundleError);
|
|
754
|
+
frame.addEventListener("unhandledrejection", reportBundleRejection);
|
|
453
755
|
const uninstall = installDataAppTransport(bridge, frame);
|
|
454
756
|
return () => {
|
|
455
|
-
frame.removeEventListener("error",
|
|
456
|
-
frame.removeEventListener("unhandledrejection",
|
|
757
|
+
frame.removeEventListener("error", reportBundleError);
|
|
758
|
+
frame.removeEventListener("unhandledrejection", reportBundleRejection);
|
|
457
759
|
uninstall();
|
|
458
760
|
};
|
|
459
761
|
}
|
package/docs/app-authoring.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# Author a data app
|
|
2
2
|
|
|
3
3
|
Build an exploration that answers the user's question and a story that presents
|
|
4
|
-
its strongest findings.
|
|
4
|
+
its strongest findings. Include CSV export so readers can take the displayed
|
|
5
|
+
results into their own tools. Exploration, export, and story use the same
|
|
6
|
+
queries, definitions, and evidence.
|
|
5
7
|
|
|
6
8
|
## Inspect the data
|
|
7
9
|
|
|
@@ -25,39 +27,71 @@ readers can inspect the source of each claim.
|
|
|
25
27
|
|
|
26
28
|
Connect visualizations with introductions and explanations. Use `<TextWidget>`
|
|
27
29
|
for a narrative panel with the standard widget frame, or `<TextContent>` for
|
|
28
|
-
borderless prose.
|
|
29
|
-
|
|
30
|
-
and
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
30
|
+
borderless prose. Render static titles, descriptions, and instructions immediately. Use metric and dataset
|
|
31
|
+
bindings for dynamic values and `<DataValue>` for values within static prose.
|
|
32
|
+
Skeletonize only the content that needs data. Reuse bindings and the displayed
|
|
33
|
+
source in narrative so values, formatting, and evidence follow filter changes,
|
|
34
|
+
refresh, and failure.
|
|
35
|
+
|
|
36
|
+
| Task | Documentation |
|
|
37
|
+
| ------------------------------------------------ | ----------------------------------------------------------------- |
|
|
38
|
+
| Choose components, CSS tokens, and styling hooks | [Styling](styling.md) |
|
|
39
|
+
| Define queries, inputs, and result parsing | [Operations](contract.md) |
|
|
40
|
+
| Build views, filters, and request states | [Views](views.md) |
|
|
41
|
+
| Introduce and explain visualizations | [Narrative text](widgets.md#narrative-text) |
|
|
42
|
+
| Find formatters and presentation helpers | [App helpers](formatting-and-appearance.md) |
|
|
43
|
+
| Register source names | [Source identifiers](data-context.md#register-source-identifiers) |
|
|
44
|
+
| Choose date and field filters | [Filter variables](variables.md) |
|
|
45
|
+
| Handle refresh and stale results | [Displayed results](views.md#preserve-displayed-results) |
|
|
46
|
+
| Export displayed data as CSV | [CSV export](stories-and-export.md#export-displayed-data-as-csv) |
|
|
47
|
+
| Render widgets and custom visuals | [Widgets](widgets.md) |
|
|
48
|
+
| Bind definitions and source evidence | [Data context](data-context.md#bind-evidence) |
|
|
49
|
+
|
|
50
|
+
Declare reusable [datasets and metrics](widgets.md#declare-datasets-and-metrics)
|
|
51
|
+
on the view so tables, exports, and stories share values and evidence.
|
|
52
|
+
|
|
53
|
+
Every data app supplies a [story and CSV export](stories-and-export.md)
|
|
54
|
+
from the displayed result. Export all distinct datasets at their displayed grain;
|
|
55
|
+
reuse a dataset when several visuals derive from the same rows. Present the
|
|
56
|
+
findings that answer the reader's question, rather than every row or chart.
|
|
57
|
+
Use the [standard layout](layout.md) and built-in toolbar actions.
|
|
58
|
+
|
|
59
|
+
### Organize the exploration
|
|
60
|
+
|
|
61
|
+
Use sections for a focused question and for findings readers should compare
|
|
62
|
+
side by side. Add app-level `<Tabs>` from `/react/ui` when the exploration has
|
|
63
|
+
distinct analytical questions, such as Overview, Retention, and Segments, each
|
|
64
|
+
with its own context and group of widgets. Lead with the most useful overview
|
|
65
|
+
and label tabs by the question or subject they explore.
|
|
66
|
+
|
|
67
|
+
Use `<VisualizationWidget>`'s `views` for alternate representations of the same
|
|
68
|
+
dataset, such as a chart and its rows. Keep these choices within the widget;
|
|
69
|
+
use filter variables when the reader is changing the data scope.
|
|
70
|
+
|
|
71
|
+
Navigation tabs, widget views, and declared data views have different roles.
|
|
72
|
+
A declared view owns inputs, requests, and displayed results; a tab does not
|
|
73
|
+
require a separate data view. Keep related datasets in one view for a coherent
|
|
74
|
+
snapshot. Use independent views and `<DataSection>` boundaries when content
|
|
75
|
+
needs separate requests. Keep filter placement consistent across tabs and make
|
|
76
|
+
each filter's scope clear. Derive findings, story, and export from the displayed
|
|
77
|
+
results and their inputs.
|
|
78
|
+
|
|
79
|
+
Choose each visual for the question it answers; see
|
|
80
|
+
[visualization selection](widgets.md#choose-a-visualization).
|
|
53
81
|
|
|
54
82
|
## Verify the app
|
|
55
83
|
|
|
84
|
+
Follow the [UI quality contract](ui-quality.md) for hierarchy, responsive
|
|
85
|
+
composition, typography, and control states.
|
|
86
|
+
|
|
56
87
|
Verify findings against the source and the user's question. Distinguish measured
|
|
57
88
|
zero, unavailable values, and empty results. Check filters, refresh, loading,
|
|
58
|
-
empty, error, and stale states, then present the story.
|
|
89
|
+
empty, error, and stale states, then present the story. Download CSV from the
|
|
90
|
+
standalone and embedded toolbar and verify its filename, columns, raw values,
|
|
91
|
+
and filter scope against the displayed result. Export and Present story must be
|
|
92
|
+
available once results are shown; initial loading, empty, and initial
|
|
93
|
+
errors keep both actions visible and disabled. Inspect both experiences
|
|
59
94
|
at phone and desktop widths in light and dark themes.
|
|
60
95
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
stay backend-owned. Edit app-owned files; installed package files are dependencies.
|
|
96
|
+
Keep credentials, authorization, and enforced query limits in the host/backend.
|
|
97
|
+
Edit app-owned files; installed package files are dependencies.
|