@xenosystem/blocks 0.4.1 → 0.5.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/dist/auth/index.js +2 -0
- package/dist/chunk-2KG3PWR4.js +17 -0
- package/dist/chunk-DJJGZ6U4.js +215 -0
- package/dist/data/duckdb.d.ts +123 -0
- package/dist/data/duckdb.js +142 -0
- package/dist/data/index.d.ts +3255 -0
- package/dist/data/index.js +7185 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/ops/index.js +2 -1
- package/dist/time/index.d.ts +2261 -0
- package/dist/time/index.js +4658 -0
- package/dist/transport-B1cdciP8.d.ts +787 -0
- package/dist/trust/index.js +2 -0
- package/dist/{xterm-R3GIKSHA.js → xterm-R3ZKBPPC.js} +1 -0
- package/package.json +23 -3
|
@@ -0,0 +1,3255 @@
|
|
|
1
|
+
import { PanelModule, PanelManifest, LogSink } from '@xenosystem/panel-sdk';
|
|
2
|
+
import * as _xenosystem_data_core from '@xenosystem/data-core';
|
|
3
|
+
import { XenoColumn, XenoQuery, XenoResultSet, XenoValue, XenoRow, XenoAggFn, XenoValueFormat, XenoFilterGroup, XenoColumnRole, XenoSchemaCatalog, XenoFilter, XenoSourceSchema } from '@xenosystem/data-core';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
5
|
+
import { C as ConnectorExecuteInput, a as ConnectorExecuteOutput, b as ConnectorsPanelState, S as SubstrateFacilities, c as ConnectorsViewState, d as XenoConnection, e as XenoCredentialFormSpec, f as XenoCredentialResult, g as XenoConnectionStatus, P as PushdownSplit, h as XenoConnectorQueryRequest, i as XenoRestPaging, j as XenoConnectorFetch, k as XenoResolverErrorCode, l as XenoConnectorFileReader, X as XenoSqlEngine, m as ConnectorTransport, n as XenoCredentialField } from '../transport-B1cdciP8.js';
|
|
6
|
+
export { B as BrowserFetchOptions, o as ConnectorTransportError, p as SECRET_HEADER_NAMES, q as XenoConnectionConfig, r as XenoConnectionHealth, s as XenoConnectionKind, t as XenoConnectionState, u as XenoConnectorCapabilities, v as XenoCredentialSubmission, w as XenoFetchRequest, x as XenoFetchResponse, y as XenoFileReadRequest, z as XenoFileReadResponse, A as XenoRefreshPolicy, D as XenoSqlTable, E as browserFetch, F as classifyBrowserFetchFailure, G as defaultCapabilitiesFor, H as errorResult, I as evaluateSupport, J as facilitiesOf, K as normalizePaging, L as secretHeaderNames } from '../transport-B1cdciP8.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Column layout — width, order, visibility and freezing.
|
|
10
|
+
*
|
|
11
|
+
* Layout is **panel state, not query state**: it persists through `serialize()` and never travels in
|
|
12
|
+
* a `XenoQuery`. Widths are keyed by `XenoColumn.id`, so a source that renames a column keeps its
|
|
13
|
+
* width and a source that returns two columns with the same display name keeps them apart (§6.2).
|
|
14
|
+
*
|
|
15
|
+
* All of it is pure so the geometry is unit-tested without a DOM — the same discipline that let the
|
|
16
|
+
* Transport panel's time math be tested without a clock.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Per-column layout overrides, keyed by column id. */
|
|
22
|
+
interface ColumnLayout {
|
|
23
|
+
/** Explicit widths in px. */
|
|
24
|
+
widths: Record<string, number>;
|
|
25
|
+
/** Explicit left-to-right order; ids absent from the result are ignored. */
|
|
26
|
+
order: string[];
|
|
27
|
+
/** Ids the user hid. */
|
|
28
|
+
hidden: string[];
|
|
29
|
+
/** How many leading columns are frozen (pinned) — counted in RENDER order. */
|
|
30
|
+
frozenCount: number;
|
|
31
|
+
}
|
|
32
|
+
/** An empty layout — every column visible, in source order, at the default width. */
|
|
33
|
+
declare const EMPTY_LAYOUT: ColumnLayout;
|
|
34
|
+
/** Default column width in px when neither the source nor the user set one. */
|
|
35
|
+
declare const DEFAULT_COLUMN_WIDTH = 140;
|
|
36
|
+
/** Minimum width a column can be dragged to — below this a header is unreadable and unclickable. */
|
|
37
|
+
declare const MIN_COLUMN_WIDTH = 40;
|
|
38
|
+
/** A column resolved for rendering: the source column plus its resolved geometry. */
|
|
39
|
+
interface ResolvedColumn {
|
|
40
|
+
/** The source column. */
|
|
41
|
+
column: XenoColumn;
|
|
42
|
+
/** Resolved width in px. */
|
|
43
|
+
width: number;
|
|
44
|
+
/** Distance from the left edge of the full column strip, in px. */
|
|
45
|
+
offset: number;
|
|
46
|
+
/** Frozen (pinned) — rendered in the sticky leading region. */
|
|
47
|
+
frozen: boolean;
|
|
48
|
+
/** Index in render order. */
|
|
49
|
+
index: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the render-order column list with widths and offsets.
|
|
53
|
+
*
|
|
54
|
+
* Ordering rule: ids named in `layout.order` come first in that order, then any remaining source
|
|
55
|
+
* columns in **source order**. A source that adds a column therefore appends it rather than
|
|
56
|
+
* silently dropping it — the failure mode of a layout that stores a closed list.
|
|
57
|
+
*
|
|
58
|
+
* @param columns - The result's columns.
|
|
59
|
+
* @param layout - Persisted layout state.
|
|
60
|
+
* @returns Visible columns, resolved.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* const resolved = resolveColumns(result.columns, layout)
|
|
65
|
+
* const totalWidth = resolved.at(-1)!.offset + resolved.at(-1)!.width
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
declare function resolveColumns(columns: readonly XenoColumn[], layout?: ColumnLayout): ResolvedColumn[];
|
|
69
|
+
/** Total width of the resolved column strip, in px. */
|
|
70
|
+
declare function totalWidth(resolved: readonly ResolvedColumn[]): number;
|
|
71
|
+
/** Width of the frozen (sticky) leading region, in px. */
|
|
72
|
+
declare function frozenWidth(resolved: readonly ResolvedColumn[]): number;
|
|
73
|
+
/**
|
|
74
|
+
* Set a column's width, clamped to the minimum.
|
|
75
|
+
*
|
|
76
|
+
* @param layout - Current layout.
|
|
77
|
+
* @param columnId - Column to resize.
|
|
78
|
+
* @param width - Requested width in px.
|
|
79
|
+
* @returns A new layout (the input is not mutated).
|
|
80
|
+
*/
|
|
81
|
+
declare function setColumnWidth(layout: ColumnLayout, columnId: string, width: number): ColumnLayout;
|
|
82
|
+
/**
|
|
83
|
+
* Move a column to a new index in render order.
|
|
84
|
+
*
|
|
85
|
+
* The order list is rewritten from the CURRENT resolved order, not patched, so a reorder is
|
|
86
|
+
* meaningful even the first time (when `layout.order` is still empty).
|
|
87
|
+
*
|
|
88
|
+
* @param layout - Current layout.
|
|
89
|
+
* @param columns - The result's columns (for the current order).
|
|
90
|
+
* @param columnId - Column to move.
|
|
91
|
+
* @param toIndex - Destination index among visible columns.
|
|
92
|
+
* @returns A new layout.
|
|
93
|
+
*/
|
|
94
|
+
declare function moveColumn(layout: ColumnLayout, columns: readonly XenoColumn[], columnId: string, toIndex: number): ColumnLayout;
|
|
95
|
+
/**
|
|
96
|
+
* Toggle a column's visibility.
|
|
97
|
+
*
|
|
98
|
+
* @param layout - Current layout.
|
|
99
|
+
* @param columnId - Column to toggle.
|
|
100
|
+
* @param visible - Desired visibility; omitted flips.
|
|
101
|
+
* @returns A new layout.
|
|
102
|
+
*/
|
|
103
|
+
declare function setColumnVisible(layout: ColumnLayout, columnId: string, visible?: boolean): ColumnLayout;
|
|
104
|
+
/**
|
|
105
|
+
* Set how many leading columns are frozen.
|
|
106
|
+
*
|
|
107
|
+
* @param layout - Current layout.
|
|
108
|
+
* @param count - Number of columns to pin; clamped to `>= 0`.
|
|
109
|
+
* @returns A new layout.
|
|
110
|
+
*/
|
|
111
|
+
declare function setFrozenCount(layout: ColumnLayout, count: number): ColumnLayout;
|
|
112
|
+
/** Normalize an unknown (deserialized) layout, dropping anything malformed. */
|
|
113
|
+
declare function normalizeLayout(raw: unknown): ColumnLayout;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Panel-local types for `xeno.core.table`.
|
|
117
|
+
*
|
|
118
|
+
* The data contract itself lives in `@xenosystem/data-core` — this file holds only what the PANEL owns:
|
|
119
|
+
* view mode, selection, the cell-edit intent, and the serialized shape.
|
|
120
|
+
*
|
|
121
|
+
* @module
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* How the row set is laid out.
|
|
126
|
+
*
|
|
127
|
+
* v1 ships `grid` and `pivot`. `board` and `gallery` are reserved in the union — they are the same
|
|
128
|
+
* rows and the same cell editor with a different layout function, and reserving the values now means
|
|
129
|
+
* adding them later is not a config migration. See the README for why they are not in v1.
|
|
130
|
+
*/
|
|
131
|
+
type TableViewMode = 'grid' | 'pivot' | 'board' | 'gallery';
|
|
132
|
+
/** Every view mode the panel currently implements. */
|
|
133
|
+
declare const IMPLEMENTED_VIEW_MODES: readonly TableViewMode[];
|
|
134
|
+
/** An edit the user performed on a cell. Emitted as an INTENT — the host writes. */
|
|
135
|
+
interface XenoCellEdit {
|
|
136
|
+
/** Row to edit. */
|
|
137
|
+
rowId: string;
|
|
138
|
+
/** Column to edit. */
|
|
139
|
+
columnId: string;
|
|
140
|
+
/** The new value. */
|
|
141
|
+
value: XenoValue;
|
|
142
|
+
/** The value before the edit, so a host can build an undo entry without re-reading. */
|
|
143
|
+
previousValue: XenoValue;
|
|
144
|
+
/**
|
|
145
|
+
* Gesture correlation id. Every preview and the final commit of one edit share it, so a host
|
|
146
|
+
* coalesces them into **one** undo entry.
|
|
147
|
+
*/
|
|
148
|
+
gestureId: string;
|
|
149
|
+
}
|
|
150
|
+
/** A row activation (double-click / Enter) — the event that drives BACK-view execution. */
|
|
151
|
+
interface XenoRowActivate {
|
|
152
|
+
/** The activated row. */
|
|
153
|
+
rowId: string;
|
|
154
|
+
/** The column the activation happened in, when it was a cell. */
|
|
155
|
+
columnId?: string;
|
|
156
|
+
}
|
|
157
|
+
/** The panel's serialized state. `.xapp` stores this; it never stores a result. */
|
|
158
|
+
interface TablePanelState {
|
|
159
|
+
/** The query — **this persists**. */
|
|
160
|
+
query: XenoQuery;
|
|
161
|
+
/** View mode. */
|
|
162
|
+
viewMode: TableViewMode;
|
|
163
|
+
/** Column widths / order / hidden / frozen. */
|
|
164
|
+
layout: ColumnLayout;
|
|
165
|
+
/** Ids of collapsed groups, serialized from their structured keys. */
|
|
166
|
+
collapsedGroups: string[];
|
|
167
|
+
/** Selected row ids. */
|
|
168
|
+
selection: string[];
|
|
169
|
+
}
|
|
170
|
+
/** What the panel exposes to its view layer. */
|
|
171
|
+
interface TableViewState {
|
|
172
|
+
/** The newest result, or `null` before the first one lands. */
|
|
173
|
+
result: XenoResultSet | null;
|
|
174
|
+
/** The query the panel intends. Chrome renders from `result.query` when one exists. */
|
|
175
|
+
query: XenoQuery;
|
|
176
|
+
/** Current view mode. */
|
|
177
|
+
viewMode: TableViewMode;
|
|
178
|
+
/** Column layout. */
|
|
179
|
+
layout: ColumnLayout;
|
|
180
|
+
/** Serialized keys of collapsed groups. */
|
|
181
|
+
collapsedGroups: string[];
|
|
182
|
+
/** Selected row ids. */
|
|
183
|
+
selection: string[];
|
|
184
|
+
/** The focused cell, when the grid has a cursor. */
|
|
185
|
+
cursor: {
|
|
186
|
+
rowIndex: number;
|
|
187
|
+
columnIndex: number;
|
|
188
|
+
} | null;
|
|
189
|
+
/** The cell currently being edited. */
|
|
190
|
+
editing: {
|
|
191
|
+
rowId: string;
|
|
192
|
+
columnId: string;
|
|
193
|
+
draft: string;
|
|
194
|
+
} | null;
|
|
195
|
+
/** A request is outstanding. */
|
|
196
|
+
pending: boolean;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The host seam the controller emits through. */
|
|
200
|
+
interface TableHostBridge {
|
|
201
|
+
/** Emit on a declared output port. */
|
|
202
|
+
emit(portId: string, value: unknown): void;
|
|
203
|
+
/** Generate a correlation id. Injectable so tests are deterministic. */
|
|
204
|
+
makeId?: () => string;
|
|
205
|
+
}
|
|
206
|
+
/** Construction options. */
|
|
207
|
+
interface TableControllerOptions {
|
|
208
|
+
/** The host bridge. */
|
|
209
|
+
host: TableHostBridge;
|
|
210
|
+
/** Initial state (from `deserialize`). */
|
|
211
|
+
initial?: Partial<TablePanelState>;
|
|
212
|
+
/** Rows per page. Default 100. */
|
|
213
|
+
pageSize?: number;
|
|
214
|
+
/** Emit `editPreview` while typing. Default `false` — cell editing is commit-on-blur by nature. */
|
|
215
|
+
livePreview?: boolean;
|
|
216
|
+
}
|
|
217
|
+
/** Default page size. */
|
|
218
|
+
declare const DEFAULT_PAGE_SIZE = 100;
|
|
219
|
+
/** Structural equality for queries — the diff-before-emit gate. */
|
|
220
|
+
declare function queriesEqual(a: XenoQuery | undefined, b: XenoQuery | undefined): boolean;
|
|
221
|
+
/** The Table panel controller. */
|
|
222
|
+
declare class TableController {
|
|
223
|
+
private readonly host;
|
|
224
|
+
private readonly makeId;
|
|
225
|
+
private readonly pageSize;
|
|
226
|
+
private readonly livePreview;
|
|
227
|
+
private readonly listeners;
|
|
228
|
+
private result;
|
|
229
|
+
private query;
|
|
230
|
+
private viewMode;
|
|
231
|
+
private layout;
|
|
232
|
+
private collapsedGroups;
|
|
233
|
+
private selection;
|
|
234
|
+
private cursor;
|
|
235
|
+
private editing;
|
|
236
|
+
/** The newest emitted request id. Anything else that arrives is stale and dropped. */
|
|
237
|
+
private outstandingId;
|
|
238
|
+
/** The last query actually emitted, for diff-before-emit. */
|
|
239
|
+
private lastEmitted;
|
|
240
|
+
/** Cached view snapshot, so `useSyncExternalStore` sees a stable reference between changes. */
|
|
241
|
+
private snapshot;
|
|
242
|
+
constructor(options: TableControllerOptions);
|
|
243
|
+
/** Subscribe to state changes. Returns an unsubscribe function. */
|
|
244
|
+
subscribe: (listener: () => void) => (() => void);
|
|
245
|
+
/** The current view snapshot (stable reference until something changes). */
|
|
246
|
+
getState: () => TableViewState;
|
|
247
|
+
private notify;
|
|
248
|
+
/**
|
|
249
|
+
* Deliver a result.
|
|
250
|
+
*
|
|
251
|
+
* @param payload - `{requestId, result}` — the id must be echoed from the request.
|
|
252
|
+
* @returns `true` if the result was accepted, `false` if it was dropped as stale.
|
|
253
|
+
*/
|
|
254
|
+
deliverResult(payload: {
|
|
255
|
+
requestId?: string;
|
|
256
|
+
result: XenoResultSet;
|
|
257
|
+
} | XenoResultSet): boolean;
|
|
258
|
+
/**
|
|
259
|
+
* Apply an incremental update from a live source.
|
|
260
|
+
*
|
|
261
|
+
* @param delta - The delta.
|
|
262
|
+
* @returns `true` if applied, `false` if there is nothing to apply it to.
|
|
263
|
+
*/
|
|
264
|
+
deliverDelta(delta: _xenosystem_data_core.XenoResultDelta): boolean;
|
|
265
|
+
/** Selection may reference rows the newest page no longer contains — drop those. */
|
|
266
|
+
private pruneSelection;
|
|
267
|
+
/**
|
|
268
|
+
* Emit a query request, unless the query is structurally unchanged.
|
|
269
|
+
*
|
|
270
|
+
* @param reason - Why the query changed (telemetry + host debounce hints).
|
|
271
|
+
* @param force - Emit even if unchanged (an explicit refresh).
|
|
272
|
+
* @returns The emitted request id, or `null` if the emission was suppressed.
|
|
273
|
+
*/
|
|
274
|
+
requestQuery(reason: _xenosystem_data_core.XenoQueryRequest['reason'], force?: boolean): string | null;
|
|
275
|
+
/** Replace the query wholesale (from a wired `fields` panel) and request it. */
|
|
276
|
+
setQuery(query: XenoQuery): void;
|
|
277
|
+
/**
|
|
278
|
+
* Cycle a column's sort: none → asc → desc → none.
|
|
279
|
+
*
|
|
280
|
+
* @param columnId - The column.
|
|
281
|
+
* @param additive - Keep existing terms and append/update this one (Shift-click).
|
|
282
|
+
*/
|
|
283
|
+
toggleSort(columnId: string, additive?: boolean): void;
|
|
284
|
+
/** Replace the filter tree. */
|
|
285
|
+
setFilters(filters: XenoQuery['filters']): void;
|
|
286
|
+
/** Replace the grouping levels. */
|
|
287
|
+
setGroupBy(groupBy: XenoQuery['groupBy']): void;
|
|
288
|
+
/** Replace the pivot spec. Switching it on also switches the view mode. */
|
|
289
|
+
setPivot(pivot: XenoQuery['pivot']): void;
|
|
290
|
+
/** Go to a page by index (0-based). */
|
|
291
|
+
goToPage(index: number): void;
|
|
292
|
+
/** Step one page forward/backward, clamped to what the result says exists. */
|
|
293
|
+
stepPage(direction: 1 | -1): void;
|
|
294
|
+
/** Change the page size, returning to the first page. */
|
|
295
|
+
setPageSize(limit: number): void;
|
|
296
|
+
/** Re-run the current query unchanged. */
|
|
297
|
+
refresh(): void;
|
|
298
|
+
private firstPage;
|
|
299
|
+
/**
|
|
300
|
+
* The query the CHROME should render from.
|
|
301
|
+
*
|
|
302
|
+
* `result.query` when there is one and the result is settled; the local intent only while a
|
|
303
|
+
* request is in flight. This is invariant 2, in one method.
|
|
304
|
+
*/
|
|
305
|
+
effectiveQuery(): XenoQuery;
|
|
306
|
+
/** Switch view mode. Unimplemented modes are refused rather than rendering blank. */
|
|
307
|
+
setViewMode(mode: TableViewMode): boolean;
|
|
308
|
+
/** Resolved columns for the current result + layout. */
|
|
309
|
+
resolvedColumns(): ResolvedColumn[];
|
|
310
|
+
/** Set a column width. */
|
|
311
|
+
resizeColumn(columnId: string, width: number): void;
|
|
312
|
+
/** Move a column in render order. */
|
|
313
|
+
reorderColumn(columnId: string, toIndex: number): void;
|
|
314
|
+
/** Show/hide a column. */
|
|
315
|
+
toggleColumn(columnId: string, visible?: boolean): void;
|
|
316
|
+
/** Freeze the leading `count` columns. */
|
|
317
|
+
freezeColumns(count: number): void;
|
|
318
|
+
/** Collapse/expand a group by its serialized key. */
|
|
319
|
+
toggleGroup(serializedKey: string): void;
|
|
320
|
+
/** Select rows. `additive` toggles membership instead of replacing. */
|
|
321
|
+
select(rowIds: readonly string[], additive?: boolean): void;
|
|
322
|
+
/** Move the cell cursor, clamped to the grid. */
|
|
323
|
+
moveCursor(delta: {
|
|
324
|
+
rows?: number;
|
|
325
|
+
columns?: number;
|
|
326
|
+
}): void;
|
|
327
|
+
/** Put the cursor at an explicit address. */
|
|
328
|
+
setCursor(address: {
|
|
329
|
+
rowIndex: number;
|
|
330
|
+
columnIndex: number;
|
|
331
|
+
} | null): void;
|
|
332
|
+
/** Emit a row activation (double-click / Enter) — the BACK-view execution trigger. */
|
|
333
|
+
activateRow(rowId: string, columnId?: string): void;
|
|
334
|
+
/**
|
|
335
|
+
* Begin editing a cell.
|
|
336
|
+
*
|
|
337
|
+
* @param rowId - Row.
|
|
338
|
+
* @param columnId - Column.
|
|
339
|
+
* @returns `true` if editing started; `false` when the column is not editable or the row is gone.
|
|
340
|
+
*/
|
|
341
|
+
beginEdit(rowId: string, columnId: string): boolean;
|
|
342
|
+
/** Update the in-flight draft. Emits `editPreview` only when `livePreview` is on. */
|
|
343
|
+
updateDraft(text: string): void;
|
|
344
|
+
/**
|
|
345
|
+
* Commit the edit.
|
|
346
|
+
*
|
|
347
|
+
* Rejects invalid input by REVERTING rather than committing a coerced value — the whole point of
|
|
348
|
+
* inheriting the Inspector's parse-as-a-result contract. Also suppresses a no-op commit, so
|
|
349
|
+
* tabbing through a column does not write an undo entry per cell.
|
|
350
|
+
*
|
|
351
|
+
* @returns `true` if an `edit` intent was emitted.
|
|
352
|
+
*/
|
|
353
|
+
commitEdit(): boolean;
|
|
354
|
+
/** Abandon the edit. Escape reverts; nothing is emitted. */
|
|
355
|
+
cancelEdit(): void;
|
|
356
|
+
private buildEdit;
|
|
357
|
+
/** Parse a draft according to the column's declared type. */
|
|
358
|
+
private parseDraft;
|
|
359
|
+
private rowById;
|
|
360
|
+
private columnById;
|
|
361
|
+
/** Format a cell for display — the column's format, honoured. */
|
|
362
|
+
displayValue(row: XenoRow, column: XenoColumn): string;
|
|
363
|
+
/**
|
|
364
|
+
* Serialize the panel's state.
|
|
365
|
+
*
|
|
366
|
+
* **The query persists; the result never does.** A result is transient and can be megabytes; an
|
|
367
|
+
* `.xapp` that embeds one is both stale and enormous the moment it is opened.
|
|
368
|
+
*/
|
|
369
|
+
serialize(): TablePanelState;
|
|
370
|
+
/** Restore serialized state. Does NOT emit — the caller decides when to ask for data. */
|
|
371
|
+
deserialize(state: Partial<TablePanelState>): void;
|
|
372
|
+
/** Drop listeners and in-flight state. */
|
|
373
|
+
dispose(): void;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The `PanelModule` — what a host registers and mounts.
|
|
378
|
+
*
|
|
379
|
+
* Everything the panel does flows through the injected {@link PanelHost}: inputs arrive on
|
|
380
|
+
* `onInput`, commands on `onCommand`, and every output leaves through `host.emit`. There is no
|
|
381
|
+
* other door.
|
|
382
|
+
*
|
|
383
|
+
* @module
|
|
384
|
+
*/
|
|
385
|
+
|
|
386
|
+
/** Everything a renderer needs: the controller plus the host's resolved config. */
|
|
387
|
+
interface TableRenderContext {
|
|
388
|
+
controller: TableController;
|
|
389
|
+
/**
|
|
390
|
+
* The host's config, already defaulted from the manifest.
|
|
391
|
+
*
|
|
392
|
+
* Passed in because a renderer that cannot see config makes every manifest-declared display
|
|
393
|
+
* option unreachable — the panel would document `rowHeight` and `zebra` and then ignore them.
|
|
394
|
+
*/
|
|
395
|
+
config: {
|
|
396
|
+
rowHeight: number;
|
|
397
|
+
showFooter: boolean;
|
|
398
|
+
zebra: boolean;
|
|
399
|
+
showRowNumbers: boolean;
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/** Options for {@link createTablePanel}. */
|
|
403
|
+
interface CreateTablePanelOptions {
|
|
404
|
+
/**
|
|
405
|
+
* Override the renderer.
|
|
406
|
+
*
|
|
407
|
+
* **Rarely needed.** The module wires {@link TablePanelView} itself, which is what keeps `react`
|
|
408
|
+
* and `react-dom` resolving to ONE copy: if the host calls `createRoot` against a view imported
|
|
409
|
+
* from the package, a `file:`/workspace link gives the host's `react-dom` and the package's
|
|
410
|
+
* `react` two different copies and every hook throws *"Invalid hook call"*. Vitest hides this
|
|
411
|
+
* (its React plugin dedupes implicitly); a real app does not.
|
|
412
|
+
*
|
|
413
|
+
* Supply this only for a non-React host or a test double.
|
|
414
|
+
*/
|
|
415
|
+
render?: (root: HTMLElement, context: TableRenderContext) => () => void;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Build the Table panel module.
|
|
419
|
+
*
|
|
420
|
+
* @param options - Optional renderer.
|
|
421
|
+
* @returns The module.
|
|
422
|
+
*/
|
|
423
|
+
declare function createTablePanel(options?: CreateTablePanelOptions): PanelModule;
|
|
424
|
+
/**
|
|
425
|
+
* The default Table panel module — view already wired.
|
|
426
|
+
*
|
|
427
|
+
* **Register THIS**, not `createTablePanel()` plus a hand-rolled renderer. Every other canonical
|
|
428
|
+
* panel exports the same shape, and the reason is not symmetry: it is that mounting the view inside
|
|
429
|
+
* the package is what guarantees a single React copy.
|
|
430
|
+
*/
|
|
431
|
+
declare const tablePanel: PanelModule;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* The `xeno.core.table` manifest.
|
|
435
|
+
*
|
|
436
|
+
* Ports follow the union spec §5 sketch: `result` + `query` in, `queryRequest` + `edit` +
|
|
437
|
+
* `selection` + `rowActivate` out. Every structured port carries a **shape tag**, so the builder
|
|
438
|
+
* cannot wire a Layers `op` into `result` (SDK 1.1's `PanelPort.schema`).
|
|
439
|
+
*
|
|
440
|
+
* **Capabilities: `storage.local` ONLY.** Zero network, zero fs, zero gpu — query execution stays
|
|
441
|
+
* host-side, so the panel is fully sandboxable under `iframe-quickjs`, web-exportable, and
|
|
442
|
+
* marketplace-shippable. That property is the whole point of the request/response seam.
|
|
443
|
+
*
|
|
444
|
+
* @module
|
|
445
|
+
*/
|
|
446
|
+
|
|
447
|
+
/** The canonical manifest id. */
|
|
448
|
+
declare const TABLE_PANEL_ID = "xeno.core.table";
|
|
449
|
+
/** The `xeno.core.table` manifest. */
|
|
450
|
+
declare const tableManifest: PanelManifest;
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Grid viewport math — the windowing that keeps a wide, tall table cheap.
|
|
454
|
+
*
|
|
455
|
+
* Rendering discipline taken from sheets' `Grid.tsx` as a **reference only** — its viewport
|
|
456
|
+
* derivation, its `ResizeObserver`-driven measurement, and its 2×`requestAnimationFrame` defer.
|
|
457
|
+
* None of its cell/formula/A1 semantics come along: this grid renders a `XenoResultSet`, which has
|
|
458
|
+
* ids and types, not a spreadsheet address space.
|
|
459
|
+
*
|
|
460
|
+
* Rows use the fixed-height virtualizer (`virtualize.ts`). Columns need their own pass because they
|
|
461
|
+
* are variable width, so the window is found by binary search over the resolved offsets — O(log n)
|
|
462
|
+
* per scroll instead of the O(n) scan a naive loop does on every frame of a horizontal drag.
|
|
463
|
+
*
|
|
464
|
+
* @module
|
|
465
|
+
*/
|
|
466
|
+
|
|
467
|
+
/** The visible column window. */
|
|
468
|
+
interface ColumnWindow {
|
|
469
|
+
/** First rendered column index (inclusive). */
|
|
470
|
+
startIndex: number;
|
|
471
|
+
/** Last rendered column index (exclusive). */
|
|
472
|
+
endIndex: number;
|
|
473
|
+
/** Left spacer width in px — the offset of `startIndex`. */
|
|
474
|
+
offsetLeft: number;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Index of the last column whose offset is `<= x`, by binary search.
|
|
478
|
+
*
|
|
479
|
+
* @param columns - Resolved columns, ascending by offset.
|
|
480
|
+
* @param x - Horizontal position in px.
|
|
481
|
+
* @returns The index, or `0` when the strip is empty.
|
|
482
|
+
*/
|
|
483
|
+
declare function columnIndexAt(columns: readonly ResolvedColumn[], x: number): number;
|
|
484
|
+
/**
|
|
485
|
+
* Compute the visible column window.
|
|
486
|
+
*
|
|
487
|
+
* Frozen columns are ALWAYS rendered — they are sticky, so scrolling them out of the window would
|
|
488
|
+
* make them disappear from a region that is still on screen. They are excluded from the spacer, and
|
|
489
|
+
* the window starts after them.
|
|
490
|
+
*
|
|
491
|
+
* @param columns - Resolved columns.
|
|
492
|
+
* @param scrollLeft - Horizontal scroll offset in px.
|
|
493
|
+
* @param viewportWidth - Visible width in px.
|
|
494
|
+
* @param overscan - Extra columns to render on each side. Default 1.
|
|
495
|
+
* @returns The window.
|
|
496
|
+
*/
|
|
497
|
+
declare function computeColumnWindow(columns: readonly ResolvedColumn[], scrollLeft: number, viewportWidth: number, overscan?: number): ColumnWindow;
|
|
498
|
+
/**
|
|
499
|
+
* The scroll position that brings a column fully into view, or `null` if it already is.
|
|
500
|
+
*
|
|
501
|
+
* Returning `null` for the no-op case is deliberate: a caller that always assigns `scrollLeft`
|
|
502
|
+
* cancels a user's in-flight momentum scroll on every keystroke.
|
|
503
|
+
*
|
|
504
|
+
* @param columns - Resolved columns.
|
|
505
|
+
* @param index - Target column index.
|
|
506
|
+
* @param scrollLeft - Current scroll offset.
|
|
507
|
+
* @param viewportWidth - Visible width.
|
|
508
|
+
* @returns The new `scrollLeft`, or `null`.
|
|
509
|
+
*/
|
|
510
|
+
declare function scrollLeftToReveal(columns: readonly ResolvedColumn[], index: number, scrollLeft: number, viewportWidth: number): number | null;
|
|
511
|
+
/**
|
|
512
|
+
* The scroll position that brings a row fully into view, or `null` if it already is.
|
|
513
|
+
*
|
|
514
|
+
* @param index - Row index.
|
|
515
|
+
* @param rowHeight - Fixed row height.
|
|
516
|
+
* @param scrollTop - Current vertical scroll offset.
|
|
517
|
+
* @param viewportHeight - Visible height.
|
|
518
|
+
* @returns The new `scrollTop`, or `null`.
|
|
519
|
+
*/
|
|
520
|
+
declare function scrollTopToReveal(index: number, rowHeight: number, scrollTop: number, viewportHeight: number): number | null;
|
|
521
|
+
/** A cell address within the rendered page. */
|
|
522
|
+
interface CellAddress {
|
|
523
|
+
/** Row index within the page. */
|
|
524
|
+
rowIndex: number;
|
|
525
|
+
/** Column index in render order. */
|
|
526
|
+
columnIndex: number;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Move a cell cursor, clamped to the grid.
|
|
530
|
+
*
|
|
531
|
+
* Clamping rather than wrapping: an arrow key at the last row should stop, not jump to the first —
|
|
532
|
+
* wrapping in a grid loses the user's place and is not what any spreadsheet does.
|
|
533
|
+
*
|
|
534
|
+
* @param from - Current address.
|
|
535
|
+
* @param delta - Movement in rows/columns.
|
|
536
|
+
* @param bounds - Grid extent.
|
|
537
|
+
* @returns The new address.
|
|
538
|
+
*/
|
|
539
|
+
declare function moveCell(from: CellAddress, delta: {
|
|
540
|
+
rows?: number;
|
|
541
|
+
columns?: number;
|
|
542
|
+
}, bounds: {
|
|
543
|
+
rowCount: number;
|
|
544
|
+
columnCount: number;
|
|
545
|
+
}): CellAddress;
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Fixed-height row virtualization — Canvas's architecture (union spec §1.2).
|
|
549
|
+
*
|
|
550
|
+
* Canvas added this after a documented ~200 ms-at-5k-layers wall; Pixel has none, which is why the
|
|
551
|
+
* parity gate includes a perf floor Pixel itself does not meet today (spec §6). Fixed `rowHeight`
|
|
552
|
+
* keeps the index math O(1) — no measurement, no prefix sums (variable height is v1.1).
|
|
553
|
+
*
|
|
554
|
+
* @module
|
|
555
|
+
*/
|
|
556
|
+
/** The slice of rows to render, plus the spacer geometry that preserves scroll extent. */
|
|
557
|
+
interface VirtualWindow {
|
|
558
|
+
/** First row index to render (inclusive), overscan included. */
|
|
559
|
+
startIndex: number;
|
|
560
|
+
/** Last row index to render (exclusive), overscan included. */
|
|
561
|
+
endIndex: number;
|
|
562
|
+
/** Top spacer height in px — `startIndex * rowHeight`. */
|
|
563
|
+
offsetTop: number;
|
|
564
|
+
/** Total scrollable height in px — `rowCount * rowHeight`. */
|
|
565
|
+
totalHeight: number;
|
|
566
|
+
}
|
|
567
|
+
/** Inputs for {@link computeWindow}. */
|
|
568
|
+
interface WindowInput {
|
|
569
|
+
rowCount: number;
|
|
570
|
+
rowHeight: number;
|
|
571
|
+
scrollTop: number;
|
|
572
|
+
viewportHeight: number;
|
|
573
|
+
/** Extra rows rendered above and below the viewport. Canvas ships 8. */
|
|
574
|
+
overscan?: number;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Compute the visible row window.
|
|
578
|
+
*
|
|
579
|
+
* Clamped and NaN-safe: a zero/unknown viewport (before the first `ResizeObserver` tick) yields an
|
|
580
|
+
* empty-but-valid window rather than rendering the whole list.
|
|
581
|
+
*
|
|
582
|
+
* @param input - Row count, fixed row height, scroll offset, viewport height, overscan.
|
|
583
|
+
* @returns The {@link VirtualWindow} to render.
|
|
584
|
+
*/
|
|
585
|
+
declare function computeWindow(input: WindowInput): VirtualWindow;
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Numeric cell parsing — **inherited wholesale from `@xenosystem/panel-inspector`'s `numeric.ts`**, not
|
|
589
|
+
* re-invented.
|
|
590
|
+
*
|
|
591
|
+
* The union spec (§6.3) names `parseFloat(e.target.value) || 0` in notes' number-cell editor as a
|
|
592
|
+
* live instance of checklist #91 (`x || default` where `0` is meaningful) *in the richest data
|
|
593
|
+
* surface in the ecosystem*: an empty box, a typo, or a half-typed `-` all commit a silent `0` over
|
|
594
|
+
* whatever the cell held. The spec's remedy is explicit — the cell editor **inherits the Inspector's
|
|
595
|
+
* numeric-field synthesis, not a re-implementation** — because a second implementation is a second
|
|
596
|
+
* chance to reintroduce the bug.
|
|
597
|
+
*
|
|
598
|
+
* Copied rather than imported: panels are standalone (zero cross-panel dependencies), the same rule
|
|
599
|
+
* under which `throttle` and `virtualize` are copied. The Inspector's file is the SOURCE OF TRUTH —
|
|
600
|
+
* a fix there must be mirrored here, and both are covered by the same assertions.
|
|
601
|
+
*
|
|
602
|
+
* Only the parse/clamp/precision half is carried over; the Inspector's slider, log-scale and scrub
|
|
603
|
+
* math have no cell-editor analogue.
|
|
604
|
+
*
|
|
605
|
+
* @module
|
|
606
|
+
*/
|
|
607
|
+
/** Parse outcome. Invalid input is a first-class case, never a silent `0`. */
|
|
608
|
+
type ParseResult = {
|
|
609
|
+
ok: true;
|
|
610
|
+
value: number;
|
|
611
|
+
} | {
|
|
612
|
+
ok: false;
|
|
613
|
+
reason: 'empty' | 'nan';
|
|
614
|
+
};
|
|
615
|
+
/** Constraints a numeric cell can carry (read off the column's format). */
|
|
616
|
+
interface NumericConstraints {
|
|
617
|
+
min?: number;
|
|
618
|
+
max?: number;
|
|
619
|
+
step?: number;
|
|
620
|
+
precision?: number;
|
|
621
|
+
/** Reject a fractional result — an `integer` column cannot hold `1.5`. */
|
|
622
|
+
integer?: boolean;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Parse user text into a number.
|
|
626
|
+
*
|
|
627
|
+
* Accepts leading/trailing whitespace, a leading `+`/`-`, decimals, exponents, and a trailing unit
|
|
628
|
+
* suffix (`12px`, `45°`, `50 %`) so a suffix the panel itself rendered round-trips. Everything else
|
|
629
|
+
* is REJECTED — including empty input, a bare `-`, and `NaN`/`Infinity`.
|
|
630
|
+
*
|
|
631
|
+
* @param text - Raw input.
|
|
632
|
+
* @returns `{ok:true, value}` or `{ok:false, reason}` — never a coerced number.
|
|
633
|
+
*/
|
|
634
|
+
declare function parseNumeric(text: string): ParseResult;
|
|
635
|
+
/** Clamp to `[min, max]` when declared. */
|
|
636
|
+
declare function clamp(value: number, { min, max }: NumericConstraints): number;
|
|
637
|
+
/** Round to the declared precision (decimal places). No precision ⇒ unchanged. */
|
|
638
|
+
declare function applyPrecision(value: number, precision?: number): number;
|
|
639
|
+
/**
|
|
640
|
+
* Clamp + round in one step — the canonical "make this value legal" helper.
|
|
641
|
+
*
|
|
642
|
+
* @param value - The parsed number.
|
|
643
|
+
* @param c - Constraints.
|
|
644
|
+
* @returns A value that satisfies every declared constraint.
|
|
645
|
+
*/
|
|
646
|
+
declare function normalizeNumeric(value: number, c: NumericConstraints): number;
|
|
647
|
+
/**
|
|
648
|
+
* Read the numeric constraints a column implies.
|
|
649
|
+
*
|
|
650
|
+
* @param column - The column.
|
|
651
|
+
* @returns Constraints for {@link normalizeNumeric}.
|
|
652
|
+
*/
|
|
653
|
+
declare function constraintsOfColumn(column: {
|
|
654
|
+
type: string;
|
|
655
|
+
format?: {
|
|
656
|
+
precision?: number;
|
|
657
|
+
};
|
|
658
|
+
}): NumericConstraints;
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* The Table panel's React view — grid + pivot.
|
|
662
|
+
*
|
|
663
|
+
* **First real consumer of `@xenosystem/workbench/primitives`.** Everything that is chrome (toolbar,
|
|
664
|
+
* status strip, empty/loading/error states, badges, icon buttons, segmented control) comes from the
|
|
665
|
+
* primitives; only the grid body itself — which is the panel's actual job — is local.
|
|
666
|
+
*
|
|
667
|
+
* The view is deliberately thin: all state lives in {@link TableController}, bound through
|
|
668
|
+
* `useSyncExternalStore`. Rendering is the only thing that happens here.
|
|
669
|
+
*
|
|
670
|
+
* @module
|
|
671
|
+
*/
|
|
672
|
+
|
|
673
|
+
/** Props for {@link TablePanelView}. */
|
|
674
|
+
interface TablePanelViewProps {
|
|
675
|
+
/** The controller. */
|
|
676
|
+
controller: TableController;
|
|
677
|
+
/** Fixed row height in px (config `rowHeight`). */
|
|
678
|
+
rowHeight?: number;
|
|
679
|
+
/** Render the grand-totals footer. */
|
|
680
|
+
showFooter?: boolean;
|
|
681
|
+
/** Shade alternate rows. */
|
|
682
|
+
zebra?: boolean;
|
|
683
|
+
/**
|
|
684
|
+
* Render a leading ordinal gutter.
|
|
685
|
+
*
|
|
686
|
+
* U5: the manifest declared this from day one and the view never rendered it — declared config
|
|
687
|
+
* with no implementation. The numbers are 1-based over the CURRENT PAGE plus the query offset,
|
|
688
|
+
* so row 1 of page 2 reads as its absolute position rather than restarting.
|
|
689
|
+
*/
|
|
690
|
+
showRowNumbers?: boolean;
|
|
691
|
+
}
|
|
692
|
+
/** The Table panel view. */
|
|
693
|
+
declare function TablePanelView({ controller, showRowNumbers, rowHeight, showFooter, zebra, }: TablePanelViewProps): ReactNode;
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* The `xeno.core.chart` contract.
|
|
697
|
+
*
|
|
698
|
+
* ## 🔴 Landmine §6.11, stated as a type
|
|
699
|
+
*
|
|
700
|
+
* sheets' chart is bound to a **viewport selection**, its type lives in component `useState` (lost
|
|
701
|
+
* on unmount), and its `ChartConfig` is written but never read. All three are the same mistake:
|
|
702
|
+
* chart state that is not part of the panel's serialized model.
|
|
703
|
+
*
|
|
704
|
+
* Here, data always arrives as a **`XenoResultSet`** — never a viewport range — and the encoding is
|
|
705
|
+
* the panel's persisted state, round-tripped through `serialize()`. A chart that forgets what it was
|
|
706
|
+
* showing is a chart nobody trusts.
|
|
707
|
+
*
|
|
708
|
+
* @module
|
|
709
|
+
*/
|
|
710
|
+
|
|
711
|
+
/** Chart types v1 renders. */
|
|
712
|
+
type XenoChartType = 'bar' | 'horizontalBar' | 'line' | 'area' | 'scatter' | 'pie' | 'doughnut';
|
|
713
|
+
/** Every chart type, in menu order. */
|
|
714
|
+
declare const ALL_CHART_TYPES: readonly XenoChartType[];
|
|
715
|
+
/**
|
|
716
|
+
* How columns map to visual channels.
|
|
717
|
+
*
|
|
718
|
+
* **This is the panel's persisted state**, not component state — see the module note.
|
|
719
|
+
*/
|
|
720
|
+
interface XenoChartEncoding {
|
|
721
|
+
/** The mark. */
|
|
722
|
+
type: XenoChartType;
|
|
723
|
+
/** Category / time axis. */
|
|
724
|
+
x?: string;
|
|
725
|
+
/** Measure(s). More than one produces multiple series. */
|
|
726
|
+
y?: string[];
|
|
727
|
+
/** Column that splits `y` into one series per distinct value. */
|
|
728
|
+
series?: string;
|
|
729
|
+
/** Column driving mark colour. */
|
|
730
|
+
color?: string;
|
|
731
|
+
/** Column driving mark size (scatter). */
|
|
732
|
+
size?: string;
|
|
733
|
+
/** Aggregate applied to `y` when the query groups. */
|
|
734
|
+
aggregate?: XenoAggFn;
|
|
735
|
+
/** Stack series rather than grouping them. */
|
|
736
|
+
stacked?: boolean;
|
|
737
|
+
/** Line smoothing, `0`–`1`. */
|
|
738
|
+
smoothing?: number;
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* The monochrome series palette — SIX steps of `--xeno-chart-series-<n>`, and the fill ramp that pairs
|
|
742
|
+
* with it, `--xeno-chart-fill-<n>`. The VALUES live one rung down (`@xenosystem/elements/tokens`
|
|
743
|
+
* `chart`, emitted by elements-react's theme); a block may carry no colour literal. Until 2026-09-18
|
|
744
|
+
* these were twelve white-alpha strings lifted from sheets' `ChartModel`.
|
|
745
|
+
*
|
|
746
|
+
* A `var()` cannot reach a `<canvas>`, so a RENDERER resolves them against the element it draws in —
|
|
747
|
+
* `resolveChartColors(element)` — right before handing the config to Chart.js. The model stays pure
|
|
748
|
+
* and testable: it assigns var strings, never computed colours.
|
|
749
|
+
*/
|
|
750
|
+
declare const CHART_PALETTE_STEPS = 6;
|
|
751
|
+
declare const MONO_PALETTE: readonly string[];
|
|
752
|
+
/** The fill palette that pairs with it. */
|
|
753
|
+
declare const MONO_FILL_PALETTE: readonly string[];
|
|
754
|
+
/** The fill for a category with no value (a null slice). */
|
|
755
|
+
declare const CHART_EMPTY_FILL = "var(--xeno-border-subtle)";
|
|
756
|
+
/**
|
|
757
|
+
* A function that turns `var(--name)` strings into the computed value in scope at `element`. Anything
|
|
758
|
+
* that is not a `var()` passes through; a var the theme does not define resolves to `''` and Chart.js
|
|
759
|
+
* uses its own default for it — visible, never a wrong colour.
|
|
760
|
+
*/
|
|
761
|
+
declare function resolveChartColors(element: {
|
|
762
|
+
ownerDocument?: {
|
|
763
|
+
defaultView?: {
|
|
764
|
+
getComputedStyle(el: unknown): {
|
|
765
|
+
getPropertyValue(name: string): string;
|
|
766
|
+
};
|
|
767
|
+
} | null;
|
|
768
|
+
} | null;
|
|
769
|
+
} | null | undefined): (css: string) => string;
|
|
770
|
+
/** One plotted series. */
|
|
771
|
+
interface XenoChartSeries {
|
|
772
|
+
/** Stable id — the `series` value, or the `y` column id when there is no split. */
|
|
773
|
+
id: string;
|
|
774
|
+
/** Display label. */
|
|
775
|
+
label: string;
|
|
776
|
+
/** Points, aligned to {@link XenoChartData.labels} by index. `null` is a genuine gap. */
|
|
777
|
+
data: (number | null)[];
|
|
778
|
+
/** Stroke colour. */
|
|
779
|
+
color: string;
|
|
780
|
+
/** Fill colour, for area and bar marks. */
|
|
781
|
+
fill: string;
|
|
782
|
+
}
|
|
783
|
+
/** The renderer-agnostic plot model. */
|
|
784
|
+
interface XenoChartData {
|
|
785
|
+
/** Category labels, one per point index. */
|
|
786
|
+
labels: string[];
|
|
787
|
+
/** The series. */
|
|
788
|
+
series: XenoChartSeries[];
|
|
789
|
+
/** The encoding that produced it. */
|
|
790
|
+
encoding: XenoChartEncoding;
|
|
791
|
+
/**
|
|
792
|
+
* Set when the source has more points than `maxPoints`.
|
|
793
|
+
*
|
|
794
|
+
* The panel then requests an AGGREGATED query rather than downsampling: **silent downsampling is a
|
|
795
|
+
* lie about the data**, and a user cannot tell a thinned line from a real one.
|
|
796
|
+
*/
|
|
797
|
+
tooDense?: {
|
|
798
|
+
points: number;
|
|
799
|
+
limit: number;
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
/** A cross-filter highlight arriving from a sibling panel. */
|
|
803
|
+
type XenoChartHighlight = {
|
|
804
|
+
rowIds: string[];
|
|
805
|
+
} | {
|
|
806
|
+
columnId: string;
|
|
807
|
+
values: XenoValue[];
|
|
808
|
+
} | null;
|
|
809
|
+
/** What the panel emits when a mark is clicked. */
|
|
810
|
+
interface XenoChartDrillDown {
|
|
811
|
+
columnId: string;
|
|
812
|
+
value: XenoValue;
|
|
813
|
+
/** The series the mark belonged to, when there is one. */
|
|
814
|
+
seriesId?: string;
|
|
815
|
+
}
|
|
816
|
+
/** An export request. The HOST renders and writes — the panel never touches a filesystem. */
|
|
817
|
+
interface XenoChartExportRequest {
|
|
818
|
+
requestId: string;
|
|
819
|
+
format: 'png' | 'svg';
|
|
820
|
+
width: number;
|
|
821
|
+
height: number;
|
|
822
|
+
}
|
|
823
|
+
/** The panel's serialized state. **The encoding persists** — that is the §6.11 fix. */
|
|
824
|
+
interface ChartPanelState {
|
|
825
|
+
encoding: XenoChartEncoding;
|
|
826
|
+
}
|
|
827
|
+
/** What the controller exposes to its view. */
|
|
828
|
+
interface ChartViewState {
|
|
829
|
+
/** The plot model, or `null` before a usable result + encoding exist. */
|
|
830
|
+
data: XenoChartData | null;
|
|
831
|
+
/** The current encoding. */
|
|
832
|
+
encoding: XenoChartEncoding;
|
|
833
|
+
/** Columns available to encode. */
|
|
834
|
+
columns: XenoColumn[];
|
|
835
|
+
/** Chart types this host allows. */
|
|
836
|
+
allowedTypes: readonly XenoChartType[];
|
|
837
|
+
/** Highlighted row ids, from a sibling's cross-filter. */
|
|
838
|
+
highlightedRowIds: string[];
|
|
839
|
+
/** No result, or a result with no rows. */
|
|
840
|
+
empty: boolean;
|
|
841
|
+
/** The result was `partial`. */
|
|
842
|
+
partial: boolean;
|
|
843
|
+
/** A typed error from the result. */
|
|
844
|
+
errorMessage: string | null;
|
|
845
|
+
/** Set when the encoding cannot be plotted, with the reason. */
|
|
846
|
+
encodingProblem: string | null;
|
|
847
|
+
}
|
|
848
|
+
/** Marks that plot a single series against a category axis. */
|
|
849
|
+
declare function isCategorical(type: XenoChartType): boolean;
|
|
850
|
+
/** Marks that draw a filled region. */
|
|
851
|
+
declare function isFilled(type: XenoChartType): boolean;
|
|
852
|
+
/**
|
|
853
|
+
* Whether an encoding can be plotted, and if not, why.
|
|
854
|
+
*
|
|
855
|
+
* Returns a SENTENCE rather than a boolean: a chart that renders blank because the encoding is
|
|
856
|
+
* incomplete is indistinguishable from one that is broken.
|
|
857
|
+
*/
|
|
858
|
+
declare function validateEncoding(encoding: XenoChartEncoding, columns: readonly XenoColumn[]): string | null;
|
|
859
|
+
/** The default encoding for a result — the first time column vs the first measure. */
|
|
860
|
+
declare function defaultEncoding(columns: readonly XenoColumn[], type?: XenoChartType): XenoChartEncoding;
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* The Chart controller.
|
|
864
|
+
*
|
|
865
|
+
* @module
|
|
866
|
+
*/
|
|
867
|
+
|
|
868
|
+
/** The host seam. */
|
|
869
|
+
interface ChartHostBridge {
|
|
870
|
+
emit(portId: string, value: unknown): void;
|
|
871
|
+
makeId?: () => string;
|
|
872
|
+
}
|
|
873
|
+
/** Construction options. */
|
|
874
|
+
interface ChartControllerOptions {
|
|
875
|
+
host: ChartHostBridge;
|
|
876
|
+
initial?: Partial<ChartPanelState>;
|
|
877
|
+
/** Chart types this host permits. */
|
|
878
|
+
allowedTypes?: readonly XenoChartType[];
|
|
879
|
+
defaultType?: XenoChartType;
|
|
880
|
+
maxSeries?: number;
|
|
881
|
+
maxPoints?: number;
|
|
882
|
+
}
|
|
883
|
+
/** The Chart panel controller. */
|
|
884
|
+
declare class ChartController {
|
|
885
|
+
private readonly host;
|
|
886
|
+
private readonly makeId;
|
|
887
|
+
private readonly allowedTypes;
|
|
888
|
+
private readonly buildOptions;
|
|
889
|
+
private readonly listeners;
|
|
890
|
+
private result;
|
|
891
|
+
private encoding;
|
|
892
|
+
private highlight;
|
|
893
|
+
private snapshot;
|
|
894
|
+
/** Set once `tooDense` has been reported, so the request is not re-emitted every render. */
|
|
895
|
+
private densityReported;
|
|
896
|
+
constructor(options: ChartControllerOptions);
|
|
897
|
+
subscribe: (listener: () => void) => (() => void);
|
|
898
|
+
getState: () => ChartViewState;
|
|
899
|
+
private notify;
|
|
900
|
+
/**
|
|
901
|
+
* Receive a result.
|
|
902
|
+
*
|
|
903
|
+
* **Data always arrives as a result set** — never a viewport range. That is landmine §6.11: a
|
|
904
|
+
* chart bound to a selection cannot be saved, cannot be shared, and shows nothing the moment the
|
|
905
|
+
* selection moves.
|
|
906
|
+
*/
|
|
907
|
+
setResult(result: XenoResultSet): void;
|
|
908
|
+
/** Receive an externally authored encoding (from a wired `fields` panel). */
|
|
909
|
+
setEncoding(encoding: XenoChartEncoding, emit?: boolean): void;
|
|
910
|
+
/** Patch one channel. */
|
|
911
|
+
patchEncoding(patch: Partial<XenoChartEncoding>): void;
|
|
912
|
+
/** Change the mark. Refuses a type this host disallows. */
|
|
913
|
+
setType(type: XenoChartType): boolean;
|
|
914
|
+
/** Receive a cross-filter highlight from a sibling panel. */
|
|
915
|
+
setHighlight(highlight: XenoChartHighlight): void;
|
|
916
|
+
/** The plot model, or `null` when there is nothing plottable. */
|
|
917
|
+
buildData(): XenoChartData | null;
|
|
918
|
+
/** Row ids the current highlight resolves to. */
|
|
919
|
+
private resolveHighlight;
|
|
920
|
+
/**
|
|
921
|
+
* Ask for an aggregated query when the result is too dense to plot honestly.
|
|
922
|
+
*
|
|
923
|
+
* **Never a silent downsample.** A thinned line is indistinguishable from a real one, so the panel
|
|
924
|
+
* asks the resolver to aggregate instead — and asks once per encoding/result, not every render.
|
|
925
|
+
*/
|
|
926
|
+
private reportDensity;
|
|
927
|
+
/** A mark was clicked: emit the selection and a drill-down. */
|
|
928
|
+
activateMark(label: string, seriesId?: string): boolean;
|
|
929
|
+
/** A brush selected several categories. */
|
|
930
|
+
brush(labels: readonly string[]): boolean;
|
|
931
|
+
/**
|
|
932
|
+
* Ask the HOST to render an image.
|
|
933
|
+
*
|
|
934
|
+
* The panel never touches a filesystem — it declares `storage.local` and nothing else, which is
|
|
935
|
+
* only true because export is a request rather than a write.
|
|
936
|
+
*/
|
|
937
|
+
requestExport(format: 'png' | 'svg', width?: number, height?: number): string;
|
|
938
|
+
/**
|
|
939
|
+
* Serialize.
|
|
940
|
+
*
|
|
941
|
+
* **The encoding persists.** sheets keeps its chart type in component `useState` and loses it on
|
|
942
|
+
* unmount, and writes a `ChartConfig` nothing ever reads — the same bug twice.
|
|
943
|
+
*/
|
|
944
|
+
serialize(): ChartPanelState;
|
|
945
|
+
/** Restore. Does not emit. */
|
|
946
|
+
deserialize(state: Partial<ChartPanelState>): void;
|
|
947
|
+
/** Columns available to encode. */
|
|
948
|
+
columns(): XenoColumn[];
|
|
949
|
+
/** Drop listeners. */
|
|
950
|
+
dispose(): void;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* The renderer SEAM.
|
|
955
|
+
*
|
|
956
|
+
* v1 renders with Chart.js — proven in sheets' 195-LOC `ChartView` — but the panel never imports it
|
|
957
|
+
* directly. A renderer receives the derived {@link XenoChartData} and a host element, and returns a
|
|
958
|
+
* disposer. That is the whole contract.
|
|
959
|
+
*
|
|
960
|
+
* The seam exists because spectra's future Canvas2D/WebGPU engine must be a **swap, not a rewrite**:
|
|
961
|
+
* everything expensive (deriving series, resolving labels, palette assignment, density detection)
|
|
962
|
+
* lives in `model.ts` and is renderer-agnostic. A second renderer implements ~40 lines.
|
|
963
|
+
*
|
|
964
|
+
* @module
|
|
965
|
+
*/
|
|
966
|
+
|
|
967
|
+
/** What a renderer is handed. */
|
|
968
|
+
interface ChartRenderInput {
|
|
969
|
+
/** The element to draw into. */
|
|
970
|
+
element: HTMLElement;
|
|
971
|
+
/** The derived, renderer-agnostic model. */
|
|
972
|
+
data: XenoChartData;
|
|
973
|
+
/** Presentation toggles. */
|
|
974
|
+
options: ChartRenderOptions;
|
|
975
|
+
/** Row ids currently highlighted by a sibling's cross-filter. */
|
|
976
|
+
highlightedRowIds?: readonly string[];
|
|
977
|
+
/** Called when a mark is activated. */
|
|
978
|
+
onMarkActivate?: (label: string, seriesId?: string) => void;
|
|
979
|
+
}
|
|
980
|
+
/** Presentation toggles a renderer honours. */
|
|
981
|
+
interface ChartRenderOptions {
|
|
982
|
+
showLegend: boolean;
|
|
983
|
+
showGrid: boolean;
|
|
984
|
+
showTooltip: boolean;
|
|
985
|
+
stacked: boolean;
|
|
986
|
+
/** Line smoothing `0`–`1`. */
|
|
987
|
+
smoothing: number;
|
|
988
|
+
}
|
|
989
|
+
/** A renderer: draw, and hand back a disposer. */
|
|
990
|
+
type ChartRenderer = (input: ChartRenderInput) => () => void;
|
|
991
|
+
/**
|
|
992
|
+
* Install the renderer.
|
|
993
|
+
*
|
|
994
|
+
* @param next - The renderer, or `null` to uninstall.
|
|
995
|
+
*
|
|
996
|
+
* @example
|
|
997
|
+
* ```ts
|
|
998
|
+
* import { Chart } from 'chart.js/auto'
|
|
999
|
+
* setChartRenderer(createChartJsRenderer(Chart))
|
|
1000
|
+
* ```
|
|
1001
|
+
*/
|
|
1002
|
+
declare function setChartRenderer(next: ChartRenderer | null): void;
|
|
1003
|
+
/** The installed renderer. */
|
|
1004
|
+
declare function getChartRenderer(): ChartRenderer | null;
|
|
1005
|
+
/**
|
|
1006
|
+
* Chart.js dataset shape — derived from the model so a host can wire Chart.js in a few lines
|
|
1007
|
+
* without the panel importing it.
|
|
1008
|
+
*
|
|
1009
|
+
* Exported rather than inlined so the mapping is testable without a DOM or a chart library.
|
|
1010
|
+
*
|
|
1011
|
+
* @param data - The derived model.
|
|
1012
|
+
* @param options - Presentation toggles.
|
|
1013
|
+
* @returns A Chart.js-compatible `{type, data, options}` config.
|
|
1014
|
+
*/
|
|
1015
|
+
declare function toChartJsConfig(data: XenoChartData, options: ChartRenderOptions): Record<string, unknown>;
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* The `PanelModule` — view wired inside the package (one React copy).
|
|
1019
|
+
*
|
|
1020
|
+
* @module
|
|
1021
|
+
*/
|
|
1022
|
+
|
|
1023
|
+
/** Everything a renderer needs: the controller plus the resolved config. */
|
|
1024
|
+
interface ChartRenderContext {
|
|
1025
|
+
controller: ChartController;
|
|
1026
|
+
config: {
|
|
1027
|
+
options: ChartRenderOptions;
|
|
1028
|
+
emptyHint?: string;
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
/** Options for {@link createChartPanel}. */
|
|
1032
|
+
interface CreateChartPanelOptions {
|
|
1033
|
+
/** Override the view. Rarely needed — mounting inside the package keeps React singular. */
|
|
1034
|
+
render?: (root: HTMLElement, context: ChartRenderContext) => () => void;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Build the Chart panel module.
|
|
1038
|
+
*
|
|
1039
|
+
* @param options - Optional renderer override.
|
|
1040
|
+
* @returns The module.
|
|
1041
|
+
*/
|
|
1042
|
+
declare function createChartPanel(options?: CreateChartPanelOptions): PanelModule;
|
|
1043
|
+
/** The default Chart panel module — view already wired. Register THIS. */
|
|
1044
|
+
declare const chartPanel: PanelModule;
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* The `xeno.core.chart` manifest.
|
|
1048
|
+
*
|
|
1049
|
+
* Capabilities: `storage.local` only — export is a REQUEST to the host, never a write, which is what
|
|
1050
|
+
* keeps the capability list at one entry.
|
|
1051
|
+
*
|
|
1052
|
+
* @module
|
|
1053
|
+
*/
|
|
1054
|
+
|
|
1055
|
+
/** The canonical manifest id. */
|
|
1056
|
+
declare const CHART_PANEL_ID = "xeno.core.chart";
|
|
1057
|
+
/** The `xeno.core.chart` manifest. */
|
|
1058
|
+
declare const chartManifest: PanelManifest;
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* The plot model — `XenoResultSet` + `XenoChartEncoding` → `XenoChartData`.
|
|
1062
|
+
*
|
|
1063
|
+
* Pure, renderer-agnostic, and the whole reason the renderer is a **seam**: Chart.js consumes this,
|
|
1064
|
+
* and spectra's future Canvas2D/WebGPU engine consumes the same thing. Swapping the renderer must
|
|
1065
|
+
* never mean re-deriving the data.
|
|
1066
|
+
*
|
|
1067
|
+
* @module
|
|
1068
|
+
*/
|
|
1069
|
+
|
|
1070
|
+
/** Options for {@link buildChartData}. */
|
|
1071
|
+
interface BuildChartOptions {
|
|
1072
|
+
/** Series ceiling. Beyond this the tail is dropped rather than rendering an unreadable legend. */
|
|
1073
|
+
maxSeries?: number;
|
|
1074
|
+
/**
|
|
1075
|
+
* Point ceiling. Above it the data is flagged `tooDense` and the panel requests an AGGREGATED
|
|
1076
|
+
* query — **never a silent downsample**, which is a lie about the data.
|
|
1077
|
+
*/
|
|
1078
|
+
maxPoints?: number;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Build the plot model.
|
|
1082
|
+
*
|
|
1083
|
+
* Two paths. A result carrying `groups` whose aggregates match the encoded measures is plotted from
|
|
1084
|
+
* the GROUPS; everything else is plotted from the rows. See {@link buildFromGroups} for why the row
|
|
1085
|
+
* path is a wrong answer for an aggregated query.
|
|
1086
|
+
*
|
|
1087
|
+
* @param result - The result set. Rows are THE PAGE — the model never invents rows the page lacks.
|
|
1088
|
+
* @param encoding - How columns map to channels.
|
|
1089
|
+
* @param options - Series and point ceilings.
|
|
1090
|
+
* @returns The renderer-agnostic model.
|
|
1091
|
+
*
|
|
1092
|
+
* @example
|
|
1093
|
+
* ```ts
|
|
1094
|
+
* const data = buildChartData(result, { type: 'line', x: 'day', y: ['revenue'], series: 'region' })
|
|
1095
|
+
* ```
|
|
1096
|
+
*/
|
|
1097
|
+
declare function buildChartData(result: XenoResultSet, encoding: XenoChartEncoding, options?: BuildChartOptions): XenoChartData;
|
|
1098
|
+
/**
|
|
1099
|
+
* The row ids a mark corresponds to — what `selection` and `drillDown` are built from.
|
|
1100
|
+
*
|
|
1101
|
+
* @param result - The result the model was built from.
|
|
1102
|
+
* @param encoding - The encoding.
|
|
1103
|
+
* @param label - The category label that was clicked.
|
|
1104
|
+
* @param seriesId - The series, when the mark belongs to one.
|
|
1105
|
+
* @returns Matching row ids, in row order.
|
|
1106
|
+
*/
|
|
1107
|
+
declare function rowsForMark(result: XenoResultSet, encoding: XenoChartEncoding, label: string, seriesId?: string): string[];
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* The Chart panel view.
|
|
1111
|
+
*
|
|
1112
|
+
* Composes `@xenosystem/workbench/primitives`; the host must ensure `@xenosystem/workbench/primitives.css` is
|
|
1113
|
+
* present. The plot area delegates to the installed renderer (see `renderer.ts`) — this view owns
|
|
1114
|
+
* the chrome and the states, not the marks.
|
|
1115
|
+
*
|
|
1116
|
+
* **There is no shelf UI here.** The encoding editor is `xeno.core.fields` with chart shelves,
|
|
1117
|
+
* wired as a sibling panel; building a second shelf UI is exactly what sequencing chart after fields
|
|
1118
|
+
* was meant to avoid.
|
|
1119
|
+
*
|
|
1120
|
+
* @module
|
|
1121
|
+
*/
|
|
1122
|
+
|
|
1123
|
+
/** Props for {@link ChartPanelView}. */
|
|
1124
|
+
interface ChartPanelViewProps {
|
|
1125
|
+
controller: ChartController;
|
|
1126
|
+
options?: Partial<ChartRenderOptions>;
|
|
1127
|
+
emptyHint?: string;
|
|
1128
|
+
}
|
|
1129
|
+
/** The Chart panel view. */
|
|
1130
|
+
declare function ChartPanelView({ controller, options, emptyHint }: ChartPanelViewProps): ReactNode;
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* The `xeno.core.metrics` contract — KPI tiles over a `XenoResultSet`.
|
|
1134
|
+
*
|
|
1135
|
+
* The cheapest panel in the family: the tile primitives already landed in
|
|
1136
|
+
* `@xenosystem/workbench/primitives` (lifted from `xeno-post`'s kit), and the percentile math already
|
|
1137
|
+
* landed in `@xenosystem/data-core`. This package is the model that binds them.
|
|
1138
|
+
*
|
|
1139
|
+
* @module
|
|
1140
|
+
*/
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Which direction of change is GOOD.
|
|
1144
|
+
*
|
|
1145
|
+
* Load-bearing: **a rising error rate is not good news.** A delta arrow that is green whenever the
|
|
1146
|
+
* number went up is worse than no arrow, because it reads as reassurance.
|
|
1147
|
+
*/
|
|
1148
|
+
type DeltaDirection = 'higherIsBetter' | 'lowerIsBetter' | 'neutral';
|
|
1149
|
+
/** One KPI tile. */
|
|
1150
|
+
interface XenoMetricSpec {
|
|
1151
|
+
/** Stable id — the key in thresholds, alerts and `metricActivate`. */
|
|
1152
|
+
id: string;
|
|
1153
|
+
/** Display label. */
|
|
1154
|
+
label: string;
|
|
1155
|
+
/** Column to aggregate. Ignored by `count`. */
|
|
1156
|
+
columnId: string;
|
|
1157
|
+
/** The aggregate. Percentiles are R-7 / `PERCENTILE.INC`, as `@xenosystem/data-core` documents. */
|
|
1158
|
+
fn: XenoAggFn;
|
|
1159
|
+
/** Presentation. */
|
|
1160
|
+
format?: XenoValueFormat;
|
|
1161
|
+
/** Column supplying the sparkline series, when one is wanted. */
|
|
1162
|
+
sparklineColumnId?: string;
|
|
1163
|
+
/** Lucide icon name for the tile. */
|
|
1164
|
+
icon?: string;
|
|
1165
|
+
/** Which direction is good. Default `neutral` — no colour rather than a wrong colour. */
|
|
1166
|
+
deltaDirection?: DeltaDirection;
|
|
1167
|
+
/** Longer explanation, shown as a tooltip. */
|
|
1168
|
+
description?: string;
|
|
1169
|
+
}
|
|
1170
|
+
/** A warn/critical threshold for one metric. */
|
|
1171
|
+
interface XenoMetricThreshold {
|
|
1172
|
+
metricId: string;
|
|
1173
|
+
/** Crossing this raises `level: 'warn'`. */
|
|
1174
|
+
warn?: number;
|
|
1175
|
+
/** Crossing this raises `level: 'critical'`. */
|
|
1176
|
+
critical?: number;
|
|
1177
|
+
/**
|
|
1178
|
+
* Which side of the number is bad. Defaults from the metric's `deltaDirection`:
|
|
1179
|
+
* `lowerIsBetter` ⇒ `above`, `higherIsBetter` ⇒ `below`.
|
|
1180
|
+
*/
|
|
1181
|
+
breach?: 'above' | 'below';
|
|
1182
|
+
}
|
|
1183
|
+
/** Threshold severity, mirroring the profiler's bottleneck model. */
|
|
1184
|
+
type XenoAlertLevel = 'ok' | 'warn' | 'critical';
|
|
1185
|
+
/** What the panel emits when a threshold is crossed. */
|
|
1186
|
+
interface XenoMetricAlert {
|
|
1187
|
+
metricId: string;
|
|
1188
|
+
level: XenoAlertLevel;
|
|
1189
|
+
value: number;
|
|
1190
|
+
threshold: number;
|
|
1191
|
+
}
|
|
1192
|
+
/** A tile, computed. */
|
|
1193
|
+
interface XenoMetricValue {
|
|
1194
|
+
spec: XenoMetricSpec;
|
|
1195
|
+
/** The aggregate, or `null` when the sample supports no answer. */
|
|
1196
|
+
value: XenoValue;
|
|
1197
|
+
/** Formatted for display. */
|
|
1198
|
+
display: string;
|
|
1199
|
+
/** The comparison-period value, when a comparison result was supplied. */
|
|
1200
|
+
previous?: XenoValue;
|
|
1201
|
+
/** Signed fractional change vs the previous period (`0.12` = +12%). */
|
|
1202
|
+
delta?: number;
|
|
1203
|
+
/**
|
|
1204
|
+
* Whether the change is GOOD, BAD or neither — derived from `deltaDirection`, never from the sign.
|
|
1205
|
+
*/
|
|
1206
|
+
deltaTone: 'good' | 'bad' | 'neutral';
|
|
1207
|
+
/** Threshold state. */
|
|
1208
|
+
level: XenoAlertLevel;
|
|
1209
|
+
/** The sparkline series, oldest first. */
|
|
1210
|
+
spark?: number[];
|
|
1211
|
+
/**
|
|
1212
|
+
* `true` when the underlying result is `status: 'partial'`.
|
|
1213
|
+
*
|
|
1214
|
+
* A partial result means "these rows are real, the measures are still landing". Rendering it as
|
|
1215
|
+
* zero is a lie the user cannot detect; the tile shows the hint instead.
|
|
1216
|
+
*/
|
|
1217
|
+
partial?: boolean;
|
|
1218
|
+
}
|
|
1219
|
+
/** A named time range offered in the toolbar. */
|
|
1220
|
+
interface XenoMetricRange {
|
|
1221
|
+
/** Stable id. */
|
|
1222
|
+
id: string;
|
|
1223
|
+
/** Display label (`'7d'`). */
|
|
1224
|
+
label: string;
|
|
1225
|
+
/** How far back. */
|
|
1226
|
+
unit: 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
|
|
1227
|
+
/** How many units. Positive; the panel emits it as a negative relative filter. */
|
|
1228
|
+
n: number;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* The default ranges.
|
|
1232
|
+
*
|
|
1233
|
+
* **Config, not constants.** One product hardcoded 7/30/90 as tabs precisely because the filter
|
|
1234
|
+
* vocabulary had no `relative` primitive; now that it does, these are data.
|
|
1235
|
+
*/
|
|
1236
|
+
declare const DEFAULT_RANGES: readonly XenoMetricRange[];
|
|
1237
|
+
/** The panel's serialized state. */
|
|
1238
|
+
interface MetricsPanelState {
|
|
1239
|
+
/** The active range id. */
|
|
1240
|
+
rangeId?: string;
|
|
1241
|
+
/** Per-metric threshold overrides the user set. */
|
|
1242
|
+
thresholds?: XenoMetricThreshold[];
|
|
1243
|
+
}
|
|
1244
|
+
/** What the controller exposes to its view. */
|
|
1245
|
+
interface MetricsViewState {
|
|
1246
|
+
/** Computed tiles, in config order. */
|
|
1247
|
+
metrics: XenoMetricValue[];
|
|
1248
|
+
/** The active range. */
|
|
1249
|
+
rangeId: string | null;
|
|
1250
|
+
/** Ranges to offer. */
|
|
1251
|
+
ranges: readonly XenoMetricRange[];
|
|
1252
|
+
/** No result yet. */
|
|
1253
|
+
empty: boolean;
|
|
1254
|
+
/** The result is `partial`. */
|
|
1255
|
+
partial: boolean;
|
|
1256
|
+
/** The result carried an error. */
|
|
1257
|
+
errorMessage: string | null;
|
|
1258
|
+
}
|
|
1259
|
+
/** Resolve which direction is bad for a threshold. */
|
|
1260
|
+
declare function breachSideOf(threshold: XenoMetricThreshold, spec: XenoMetricSpec | undefined): 'above' | 'below';
|
|
1261
|
+
/**
|
|
1262
|
+
* The tone a delta should render with.
|
|
1263
|
+
*
|
|
1264
|
+
* Derived from `deltaDirection`, **never from the sign**. `neutral` metrics get no tone at all,
|
|
1265
|
+
* because a colour that means nothing still reads as a judgement.
|
|
1266
|
+
*/
|
|
1267
|
+
declare function deltaToneOf(delta: number | undefined, direction?: DeltaDirection): XenoMetricValue['deltaTone'];
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* The Metrics controller — result in, tiles out.
|
|
1271
|
+
*
|
|
1272
|
+
* Every number comes from `@xenosystem/data-core`'s `applyAggregate`, so a percentile here is the same
|
|
1273
|
+
* percentile a table's footer shows (R-7 / `PERCENTILE.INC`). Definitions differ by a whole element
|
|
1274
|
+
* on small samples, and a dashboard whose KPI disagrees with its own table is worse than one with no
|
|
1275
|
+
* KPI at all.
|
|
1276
|
+
*
|
|
1277
|
+
* @module
|
|
1278
|
+
*/
|
|
1279
|
+
|
|
1280
|
+
/** The host seam. */
|
|
1281
|
+
interface MetricsHostBridge {
|
|
1282
|
+
emit(portId: string, value: unknown): void;
|
|
1283
|
+
makeId?: () => string;
|
|
1284
|
+
}
|
|
1285
|
+
/** Construction options. */
|
|
1286
|
+
interface MetricsControllerOptions {
|
|
1287
|
+
host: MetricsHostBridge;
|
|
1288
|
+
/** The tiles to render. */
|
|
1289
|
+
metrics?: XenoMetricSpec[];
|
|
1290
|
+
/** Ranges to offer. Default 7/30/90 days. */
|
|
1291
|
+
ranges?: readonly XenoMetricRange[];
|
|
1292
|
+
/** Which time column a range filter applies to. */
|
|
1293
|
+
rangeColumnId?: string;
|
|
1294
|
+
/** IANA zone for the range filter. Default `'UTC'`. */
|
|
1295
|
+
timeZone?: string;
|
|
1296
|
+
/** Restored state. */
|
|
1297
|
+
initial?: Partial<MetricsPanelState>;
|
|
1298
|
+
}
|
|
1299
|
+
/** The Metrics panel controller. */
|
|
1300
|
+
declare class MetricsController {
|
|
1301
|
+
private readonly host;
|
|
1302
|
+
private readonly listeners;
|
|
1303
|
+
private readonly ranges;
|
|
1304
|
+
private readonly rangeColumnId?;
|
|
1305
|
+
private readonly timeZone;
|
|
1306
|
+
private specs;
|
|
1307
|
+
private result;
|
|
1308
|
+
private comparison;
|
|
1309
|
+
private thresholds;
|
|
1310
|
+
private rangeId;
|
|
1311
|
+
/** Last emitted level per metric, so an alert fires on TRANSITION and not on every render. */
|
|
1312
|
+
private readonly lastLevels;
|
|
1313
|
+
private snapshot;
|
|
1314
|
+
constructor(options: MetricsControllerOptions);
|
|
1315
|
+
subscribe: (listener: () => void) => (() => void);
|
|
1316
|
+
getState: () => MetricsViewState;
|
|
1317
|
+
private notify;
|
|
1318
|
+
/** Receive the current-period result. */
|
|
1319
|
+
setResult(result: XenoResultSet): void;
|
|
1320
|
+
/** Receive the prior-period result, which is what makes a delta possible. */
|
|
1321
|
+
setComparison(result: XenoResultSet | null): void;
|
|
1322
|
+
/** Receive thresholds. */
|
|
1323
|
+
setThresholds(thresholds: XenoMetricThreshold[]): void;
|
|
1324
|
+
/** Replace the tile set. */
|
|
1325
|
+
setMetrics(specs: XenoMetricSpec[]): void;
|
|
1326
|
+
/** Compute every tile. */
|
|
1327
|
+
compute(): XenoMetricValue[];
|
|
1328
|
+
private aggregate;
|
|
1329
|
+
private sparkFor;
|
|
1330
|
+
/** The threshold state of one metric. */
|
|
1331
|
+
levelFor(spec: XenoMetricSpec, value: XenoValue): XenoAlertLevel;
|
|
1332
|
+
/**
|
|
1333
|
+
* Emit an alert for every metric whose level CHANGED.
|
|
1334
|
+
*
|
|
1335
|
+
* On transition, not on every render: a dashboard that re-emits `critical` sixty times a second
|
|
1336
|
+
* is a dashboard whose alerts get muted.
|
|
1337
|
+
*/
|
|
1338
|
+
private emitAlerts;
|
|
1339
|
+
/**
|
|
1340
|
+
* Select a time range, emitting a `queryRequest` carrying a RELATIVE filter.
|
|
1341
|
+
*
|
|
1342
|
+
* Relative, not two absolute timestamps: a saved dashboard must still mean "the last 7 days"
|
|
1343
|
+
* tomorrow.
|
|
1344
|
+
*/
|
|
1345
|
+
setRange(rangeId: string): boolean;
|
|
1346
|
+
/** A tile was clicked — the drill-down trigger. */
|
|
1347
|
+
activate(metricId: string): boolean;
|
|
1348
|
+
/** Serialize. Range and thresholds — never a result. */
|
|
1349
|
+
serialize(): MetricsPanelState;
|
|
1350
|
+
/** Restore. Does not emit. */
|
|
1351
|
+
deserialize(state: Partial<MetricsPanelState>): void;
|
|
1352
|
+
/** Drop listeners. */
|
|
1353
|
+
dispose(): void;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* The `PanelModule` — view wired inside the package (one React copy).
|
|
1358
|
+
*
|
|
1359
|
+
* @module
|
|
1360
|
+
*/
|
|
1361
|
+
|
|
1362
|
+
/** Everything a renderer needs: the controller plus the resolved config. */
|
|
1363
|
+
interface MetricsRenderContext {
|
|
1364
|
+
controller: MetricsController;
|
|
1365
|
+
config: {
|
|
1366
|
+
layout: 'grid' | 'row' | 'list';
|
|
1367
|
+
columns: number;
|
|
1368
|
+
sparkline: boolean;
|
|
1369
|
+
showDelta: boolean;
|
|
1370
|
+
emptyHint?: string;
|
|
1371
|
+
partialHint: string;
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
/** Options for {@link createMetricsPanel}. */
|
|
1375
|
+
interface CreateMetricsPanelOptions {
|
|
1376
|
+
/** Override the renderer. Rarely needed — mounting inside the package keeps React singular. */
|
|
1377
|
+
render?: (root: HTMLElement, context: MetricsRenderContext) => () => void;
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Build the Metrics panel module.
|
|
1381
|
+
*
|
|
1382
|
+
* @param options - Optional renderer override.
|
|
1383
|
+
* @returns The module.
|
|
1384
|
+
*/
|
|
1385
|
+
declare function createMetricsPanel(options?: CreateMetricsPanelOptions): PanelModule;
|
|
1386
|
+
/** The default Metrics panel module — view already wired. Register THIS. */
|
|
1387
|
+
declare const metricsPanel: PanelModule;
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* The `xeno.core.metrics` manifest.
|
|
1391
|
+
*
|
|
1392
|
+
* The cheapest panel in the family and a LEAF — it consumes a result and emits events, so nothing
|
|
1393
|
+
* downstream waits on it. Capabilities: `storage.local` only.
|
|
1394
|
+
*
|
|
1395
|
+
* @module
|
|
1396
|
+
*/
|
|
1397
|
+
|
|
1398
|
+
/** The canonical manifest id. */
|
|
1399
|
+
declare const METRICS_PANEL_ID = "xeno.core.metrics";
|
|
1400
|
+
/** The `xeno.core.metrics` manifest. */
|
|
1401
|
+
declare const metricsManifest: PanelManifest;
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* The Metrics panel view — tiles, deltas, sparklines, threshold tone.
|
|
1405
|
+
*
|
|
1406
|
+
* Composes `@xenosystem/workbench/primitives` (`StatTile`, `Badge`, `SegmentedControl`, `Sparkline`); the
|
|
1407
|
+
* host must ensure `@xenosystem/workbench/primitives.css` is present. Nothing here is a local re-cut —
|
|
1408
|
+
* the sparkline moved into the primitives so there is exactly one.
|
|
1409
|
+
*
|
|
1410
|
+
* @module
|
|
1411
|
+
*/
|
|
1412
|
+
|
|
1413
|
+
/** Props for {@link MetricsPanelView}. */
|
|
1414
|
+
interface MetricsPanelViewProps {
|
|
1415
|
+
controller: MetricsController;
|
|
1416
|
+
layout?: 'grid' | 'row' | 'list';
|
|
1417
|
+
columns?: number;
|
|
1418
|
+
sparkline?: boolean;
|
|
1419
|
+
showDelta?: boolean;
|
|
1420
|
+
emptyHint?: string;
|
|
1421
|
+
partialHint?: string;
|
|
1422
|
+
}
|
|
1423
|
+
/** The Metrics panel view. */
|
|
1424
|
+
declare function MetricsPanelView({ controller, layout, columns, sparkline, showDelta, emptyHint, partialHint, }: MetricsPanelViewProps): ReactNode;
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* The `xeno.core.fields` contract — the query AUTHOR.
|
|
1428
|
+
*
|
|
1429
|
+
* This is the panel that retires three surfaces at once: sheets' mock pivot shelves (three dashed
|
|
1430
|
+
* "Drag fields here" rectangles whose `computePivot` is never called), notes' inline filter builder,
|
|
1431
|
+
* and architect's *nonexistent* schedule field editor.
|
|
1432
|
+
*
|
|
1433
|
+
* ## The design bet: shelves are CONFIG
|
|
1434
|
+
*
|
|
1435
|
+
* One panel serves three consumers because a "shelf" is nothing but a named, typed bucket of column
|
|
1436
|
+
* references. Pivot wants `rows|columns|values|filters`; chart wants `x|y|series|color|size`; table
|
|
1437
|
+
* wants `columns|filters|sorts`. Those differ in *labels, arity and accepted roles* — not in
|
|
1438
|
+
* behaviour. Encoding them as config is what stops this becoming three panels that drift.
|
|
1439
|
+
*
|
|
1440
|
+
* @module
|
|
1441
|
+
*/
|
|
1442
|
+
|
|
1443
|
+
/** What a shelf accepts and how many. */
|
|
1444
|
+
interface XenoShelfSpec {
|
|
1445
|
+
/** Stable id — also the key in {@link FieldsPanelState.shelves}. */
|
|
1446
|
+
id: string;
|
|
1447
|
+
/** Display label. */
|
|
1448
|
+
label: string;
|
|
1449
|
+
/**
|
|
1450
|
+
* Which roles may land here. A `measure` shelf refuses a dimension, so a user cannot build a
|
|
1451
|
+
* query the source will reject.
|
|
1452
|
+
*/
|
|
1453
|
+
accepts: XenoColumnRole[];
|
|
1454
|
+
/** Maximum entries. `1` makes it a single-slot shelf (chart's `x`); omit for unlimited. */
|
|
1455
|
+
max?: number;
|
|
1456
|
+
/** Entries carry an aggregate function (a `values` shelf). */
|
|
1457
|
+
aggregated?: boolean;
|
|
1458
|
+
/** Time-bucket granularity is offered for `time` columns dropped here. */
|
|
1459
|
+
bucketed?: boolean;
|
|
1460
|
+
/** Help text. */
|
|
1461
|
+
hint?: string;
|
|
1462
|
+
}
|
|
1463
|
+
/** The three shelf layouts v1 ships, as data. */
|
|
1464
|
+
declare const SHELF_PRESETS: Readonly<Record<'pivot' | 'chart' | 'table', XenoShelfSpec[]>>;
|
|
1465
|
+
/** One column placed on a shelf. */
|
|
1466
|
+
interface XenoShelfEntry {
|
|
1467
|
+
/** The column. */
|
|
1468
|
+
columnId: string;
|
|
1469
|
+
/** Aggregate function, on an `aggregated` shelf. */
|
|
1470
|
+
fn?: XenoAggFn;
|
|
1471
|
+
/** Roll-up function for grand totals. Defaults to `fn` — never to sum. */
|
|
1472
|
+
totalFn?: XenoAggFn;
|
|
1473
|
+
/** Time bucket, on a `bucketed` shelf with a temporal column. */
|
|
1474
|
+
granularity?: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
|
|
1475
|
+
/** Sort direction, on a `sorts` shelf. */
|
|
1476
|
+
direction?: 'asc' | 'desc';
|
|
1477
|
+
/** Display label override. */
|
|
1478
|
+
label?: string;
|
|
1479
|
+
/**
|
|
1480
|
+
* `true` when the panel placed this entry by role inference rather than the user.
|
|
1481
|
+
*
|
|
1482
|
+
* Auto-assignment is a **suggestion, not a decision**: a suggested entry renders differently and
|
|
1483
|
+
* is cleared wholesale the moment the user touches the shelf, so the panel never silently decides
|
|
1484
|
+
* what a query means.
|
|
1485
|
+
*/
|
|
1486
|
+
suggested?: boolean;
|
|
1487
|
+
}
|
|
1488
|
+
/** Live per-column statistics derived from a result — cardinality and nulls. */
|
|
1489
|
+
interface XenoFieldStat {
|
|
1490
|
+
columnId: string;
|
|
1491
|
+
/** Distinct values seen on the page. */
|
|
1492
|
+
distinct: number;
|
|
1493
|
+
/** Empty cells on the page. */
|
|
1494
|
+
nulls: number;
|
|
1495
|
+
/** Rows the statistic was computed over. */
|
|
1496
|
+
sampled: number;
|
|
1497
|
+
}
|
|
1498
|
+
/** The panel's serialized state. */
|
|
1499
|
+
interface FieldsPanelState {
|
|
1500
|
+
/** Entries per shelf id. */
|
|
1501
|
+
shelves: Record<string, XenoShelfEntry[]>;
|
|
1502
|
+
/** The filter tree. */
|
|
1503
|
+
filters?: XenoFilterGroup;
|
|
1504
|
+
/** Which preset the shelves came from. */
|
|
1505
|
+
preset: 'pivot' | 'chart' | 'table' | 'custom';
|
|
1506
|
+
}
|
|
1507
|
+
/** What the controller exposes to its view. */
|
|
1508
|
+
interface FieldsViewState {
|
|
1509
|
+
/** The catalog's columns for the active source, or `[]` before one lands. */
|
|
1510
|
+
columns: XenoColumn[];
|
|
1511
|
+
/** The shelf specs in play. */
|
|
1512
|
+
shelves: XenoShelfSpec[];
|
|
1513
|
+
/** Entries per shelf. */
|
|
1514
|
+
entries: Record<string, XenoShelfEntry[]>;
|
|
1515
|
+
/** The filter tree. */
|
|
1516
|
+
filters: XenoFilterGroup;
|
|
1517
|
+
/** Live stats, keyed by column id. */
|
|
1518
|
+
stats: Record<string, XenoFieldStat>;
|
|
1519
|
+
/** Aggregate functions the SOURCE can actually do. Empty ⇒ everything is local. */
|
|
1520
|
+
allowedAggregates: XenoAggFn[];
|
|
1521
|
+
/** Filter operators the source understands. */
|
|
1522
|
+
allowedFilterOps: string[];
|
|
1523
|
+
/**
|
|
1524
|
+
* Set when the catalog was inferred from a sample.
|
|
1525
|
+
*
|
|
1526
|
+
* `fields` must WARN that a column may be absent later — a shape derived from the first N rows is
|
|
1527
|
+
* a guess, and building a query on a guessed column is how a dashboard breaks next Tuesday.
|
|
1528
|
+
*/
|
|
1529
|
+
sampleWarning: string | null;
|
|
1530
|
+
/** A query is pending an explicit Apply (only when `autoApply` is off). */
|
|
1531
|
+
dirty: boolean;
|
|
1532
|
+
/** The column the user is hovering/focused, mirrored on `fieldFocus`. */
|
|
1533
|
+
focusedColumnId: string | null;
|
|
1534
|
+
}
|
|
1535
|
+
/** A calculated-field request. The HOST validates and compiles it — never the panel. */
|
|
1536
|
+
interface XenoCalculatedFieldRequest {
|
|
1537
|
+
requestId: string;
|
|
1538
|
+
/** Proposed column id. */
|
|
1539
|
+
columnId: string;
|
|
1540
|
+
label: string;
|
|
1541
|
+
/** The expression, verbatim. Host-parsed; the panel never evaluates it. */
|
|
1542
|
+
expression: string;
|
|
1543
|
+
/** The user's declared result type, for the host to check against. */
|
|
1544
|
+
type?: XenoColumn['type'];
|
|
1545
|
+
}
|
|
1546
|
+
/** Every aggregate the family knows, in menu order. */
|
|
1547
|
+
declare const ALL_AGGREGATES: readonly XenoAggFn[];
|
|
1548
|
+
/**
|
|
1549
|
+
* The default aggregate for a column.
|
|
1550
|
+
*
|
|
1551
|
+
* A measure sums, a dimension counts. Anything else counts, because counting is the one aggregate
|
|
1552
|
+
* that is meaningful for every type.
|
|
1553
|
+
*/
|
|
1554
|
+
declare function defaultAggregateFor(column: XenoColumn | undefined): XenoAggFn;
|
|
1555
|
+
/**
|
|
1556
|
+
* Which aggregates may be offered for a column, gated by what the SOURCE can push down.
|
|
1557
|
+
*
|
|
1558
|
+
* **Never offer a pushdown the source cannot do.** A menu that lists `p95` against a REST endpoint
|
|
1559
|
+
* produces a query the resolver must silently run locally over one page — which is a different,
|
|
1560
|
+
* wrong number.
|
|
1561
|
+
*
|
|
1562
|
+
* @param column - The column.
|
|
1563
|
+
* @param sourceAggregates - `capabilities.aggregates`, or empty when the source declares none.
|
|
1564
|
+
* @returns The offerable list.
|
|
1565
|
+
*/
|
|
1566
|
+
declare function offerableAggregates(column: XenoColumn | undefined, sourceAggregates?: readonly string[]): XenoAggFn[];
|
|
1567
|
+
/** The shelf a column belongs on by role — a SUGGESTION, never a silent decision. */
|
|
1568
|
+
declare function suggestedShelfFor(column: XenoColumn, shelves: readonly XenoShelfSpec[]): XenoShelfSpec | undefined;
|
|
1569
|
+
/** An empty filter group. An empty group PASSES — a half-authored filter must not blank the table. */
|
|
1570
|
+
declare function emptyFilterGroup(): XenoFilterGroup;
|
|
1571
|
+
/** Operators offerable for a column type, intersected with what the source understands. */
|
|
1572
|
+
declare function offerableFilterOps(column: XenoColumn | undefined, sourceOps?: readonly string[]): string[];
|
|
1573
|
+
/** Read the columns of the active source out of a catalog. */
|
|
1574
|
+
declare function columnsOf(catalog: XenoSchemaCatalog | null, sourceId?: string): XenoColumn[];
|
|
1575
|
+
/** Narrow a filter-tree member to a leaf. */
|
|
1576
|
+
declare function isLeafFilter(f: XenoFilter | XenoFilterGroup): f is XenoFilter;
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* The Fields controller — shelves in, `XenoQuery` out.
|
|
1580
|
+
*
|
|
1581
|
+
* Emitting a query is **the panel's whole reason to exist**, so the interesting decisions are all
|
|
1582
|
+
* about when *not* to emit: never on a structurally unchanged query, never mid-edit when `autoApply`
|
|
1583
|
+
* is off, and never a query the source cannot run.
|
|
1584
|
+
*
|
|
1585
|
+
* @module
|
|
1586
|
+
*/
|
|
1587
|
+
|
|
1588
|
+
/** The host seam. */
|
|
1589
|
+
interface FieldsHostBridge {
|
|
1590
|
+
emit(portId: string, value: unknown): void;
|
|
1591
|
+
makeId?: () => string;
|
|
1592
|
+
}
|
|
1593
|
+
/** Construction options. */
|
|
1594
|
+
interface FieldsControllerOptions {
|
|
1595
|
+
host: FieldsHostBridge;
|
|
1596
|
+
initial?: Partial<FieldsPanelState>;
|
|
1597
|
+
/** Which shelf layout to serve. Default `'table'`. */
|
|
1598
|
+
preset?: 'pivot' | 'chart' | 'table';
|
|
1599
|
+
/** Custom shelves, overriding the preset. */
|
|
1600
|
+
shelves?: XenoShelfSpec[];
|
|
1601
|
+
/** Emit on every edit. Default `true`; when `false` the user presses Apply. */
|
|
1602
|
+
autoApply?: boolean;
|
|
1603
|
+
/** Rows per page in the emitted query. */
|
|
1604
|
+
pageSize?: number;
|
|
1605
|
+
}
|
|
1606
|
+
/** The Fields panel controller. */
|
|
1607
|
+
declare class FieldsController {
|
|
1608
|
+
private readonly host;
|
|
1609
|
+
private readonly makeId;
|
|
1610
|
+
private readonly autoApply;
|
|
1611
|
+
private readonly pageSize;
|
|
1612
|
+
private readonly listeners;
|
|
1613
|
+
private shelves;
|
|
1614
|
+
private preset;
|
|
1615
|
+
private entries;
|
|
1616
|
+
private filters;
|
|
1617
|
+
private catalog;
|
|
1618
|
+
private sourceId;
|
|
1619
|
+
private stats;
|
|
1620
|
+
private focusedColumnId;
|
|
1621
|
+
private dirty;
|
|
1622
|
+
private lastEmitted;
|
|
1623
|
+
private snapshot;
|
|
1624
|
+
constructor(options: FieldsControllerOptions);
|
|
1625
|
+
subscribe: (listener: () => void) => (() => void);
|
|
1626
|
+
getState: () => FieldsViewState;
|
|
1627
|
+
private notify;
|
|
1628
|
+
/** Receive a schema catalog. */
|
|
1629
|
+
setCatalog(catalog: XenoSchemaCatalog, sourceId?: string): void;
|
|
1630
|
+
/**
|
|
1631
|
+
* Receive a result — the source of LIVE per-column statistics.
|
|
1632
|
+
*
|
|
1633
|
+
* Cardinality and null counts come from the page, not from the schema, which is why they update on
|
|
1634
|
+
* every result rather than every catalog.
|
|
1635
|
+
*/
|
|
1636
|
+
setResult(result: XenoResultSet): void;
|
|
1637
|
+
/** Adopt an externally authored query, so a user can edit what a table already shows. */
|
|
1638
|
+
setQuery(query: XenoQuery): void;
|
|
1639
|
+
/** The shelf spec with this id. */
|
|
1640
|
+
shelf(id: string): XenoShelfSpec | undefined;
|
|
1641
|
+
/**
|
|
1642
|
+
* Place a column on a shelf.
|
|
1643
|
+
*
|
|
1644
|
+
* Refuses when the shelf does not accept the column's role or is full — a shelf that silently
|
|
1645
|
+
* accepts anything is a shelf that builds queries the source rejects.
|
|
1646
|
+
*
|
|
1647
|
+
* @returns `true` when the column was placed.
|
|
1648
|
+
*/
|
|
1649
|
+
addToShelf(shelfId: string, columnId: string, at?: number): boolean;
|
|
1650
|
+
/** Remove a column from a shelf. */
|
|
1651
|
+
removeFromShelf(shelfId: string, columnId: string): boolean;
|
|
1652
|
+
/** Move an entry within a shelf. */
|
|
1653
|
+
reorderShelf(shelfId: string, columnId: string, toIndex: number): boolean;
|
|
1654
|
+
/**
|
|
1655
|
+
* Change an entry's aggregate.
|
|
1656
|
+
*
|
|
1657
|
+
* Refuses a function the source cannot run — offering `p95` against a source with no percentile
|
|
1658
|
+
* support produces a silently different number.
|
|
1659
|
+
*/
|
|
1660
|
+
setAggregate(shelfId: string, columnId: string, fn: XenoAggFn): boolean;
|
|
1661
|
+
/** Change an entry's time bucket. */
|
|
1662
|
+
setGranularity(shelfId: string, columnId: string, granularity: XenoShelfEntry['granularity']): void;
|
|
1663
|
+
/** Flip a sort direction. */
|
|
1664
|
+
toggleDirection(shelfId: string, columnId: string): void;
|
|
1665
|
+
/**
|
|
1666
|
+
* Fill empty shelves from column roles — a SUGGESTION the user can override.
|
|
1667
|
+
*
|
|
1668
|
+
* Every entry it creates is marked `suggested`, renders differently, and disappears the moment the
|
|
1669
|
+
* user touches that shelf. Auto-assignment that silently decides what a query means is how a
|
|
1670
|
+
* dashboard shows a number nobody chose.
|
|
1671
|
+
*
|
|
1672
|
+
* @returns How many entries were suggested.
|
|
1673
|
+
*/
|
|
1674
|
+
autoAssign(): number;
|
|
1675
|
+
private clearSuggestions;
|
|
1676
|
+
/** Replace the whole filter tree. */
|
|
1677
|
+
setFilters(filters: XenoFilterGroup): void;
|
|
1678
|
+
/**
|
|
1679
|
+
* Add a leaf filter to a group, addressed by its path from the root.
|
|
1680
|
+
*
|
|
1681
|
+
* @param path - Indices into nested `filters` arrays; `[]` is the root group.
|
|
1682
|
+
* @param filter - The condition.
|
|
1683
|
+
*/
|
|
1684
|
+
addFilter(path: readonly number[], filter: XenoFilter): boolean;
|
|
1685
|
+
/** Add a nested group (an and/or branch). */
|
|
1686
|
+
addFilterGroup(path: readonly number[], combinator?: 'and' | 'or'): boolean;
|
|
1687
|
+
/** Update a leaf in place. */
|
|
1688
|
+
updateFilter(path: readonly number[], index: number, patch: Partial<XenoFilter>): boolean;
|
|
1689
|
+
/** Remove a member (leaf or group). */
|
|
1690
|
+
removeFilter(path: readonly number[], index: number): boolean;
|
|
1691
|
+
/** Flip a group between AND and OR. */
|
|
1692
|
+
setCombinator(path: readonly number[], combinator: 'and' | 'or'): boolean;
|
|
1693
|
+
private groupAt;
|
|
1694
|
+
/**
|
|
1695
|
+
* Report the focused column, so a table can highlight the matching one.
|
|
1696
|
+
*
|
|
1697
|
+
* A direct lift of the Inspector's `fieldFocus` — the same idea, the same port shape, so a host
|
|
1698
|
+
* that already wired one gets the other for free.
|
|
1699
|
+
*/
|
|
1700
|
+
focusColumn(columnId: string | null): void;
|
|
1701
|
+
/** Ask the host to validate and compile a calculated field. The panel never evaluates one. */
|
|
1702
|
+
requestCalculatedField(label: string, expression: string, type?: XenoColumn['type']): string;
|
|
1703
|
+
/**
|
|
1704
|
+
* The query the current shelves describe.
|
|
1705
|
+
*
|
|
1706
|
+
* Pivot shelves produce a `pivot` block; everything else produces `groupBy` + `aggregates`, which
|
|
1707
|
+
* is what makes one panel serve a cross-tab and a flat table without a second code path.
|
|
1708
|
+
*/
|
|
1709
|
+
buildQuery(): XenoQuery;
|
|
1710
|
+
/** Emit the query, unless it is structurally unchanged. */
|
|
1711
|
+
apply(force?: boolean): XenoQuery | null;
|
|
1712
|
+
private afterEdit;
|
|
1713
|
+
private columnById;
|
|
1714
|
+
/** Serialize. Shelves and filters — never a catalog, never a result. */
|
|
1715
|
+
serialize(): FieldsPanelState;
|
|
1716
|
+
/** Restore. Does not emit. */
|
|
1717
|
+
deserialize(state: Partial<FieldsPanelState>): void;
|
|
1718
|
+
/** Drop listeners. */
|
|
1719
|
+
dispose(): void;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* The `PanelModule` — view wired inside the package (one React copy).
|
|
1724
|
+
*
|
|
1725
|
+
* @module
|
|
1726
|
+
*/
|
|
1727
|
+
|
|
1728
|
+
/** Everything a renderer needs: the controller plus the resolved config. */
|
|
1729
|
+
interface FieldsRenderContext {
|
|
1730
|
+
controller: FieldsController;
|
|
1731
|
+
config: {
|
|
1732
|
+
showStats: boolean;
|
|
1733
|
+
manualApply: boolean;
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
/** Options for {@link createFieldsPanel}. */
|
|
1737
|
+
interface CreateFieldsPanelOptions {
|
|
1738
|
+
/** Override the renderer. Rarely needed — mounting inside the package keeps React singular. */
|
|
1739
|
+
render?: (root: HTMLElement, context: FieldsRenderContext) => () => void;
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Build the Fields panel module.
|
|
1743
|
+
*
|
|
1744
|
+
* @param options - Optional renderer override.
|
|
1745
|
+
* @returns The module.
|
|
1746
|
+
*/
|
|
1747
|
+
declare function createFieldsPanel(options?: CreateFieldsPanelOptions): PanelModule;
|
|
1748
|
+
/** The default Fields panel module — view already wired. Register THIS. */
|
|
1749
|
+
declare const fieldsPanel: PanelModule;
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* The `xeno.core.fields` manifest.
|
|
1753
|
+
*
|
|
1754
|
+
* Capabilities: `storage.local` only. The panel authors a query; the connectors panel runs it.
|
|
1755
|
+
*
|
|
1756
|
+
* @module
|
|
1757
|
+
*/
|
|
1758
|
+
|
|
1759
|
+
/** The canonical manifest id. */
|
|
1760
|
+
declare const FIELDS_PANEL_ID = "xeno.core.fields";
|
|
1761
|
+
/** The `xeno.core.fields` manifest. */
|
|
1762
|
+
declare const fieldsManifest: PanelManifest;
|
|
1763
|
+
|
|
1764
|
+
/**
|
|
1765
|
+
* The Fields panel view — column list, shelves, filter builder.
|
|
1766
|
+
*
|
|
1767
|
+
* Composes `@xenosystem/workbench/primitives`; the host must ensure
|
|
1768
|
+
* `@xenosystem/workbench/primitives.css` is present.
|
|
1769
|
+
*
|
|
1770
|
+
* @module
|
|
1771
|
+
*/
|
|
1772
|
+
|
|
1773
|
+
/** Props for {@link FieldsPanelView}. */
|
|
1774
|
+
interface FieldsPanelViewProps {
|
|
1775
|
+
controller: FieldsController;
|
|
1776
|
+
/** Render live cardinality/null counts. */
|
|
1777
|
+
showStats?: boolean;
|
|
1778
|
+
/** Show an explicit Apply (config `autoApply: false`). */
|
|
1779
|
+
manualApply?: boolean;
|
|
1780
|
+
}
|
|
1781
|
+
/** The Fields panel view. */
|
|
1782
|
+
declare function FieldsPanelView({ controller, showStats, manualApply, }: FieldsPanelViewProps): ReactNode;
|
|
1783
|
+
|
|
1784
|
+
/**
|
|
1785
|
+
* The Connectors controller — the data family's **resolver**.
|
|
1786
|
+
*
|
|
1787
|
+
* Owns connections, the credential lifecycle, pushdown negotiation, the single-flight cache, refresh
|
|
1788
|
+
* timers, and the typed error surface. Every log line leaves through the SDK's redacting sink, so a
|
|
1789
|
+
* credential cannot reach a log even by accident.
|
|
1790
|
+
*
|
|
1791
|
+
* @module
|
|
1792
|
+
*/
|
|
1793
|
+
|
|
1794
|
+
/** The host seam. */
|
|
1795
|
+
interface ConnectorsHostBridge {
|
|
1796
|
+
emit(portId: string, value: unknown): void;
|
|
1797
|
+
makeId?: () => string;
|
|
1798
|
+
/** Structured logging. Wrapped in a redacting sink before use — never called directly. */
|
|
1799
|
+
log?: LogSink;
|
|
1800
|
+
/** Executes the pushed half of a query. Supplied by the host; the panel never opens a socket. */
|
|
1801
|
+
execute?: (input: ConnectorExecuteInput) => Promise<ConnectorExecuteOutput>;
|
|
1802
|
+
/** Wall clock, injectable for tests. */
|
|
1803
|
+
now?: () => number;
|
|
1804
|
+
/** Timer seam, injectable for tests. */
|
|
1805
|
+
setTimer?: (fn: () => void, ms: number) => unknown;
|
|
1806
|
+
clearTimer?: (handle: unknown) => void;
|
|
1807
|
+
}
|
|
1808
|
+
/** Construction options. */
|
|
1809
|
+
interface ConnectorsControllerOptions {
|
|
1810
|
+
host: ConnectorsHostBridge;
|
|
1811
|
+
initial?: Partial<ConnectorsPanelState>;
|
|
1812
|
+
/** What the substrate can do. Drives honest degradation. */
|
|
1813
|
+
facilities?: SubstrateFacilities;
|
|
1814
|
+
defaultPageSize?: number;
|
|
1815
|
+
maxRows?: number;
|
|
1816
|
+
cacheTtlMs?: number;
|
|
1817
|
+
/** Whether a panel-authored `raw` query is permitted. Default `false`. */
|
|
1818
|
+
allowRawQueries?: boolean;
|
|
1819
|
+
}
|
|
1820
|
+
/** The Connectors panel controller / resolver. */
|
|
1821
|
+
declare class ConnectorsController {
|
|
1822
|
+
private readonly host;
|
|
1823
|
+
private readonly makeId;
|
|
1824
|
+
private readonly log;
|
|
1825
|
+
private readonly now;
|
|
1826
|
+
private readonly setTimer;
|
|
1827
|
+
private readonly clearTimer;
|
|
1828
|
+
private readonly facilities;
|
|
1829
|
+
private readonly defaultPageSize;
|
|
1830
|
+
private readonly maxRows;
|
|
1831
|
+
private readonly cacheTtlMs;
|
|
1832
|
+
private readonly allowRawQueries;
|
|
1833
|
+
private readonly listeners;
|
|
1834
|
+
private connections;
|
|
1835
|
+
private statuses;
|
|
1836
|
+
private lastQueries;
|
|
1837
|
+
private selectedId;
|
|
1838
|
+
private form;
|
|
1839
|
+
/** Newest outstanding request per `(connectionId, requestorPanelId)`. */
|
|
1840
|
+
private readonly outstanding;
|
|
1841
|
+
/** In-flight executions, keyed by cache key — the single-flight map. */
|
|
1842
|
+
private readonly inFlight;
|
|
1843
|
+
private readonly cache;
|
|
1844
|
+
/** One timer per connection. */
|
|
1845
|
+
private readonly timers;
|
|
1846
|
+
/** Connections whose refresh is currently running — the re-entrancy guard. */
|
|
1847
|
+
private readonly refreshing;
|
|
1848
|
+
/** Last good result per connection, so a failed refresh can keep serving. */
|
|
1849
|
+
private readonly lastGood;
|
|
1850
|
+
/**
|
|
1851
|
+
* Introspected schema per connection — the catalog this panel PUBLISHES.
|
|
1852
|
+
*
|
|
1853
|
+
* Not persisted: a schema is host knowledge, re-pushed on load. A cached one written into `.xapp`
|
|
1854
|
+
* would outlive the table it describes.
|
|
1855
|
+
*/
|
|
1856
|
+
private readonly schemas;
|
|
1857
|
+
/** Monotonic catalog revision, so `fields` re-reads without polling. */
|
|
1858
|
+
private catalogRev;
|
|
1859
|
+
/** Connections currently being read as a SQL source — the recursion guard for `sourceRows`. */
|
|
1860
|
+
private readonly resolvingSources;
|
|
1861
|
+
private hidden;
|
|
1862
|
+
private snapshot;
|
|
1863
|
+
constructor(options: ConnectorsControllerOptions);
|
|
1864
|
+
subscribe: (listener: () => void) => (() => void);
|
|
1865
|
+
getState: () => ConnectorsViewState;
|
|
1866
|
+
private notify;
|
|
1867
|
+
private stateOf;
|
|
1868
|
+
/**
|
|
1869
|
+
* Add or replace a connection.
|
|
1870
|
+
*
|
|
1871
|
+
* Health is **always rebuilt**, never taken from the input — a persisted "healthy" is a lie, and a
|
|
1872
|
+
* caller passing one must not be able to install it.
|
|
1873
|
+
*/
|
|
1874
|
+
addConnection(connection: XenoConnection, emit?: boolean): void;
|
|
1875
|
+
/** Remove a connection, clearing its timer, cache and in-flight work. */
|
|
1876
|
+
removeConnection(id: string): boolean;
|
|
1877
|
+
/**
|
|
1878
|
+
* Receive an introspected schema from the host and **publish the catalog**.
|
|
1879
|
+
*
|
|
1880
|
+
* **Why this exists.** The `catalog` output port was declared, `introspect` documented "re-read
|
|
1881
|
+
* the schema and emit a catalog", and nothing in the package ever emitted on it — `introspect`
|
|
1882
|
+
* only emitted `sourceChanged` and deferred to "a host flow". But **a host cannot emit on a
|
|
1883
|
+
* panel's output port**, so `connectors.catalog → fields.catalog` — step one of `fields`' own
|
|
1884
|
+
* INTEGRATION.md — had no producer and `fields` was unusable in a builder.
|
|
1885
|
+
*
|
|
1886
|
+
* The division of labour that fixes it, unchanged from the rest of the family: **the host does
|
|
1887
|
+
* the IO, the panel owns the aggregation.** The host answers an `introspect` ask by pushing one
|
|
1888
|
+
* `XenoSourceSchema` in here; the panel merges it across connections, bumps `rev`, and emits the
|
|
1889
|
+
* catalog itself.
|
|
1890
|
+
*
|
|
1891
|
+
* @param schema - One source's schema, or a whole catalog to install at once.
|
|
1892
|
+
* @returns Whether a catalog was emitted.
|
|
1893
|
+
*/
|
|
1894
|
+
setSchema(schema: XenoSourceSchema | XenoSchemaCatalog): boolean;
|
|
1895
|
+
/** The catalog as it currently stands. */
|
|
1896
|
+
catalog(): XenoSchemaCatalog;
|
|
1897
|
+
/** Publish it. */
|
|
1898
|
+
private emitCatalog;
|
|
1899
|
+
/** The connection with this id. */
|
|
1900
|
+
connection(id: string): XenoConnection | undefined;
|
|
1901
|
+
/** Select a connection in the UI. */
|
|
1902
|
+
select(id: string | null): void;
|
|
1903
|
+
/** The host asks for a credential. The panel renders the descriptor; it knows no platform names. */
|
|
1904
|
+
requireCredential(spec: XenoCredentialFormSpec): void;
|
|
1905
|
+
/**
|
|
1906
|
+
* Submit a credential form.
|
|
1907
|
+
*
|
|
1908
|
+
* The values leave immediately and are **cleared from local state right here** — not in a
|
|
1909
|
+
* `.then()`, not on unmount. `submit_credential` is declared `agentVisible: false`, so an agent can
|
|
1910
|
+
* neither read this form nor replay it.
|
|
1911
|
+
*/
|
|
1912
|
+
submitCredential(values: Record<string, string | number | boolean>): boolean;
|
|
1913
|
+
/** Abandon the form. Values are dropped; nothing is emitted. */
|
|
1914
|
+
cancelCredential(): void;
|
|
1915
|
+
/**
|
|
1916
|
+
* The host's answer to a credential flow.
|
|
1917
|
+
*
|
|
1918
|
+
* **Never carries the values back.** A new `credentialRefRev` invalidates every cached result for
|
|
1919
|
+
* that connection, so a re-auth cannot serve data fetched with the old identity.
|
|
1920
|
+
*/
|
|
1921
|
+
credentialChanged(result: XenoCredentialResult): void;
|
|
1922
|
+
/** A host-pushed health change. */
|
|
1923
|
+
healthChanged(connectionId: string, status: XenoConnectionStatus): void;
|
|
1924
|
+
/**
|
|
1925
|
+
* Split a query into what the source can run and what must run locally.
|
|
1926
|
+
*
|
|
1927
|
+
* The split is echoed in `result.stats` so a user can see *why* a filter was slow — a resolver that
|
|
1928
|
+
* silently runs everything locally looks identical to one that pushes everything down, right up
|
|
1929
|
+
* until the row count matters.
|
|
1930
|
+
*
|
|
1931
|
+
* @param query - The full query.
|
|
1932
|
+
* @param connection - The target connection.
|
|
1933
|
+
* @returns The split, with op names for telemetry.
|
|
1934
|
+
*/
|
|
1935
|
+
splitQuery(query: XenoQuery, connection: XenoConnection): PushdownSplit;
|
|
1936
|
+
/** The effective row cap: the smallest of query, source ceiling and host budget. */
|
|
1937
|
+
clampLimit(query: XenoQuery, connection: XenoConnection): number;
|
|
1938
|
+
/** Cache key: connection + credential revision + the pushed query. */
|
|
1939
|
+
private cacheKey;
|
|
1940
|
+
/** Drop every cached result for a connection. */
|
|
1941
|
+
invalidateCache(connectionId: string): void;
|
|
1942
|
+
/**
|
|
1943
|
+
* Resolve a query.
|
|
1944
|
+
*
|
|
1945
|
+
* The whole of §5 lives here: per-`(connection, requestor)` correlation, the pushdown split,
|
|
1946
|
+
* single-flight, the clamped-limit echo, honest `page.total`, and keeping the last good result
|
|
1947
|
+
* when a refresh fails.
|
|
1948
|
+
*
|
|
1949
|
+
* @param request - The incoming query.
|
|
1950
|
+
* @returns The result set (also emitted on `result`).
|
|
1951
|
+
*/
|
|
1952
|
+
resolve(request: XenoConnectorQueryRequest): Promise<XenoResultSet>;
|
|
1953
|
+
/**
|
|
1954
|
+
* The number of rows the source must return for this query to be answerable.
|
|
1955
|
+
*
|
|
1956
|
+
* **Not the same as the page size**, and conflating them is a real bug: when a source cannot push
|
|
1957
|
+
* `limit` down, the page is cut *locally*, so trimming the fetch to one page's worth leaves
|
|
1958
|
+
* nothing for the local cut to find and **every page after the first comes back empty**. A source
|
|
1959
|
+
* that pages for itself is asked for exactly one page; a source that cannot is asked for
|
|
1960
|
+
* everything up to the end of the requested window, capped by the row ceiling.
|
|
1961
|
+
*
|
|
1962
|
+
* @param split - The pushdown split.
|
|
1963
|
+
* @param limit - The clamped page size.
|
|
1964
|
+
* @returns The row budget, and whether the source paged for us.
|
|
1965
|
+
*/
|
|
1966
|
+
private rowBudget;
|
|
1967
|
+
/** Run the pushed half through the host, then the local half through the pipeline. */
|
|
1968
|
+
private execute;
|
|
1969
|
+
/**
|
|
1970
|
+
* Another connection's rows, as plain objects — the in-sandbox SQL bridge's source of data.
|
|
1971
|
+
*
|
|
1972
|
+
* Reuses the ordinary resolution path, so a DuckDB query over a REST source gets that source's
|
|
1973
|
+
* cache, its credential and its error handling rather than a second, subtly different resolver.
|
|
1974
|
+
*
|
|
1975
|
+
* @param connectionId - The source to read.
|
|
1976
|
+
* @returns Its rows, or `null` when it is unknown or failed.
|
|
1977
|
+
*/
|
|
1978
|
+
sourceRows(connectionId: string): Promise<{
|
|
1979
|
+
rows: Record<string, unknown>[];
|
|
1980
|
+
name?: string;
|
|
1981
|
+
} | null>;
|
|
1982
|
+
/**
|
|
1983
|
+
* Publish a **sampled** schema derived from a result, so `catalog` has a producer for every kind.
|
|
1984
|
+
*
|
|
1985
|
+
* Without this, `catalog` only ever emits for a host that answers `introspect` — which is exactly
|
|
1986
|
+
* one kind of host. A REST or CSV source has no DDL to read, but it does have a page of rows, and
|
|
1987
|
+
* §7 already names that path `derivedFrom: 'sample'` and requires `fields` to warn that a column
|
|
1988
|
+
* may be absent later.
|
|
1989
|
+
*
|
|
1990
|
+
* A host-supplied schema always wins: this never overwrites one that was genuinely introspected.
|
|
1991
|
+
* The revision is bumped **only when the derived shape actually changes**, so a 30-second refresh
|
|
1992
|
+
* does not force `fields` to re-read on every tick.
|
|
1993
|
+
*
|
|
1994
|
+
* @param connection - The source that answered.
|
|
1995
|
+
* @param result - Its result set.
|
|
1996
|
+
* @returns Whether a new catalog was published.
|
|
1997
|
+
*/
|
|
1998
|
+
private noteSampledSchema;
|
|
1999
|
+
/** Apply the local half of the query and stamp the echoed query + split telemetry. */
|
|
2000
|
+
private finish;
|
|
2001
|
+
private echoQuery;
|
|
2002
|
+
/** Emit a result, unless the request has been superseded. */
|
|
2003
|
+
private deliver;
|
|
2004
|
+
/** Install the refresh timer for a connection, replacing any existing one. */
|
|
2005
|
+
scheduleRefresh(connection: XenoConnection): void;
|
|
2006
|
+
/** Clear a connection's timer. */
|
|
2007
|
+
clearRefresh(id: string): void;
|
|
2008
|
+
/** Suspend or resume every timer — a hidden panel must not poll. */
|
|
2009
|
+
setHidden(hidden: boolean): void;
|
|
2010
|
+
/** Re-run a connection's last query, bypassing the cache. */
|
|
2011
|
+
refresh(connectionId: string): Promise<XenoResultSet | null>;
|
|
2012
|
+
/**
|
|
2013
|
+
* Refresh every connection, reporting **per connection**.
|
|
2014
|
+
*
|
|
2015
|
+
* A partial failure must never collapse to one error: with five sources and one expired token, a
|
|
2016
|
+
* single rejected promise tells the user nothing about the four that worked.
|
|
2017
|
+
*
|
|
2018
|
+
* @returns One outcome per connection, in order.
|
|
2019
|
+
*/
|
|
2020
|
+
refreshAll(): Promise<{
|
|
2021
|
+
connectionId: string;
|
|
2022
|
+
ok: boolean;
|
|
2023
|
+
code?: string;
|
|
2024
|
+
}[]>;
|
|
2025
|
+
/**
|
|
2026
|
+
* Serialize.
|
|
2027
|
+
*
|
|
2028
|
+
* Connections with their `credentialRef` and the last query. **Never a result, never `health`,
|
|
2029
|
+
* never a secret — and NO MASKED VALUES AT ALL.**
|
|
2030
|
+
*
|
|
2031
|
+
* A mask is not a security boundary, and a mask that survives a round-trip is data loss: one
|
|
2032
|
+
* shipped connector writes `password: '***'` on save and reads it back verbatim on load, so
|
|
2033
|
+
* opening and saving a document *replaces the real credential with three asterisks*. The only way
|
|
2034
|
+
* to make that unrepresentable is to never serialize a value field in the first place.
|
|
2035
|
+
*/
|
|
2036
|
+
serialize(): ConnectorsPanelState;
|
|
2037
|
+
/** Restore. Health is rebuilt as `unknown` — never read from the document. */
|
|
2038
|
+
deserialize(state: Partial<ConnectorsPanelState>): void;
|
|
2039
|
+
/** Drop timers, listeners and in-flight state. */
|
|
2040
|
+
dispose(): void;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
/**
|
|
2044
|
+
* The `sql.*` executors — and the honest refusal that replaces a stub.
|
|
2045
|
+
*
|
|
2046
|
+
* **A sandboxed panel cannot open a TCP socket.** Not "does not yet"; cannot. QuickJS has no socket
|
|
2047
|
+
* API, a browser has no raw TCP at all, and no amount of work in this package changes either. The
|
|
2048
|
+
* capability matrix therefore marks `sql.postgres`, `sql.mysql`, `sql.sqlite` and file-mode
|
|
2049
|
+
* `sql.duckdb` as **`unsupported`** in those substrates, with one instruction attached to that
|
|
2050
|
+
* decision: *"Disabled card + reason. **Never a stub.**"*
|
|
2051
|
+
*
|
|
2052
|
+
* So this module has exactly two behaviours:
|
|
2053
|
+
*
|
|
2054
|
+
* 1. A **host resolver was supplied** — delegate to it. The host owns the driver, the connection
|
|
2055
|
+
* pool and the credential; the panel contributes the compiled statement and the paging truth.
|
|
2056
|
+
* 2. **No host resolver** — return `HOST_RESOLVER_REQUIRED` with a sentence naming what the user
|
|
2057
|
+
* can actually do about it. Not an empty grid, not a spinner, not a "coming soon".
|
|
2058
|
+
*
|
|
2059
|
+
* The second case is the whole point. A stub that returns `[]` renders as a working connection with
|
|
2060
|
+
* no data, and the user's next hour goes into debugging their WHERE clause.
|
|
2061
|
+
*
|
|
2062
|
+
* @module
|
|
2063
|
+
*/
|
|
2064
|
+
|
|
2065
|
+
/** A host-side SQL resolver: the desktop main process, an IPC bridge, a server. */
|
|
2066
|
+
type HostSqlResolver = (input: ConnectorExecuteInput) => Promise<ConnectorExecuteOutput>;
|
|
2067
|
+
/** Options for {@link executeSql}. */
|
|
2068
|
+
interface ExecuteSqlOptions {
|
|
2069
|
+
/** The host's resolver. Absent means this substrate genuinely cannot reach a database. */
|
|
2070
|
+
hostResolver?: HostSqlResolver;
|
|
2071
|
+
}
|
|
2072
|
+
/**
|
|
2073
|
+
* Run a SQL query against a server or file database.
|
|
2074
|
+
*
|
|
2075
|
+
* @param input - Connection, the pushed query, and the clamped row budget.
|
|
2076
|
+
* @param options - The host resolver, when this substrate has one.
|
|
2077
|
+
* @returns Rows, or the typed refusal.
|
|
2078
|
+
*
|
|
2079
|
+
* @example
|
|
2080
|
+
* ```ts
|
|
2081
|
+
* // In a browser with no host: a visible, explained, disabled state.
|
|
2082
|
+
* await executeSql(input, {})
|
|
2083
|
+
* // → { rows: [], error: { code: 'HOST_RESOLVER_REQUIRED', message: 'Reading PostgreSQL … ' } }
|
|
2084
|
+
* ```
|
|
2085
|
+
*/
|
|
2086
|
+
declare function executeSql(input: ConnectorExecuteInput, options: ExecuteSqlOptions): Promise<ConnectorExecuteOutput>;
|
|
2087
|
+
/**
|
|
2088
|
+
* The sentence shown on a disabled source card.
|
|
2089
|
+
*
|
|
2090
|
+
* Says the constraint, then the two real options. The spec's own remedy — *"convert to an inline
|
|
2091
|
+
* snapshot"* — is included because it is the one that works today without leaving the app.
|
|
2092
|
+
*
|
|
2093
|
+
* @param kind - The connection kind.
|
|
2094
|
+
* @returns An authored, user-facing sentence.
|
|
2095
|
+
*/
|
|
2096
|
+
declare function unsupportedMessage(kind: string): string;
|
|
2097
|
+
/**
|
|
2098
|
+
* Compile the statement a host resolver would otherwise write itself.
|
|
2099
|
+
*
|
|
2100
|
+
* Offered, never imposed: a host with its own query builder ignores this and reads
|
|
2101
|
+
* `input.query` directly. A host without one gets correct identifier quoting, literal escaping and
|
|
2102
|
+
* `NULLS LAST` ordering for free, which is three classes of bug it does not have to rediscover.
|
|
2103
|
+
*
|
|
2104
|
+
* @param input - The executor input.
|
|
2105
|
+
* @param table - The table or view to read.
|
|
2106
|
+
* @param columns - Known columns, so unknown ids are refused rather than emitted.
|
|
2107
|
+
* @returns The statement or an authored refusal.
|
|
2108
|
+
*
|
|
2109
|
+
* @example
|
|
2110
|
+
* ```ts
|
|
2111
|
+
* const compiled = compileForHost(input, 'orders')
|
|
2112
|
+
* if ('sql' in compiled) rows = await pg.query(compiled.sql)
|
|
2113
|
+
* ```
|
|
2114
|
+
*/
|
|
2115
|
+
declare function compileForHost(input: ConnectorExecuteInput, table: string, columns?: string[]): {
|
|
2116
|
+
sql: string;
|
|
2117
|
+
} | {
|
|
2118
|
+
error: string;
|
|
2119
|
+
};
|
|
2120
|
+
/**
|
|
2121
|
+
* The default entity (table/view) for a connection, when its config names one.
|
|
2122
|
+
*
|
|
2123
|
+
* @param input - The executor input.
|
|
2124
|
+
* @returns The entity name, or `null`.
|
|
2125
|
+
*/
|
|
2126
|
+
declare function entityOf(input: ConnectorExecuteInput): string | null;
|
|
2127
|
+
|
|
2128
|
+
/**
|
|
2129
|
+
* The REST executor.
|
|
2130
|
+
*
|
|
2131
|
+
* Owns the protocol so no host has to: URL assembly, the four paging styles, JSONPath extraction,
|
|
2132
|
+
* HTTP-status→typed-error mapping, and the over-fetch that makes `hasMore` honest. The only thing it
|
|
2133
|
+
* does not own is the socket — that is {@link XenoConnectorFetch}, supplied by the host.
|
|
2134
|
+
*
|
|
2135
|
+
* @module
|
|
2136
|
+
*/
|
|
2137
|
+
|
|
2138
|
+
/** Options for {@link executeRest}. */
|
|
2139
|
+
interface ExecuteRestOptions {
|
|
2140
|
+
fetch: XenoConnectorFetch;
|
|
2141
|
+
timeoutMs?: number;
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Run a REST query.
|
|
2145
|
+
*
|
|
2146
|
+
* @param input - Connection, the pushed query, and the already-clamped row limit.
|
|
2147
|
+
* @param options - The host's fetch and a deadline.
|
|
2148
|
+
* @returns Rows plus paging truth, or a typed error. **Never throws for a source-side failure.**
|
|
2149
|
+
*
|
|
2150
|
+
* @example
|
|
2151
|
+
* ```ts
|
|
2152
|
+
* const out = await executeRest(
|
|
2153
|
+
* { connection, query: {}, limit: 51 },
|
|
2154
|
+
* { fetch: browserFetch() },
|
|
2155
|
+
* )
|
|
2156
|
+
* ```
|
|
2157
|
+
*/
|
|
2158
|
+
declare function executeRest(input: ConnectorExecuteInput, options: ExecuteRestOptions): Promise<ConnectorExecuteOutput>;
|
|
2159
|
+
/**
|
|
2160
|
+
* Assemble a request URL from the base, static params and the paging style.
|
|
2161
|
+
*
|
|
2162
|
+
* @param base - The configured URL, which may already carry a query string.
|
|
2163
|
+
* @param spec - Static params plus the paging position.
|
|
2164
|
+
* @returns The absolute URL to request.
|
|
2165
|
+
*
|
|
2166
|
+
* @example
|
|
2167
|
+
* ```ts
|
|
2168
|
+
* buildRestUrl('https://api.test/v1/rows?team=1', {
|
|
2169
|
+
* paging: { style: 'offset', limitParam: 'limit', offsetParam: 'offset' },
|
|
2170
|
+
* limit: 50,
|
|
2171
|
+
* offset: 100,
|
|
2172
|
+
* })
|
|
2173
|
+
* // → 'https://api.test/v1/rows?team=1&limit=50&offset=100'
|
|
2174
|
+
* ```
|
|
2175
|
+
*/
|
|
2176
|
+
declare function buildRestUrl(base: string, spec: {
|
|
2177
|
+
params?: Record<string, string>;
|
|
2178
|
+
paging: XenoRestPaging;
|
|
2179
|
+
limit: number;
|
|
2180
|
+
offset: number;
|
|
2181
|
+
cursor?: string;
|
|
2182
|
+
}): string;
|
|
2183
|
+
/**
|
|
2184
|
+
* Work out how to reach the next page, per style.
|
|
2185
|
+
*
|
|
2186
|
+
* @param paging - The declared style.
|
|
2187
|
+
* @param body - The parsed response body.
|
|
2188
|
+
* @param response - The raw response, for header-based styles.
|
|
2189
|
+
* @param pageLength - Rows in the page just read.
|
|
2190
|
+
* @returns The next cursor or URL, or `exhausted`.
|
|
2191
|
+
*/
|
|
2192
|
+
declare function nextPageToken(paging: XenoRestPaging, body: unknown, response: {
|
|
2193
|
+
headers: Record<string, string>;
|
|
2194
|
+
url?: string;
|
|
2195
|
+
}, pageLength: number): {
|
|
2196
|
+
cursor?: string;
|
|
2197
|
+
url?: string;
|
|
2198
|
+
exhausted?: boolean;
|
|
2199
|
+
};
|
|
2200
|
+
/**
|
|
2201
|
+
* Parse an RFC 8288 `Link` header.
|
|
2202
|
+
*
|
|
2203
|
+
* @param header - The raw header value.
|
|
2204
|
+
* @returns URLs by `rel`.
|
|
2205
|
+
*
|
|
2206
|
+
* @example
|
|
2207
|
+
* ```ts
|
|
2208
|
+
* parseLinkHeader('<https://api/x?page=2>; rel="next", <https://api/x?page=9>; rel="last"')
|
|
2209
|
+
* // → { next: 'https://api/x?page=2', last: 'https://api/x?page=9' }
|
|
2210
|
+
* ```
|
|
2211
|
+
*/
|
|
2212
|
+
declare function parseLinkHeader(header: string): Record<string, string>;
|
|
2213
|
+
|
|
2214
|
+
/**
|
|
2215
|
+
* Turning failures into the typed vocabulary of §5 — **authored, never echoed**.
|
|
2216
|
+
*
|
|
2217
|
+
* Two rules from the spec are enforced here rather than remembered:
|
|
2218
|
+
*
|
|
2219
|
+
* 1. *"Never surface a 500."* A server's error body is written for the server's operator; it
|
|
2220
|
+
* routinely contains stack frames, internal hostnames and occasionally a connection string. The
|
|
2221
|
+
* status **number** is diagnostic and is kept; the body and the reason phrase are not.
|
|
2222
|
+
* 2. *"Never echo env-var names."* A misconfigured platform must say what the user can fix, not
|
|
2223
|
+
* which variable the operator forgot — one surveyed implementation renders
|
|
2224
|
+
* `PLATFORM_NOT_CONFIGURED` complete with the variable names, which tells an end user nothing and
|
|
2225
|
+
* tells an attacker something.
|
|
2226
|
+
*
|
|
2227
|
+
* @module
|
|
2228
|
+
*/
|
|
2229
|
+
|
|
2230
|
+
/** A typed executor failure. */
|
|
2231
|
+
interface ExecuteError {
|
|
2232
|
+
code: XenoResolverErrorCode;
|
|
2233
|
+
message: string;
|
|
2234
|
+
retryable?: boolean;
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
2237
|
+
* Map an HTTP status onto the typed vocabulary.
|
|
2238
|
+
*
|
|
2239
|
+
* @param status - The HTTP status code.
|
|
2240
|
+
* @param statusText - The reason phrase. **Deliberately unused in the message** — see the module doc.
|
|
2241
|
+
* @returns A typed error with an authored, user-facing sentence.
|
|
2242
|
+
*
|
|
2243
|
+
* @example
|
|
2244
|
+
* ```ts
|
|
2245
|
+
* classifyHttpStatus(401) // → { code: 'AUTH_EXPIRED', message: 'The credential … ', retryable: false }
|
|
2246
|
+
* classifyHttpStatus(503) // → { code: 'NETWORK', …, retryable: true }
|
|
2247
|
+
* ```
|
|
2248
|
+
*/
|
|
2249
|
+
declare function classifyHttpStatus(status: number, statusText?: string): ExecuteError;
|
|
2250
|
+
/**
|
|
2251
|
+
* Parse a JSON body, reporting a `PARSE` failure rather than throwing.
|
|
2252
|
+
*
|
|
2253
|
+
* The offending text is **not** included: an HTML error page or a login redirect is the usual cause,
|
|
2254
|
+
* and pasting a kilobyte of markup into a panel error is neither readable nor safe. The first
|
|
2255
|
+
* meaningful characters are enough to recognise "that's HTML, not JSON".
|
|
2256
|
+
*
|
|
2257
|
+
* @param body - The raw response text.
|
|
2258
|
+
* @returns The parsed value, or a typed error.
|
|
2259
|
+
*
|
|
2260
|
+
* @example
|
|
2261
|
+
* ```ts
|
|
2262
|
+
* parseJsonBody('<!doctype html>…')
|
|
2263
|
+
* // → { error: { code: 'PARSE', message: 'The source returned HTML, not JSON. …' } }
|
|
2264
|
+
* ```
|
|
2265
|
+
*/
|
|
2266
|
+
declare function parseJsonBody(body: string): {
|
|
2267
|
+
value: unknown;
|
|
2268
|
+
} | {
|
|
2269
|
+
error: ExecuteError;
|
|
2270
|
+
};
|
|
2271
|
+
/**
|
|
2272
|
+
* Convert anything thrown during execution into a typed error.
|
|
2273
|
+
*
|
|
2274
|
+
* A {@link ConnectorTransportError} already knows its code — that is the whole point of the
|
|
2275
|
+
* transport carrying one, and it is what keeps `CORS_BLOCKED` from collapsing into `NETWORK`
|
|
2276
|
+
* (§6 L9). Everything else is genuinely unknown and says so.
|
|
2277
|
+
*
|
|
2278
|
+
* @param error - The thrown value.
|
|
2279
|
+
* @returns A typed error.
|
|
2280
|
+
*/
|
|
2281
|
+
declare function toExecuteError(error: unknown): ExecuteError;
|
|
2282
|
+
|
|
2283
|
+
/**
|
|
2284
|
+
* The GraphQL executor.
|
|
2285
|
+
*
|
|
2286
|
+
* **This file exists because of §6 L13, a live bug in shipped code.** One surveyed connector
|
|
2287
|
+
* declares `graphql` as a supported kind and then branches only on `rest`, so every other kind falls
|
|
2288
|
+
* through to the SQL path and a plain HTTP source dies with *"Database queries require main process
|
|
2289
|
+
* IPC"* — a message about the wrong subsystem entirely. The discriminated config union makes the
|
|
2290
|
+
* missing branch a compile error; this module is that branch.
|
|
2291
|
+
*
|
|
2292
|
+
* @module
|
|
2293
|
+
*/
|
|
2294
|
+
|
|
2295
|
+
/** Options for {@link executeGraphql}. */
|
|
2296
|
+
interface ExecuteGraphqlOptions {
|
|
2297
|
+
fetch: XenoConnectorFetch;
|
|
2298
|
+
timeoutMs?: number;
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Run a GraphQL query.
|
|
2302
|
+
*
|
|
2303
|
+
* @param input - Connection, the pushed query, and the clamped row limit.
|
|
2304
|
+
* @param options - The host's fetch and a deadline.
|
|
2305
|
+
* @returns Rows plus paging truth, or a typed error.
|
|
2306
|
+
*/
|
|
2307
|
+
declare function executeGraphql(input: ConnectorExecuteInput, options: ExecuteGraphqlOptions): Promise<ConnectorExecuteOutput>;
|
|
2308
|
+
/**
|
|
2309
|
+
* Read the first error out of a GraphQL `errors` array.
|
|
2310
|
+
*
|
|
2311
|
+
* An `UNAUTHENTICATED`/`FORBIDDEN` extension code is mapped onto the auth vocabulary so the
|
|
2312
|
+
* connection flips to `expired` and offers **Reconnect**, exactly as an HTTP 401 would. Without
|
|
2313
|
+
* that mapping a GraphQL auth failure is indistinguishable from a typo in the document.
|
|
2314
|
+
*
|
|
2315
|
+
* @param errors - The `errors` member of a GraphQL envelope.
|
|
2316
|
+
* @returns A typed error, or `null` when there are none.
|
|
2317
|
+
*/
|
|
2318
|
+
declare function firstGraphqlError(errors: unknown): ExecuteError | null;
|
|
2319
|
+
|
|
2320
|
+
/**
|
|
2321
|
+
* The file executors — `file.csv` and `file.json`.
|
|
2322
|
+
*
|
|
2323
|
+
* **The panel never enumerates paths and never learns one.** `fileRef` is an opaque handle the host
|
|
2324
|
+
* minted from its own picker, exactly the discipline `xeno-shell` uses for host-folder mounts, where
|
|
2325
|
+
* raw paths deliberately never cross the bridge. The panel asks "read this handle"; the host decides
|
|
2326
|
+
* whether that handle is still valid, still consented, and still inside a granted mount.
|
|
2327
|
+
*
|
|
2328
|
+
* @module
|
|
2329
|
+
*/
|
|
2330
|
+
|
|
2331
|
+
/** Options for the file executors. */
|
|
2332
|
+
interface ExecuteFileOptions {
|
|
2333
|
+
readFile: XenoConnectorFileReader;
|
|
2334
|
+
/** Byte ceiling for one read. Default 32 MiB. */
|
|
2335
|
+
maxBytes?: number;
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* Read a delimited file into rows.
|
|
2339
|
+
*
|
|
2340
|
+
* @param input - Connection and the clamped row budget.
|
|
2341
|
+
* @param options - The host's file reader and a byte ceiling.
|
|
2342
|
+
* @returns Rows, or a typed error.
|
|
2343
|
+
*
|
|
2344
|
+
* @example
|
|
2345
|
+
* ```ts
|
|
2346
|
+
* await executeCsvFile({ connection, query: {}, limit: 500 }, { readFile })
|
|
2347
|
+
* ```
|
|
2348
|
+
*/
|
|
2349
|
+
declare function executeCsvFile(input: ConnectorExecuteInput, options: ExecuteFileOptions): Promise<ConnectorExecuteOutput>;
|
|
2350
|
+
/**
|
|
2351
|
+
* Read a JSON or NDJSON file into rows.
|
|
2352
|
+
*
|
|
2353
|
+
* @param input - Connection and the clamped row budget.
|
|
2354
|
+
* @param options - The host's file reader and a byte ceiling.
|
|
2355
|
+
* @returns Rows, or a typed error.
|
|
2356
|
+
*/
|
|
2357
|
+
declare function executeJsonFile(input: ConnectorExecuteInput, options: ExecuteFileOptions): Promise<ConnectorExecuteOutput>;
|
|
2358
|
+
|
|
2359
|
+
/**
|
|
2360
|
+
* The in-sandbox SQL bridge — **the honest bridge** of the capability matrix.
|
|
2361
|
+
*
|
|
2362
|
+
* Every other route to real SQL needs something a sandboxed panel cannot have: a TCP socket, a file
|
|
2363
|
+
* handle, a host process. This one needs nothing. DuckDB-WASM runs *inside* the sandbox over buffers
|
|
2364
|
+
* that were **already fetched** by connections that had their own permission to fetch them — so a
|
|
2365
|
+
* user gets joins, `GROUP BY`, window functions and correct `NULL` semantics over their REST and CSV
|
|
2366
|
+
* sources with **zero additional capabilities**, and it survives a web export.
|
|
2367
|
+
*
|
|
2368
|
+
* That is why the capability matrix lists this row as `requires: []` while every other `sql.*` row
|
|
2369
|
+
* requires a host resolver. The data crossed the boundary once, under an existing grant; SQL over it
|
|
2370
|
+
* afterwards is arithmetic.
|
|
2371
|
+
*
|
|
2372
|
+
* ## The engine is not a dependency
|
|
2373
|
+
*
|
|
2374
|
+
* `@duckdb/duckdb-wasm` is megabytes of wasm plus a worker. Rule 8 — no heavyweight runtime
|
|
2375
|
+
* dependency — is not negotiable for a package that twenty other panels sit beside, and the lucide
|
|
2376
|
+
* incident already proved what a static import costs every consumer who never uses the feature. So
|
|
2377
|
+
* the engine arrives through the three-method {@link XenoSqlEngine} seam, resolved from the host,
|
|
2378
|
+
* exactly as xterm does in `panel-terminal`. A reference adapter ships on the `./duckdb` subpath.
|
|
2379
|
+
*
|
|
2380
|
+
* **Absent engine ⇒ a visible `unsupported` state**, never a stub.
|
|
2381
|
+
*
|
|
2382
|
+
* @module
|
|
2383
|
+
*/
|
|
2384
|
+
|
|
2385
|
+
/** Options for {@link executeDuckDb}. */
|
|
2386
|
+
interface ExecuteDuckDbOptions {
|
|
2387
|
+
/** The engine. Absent means "no in-sandbox SQL here", which is a state, not a failure. */
|
|
2388
|
+
engine?: XenoSqlEngine;
|
|
2389
|
+
/**
|
|
2390
|
+
* Fetch the rows of another connection, by id.
|
|
2391
|
+
*
|
|
2392
|
+
* Supplied by the controller, which already knows how to resolve a connection — including its
|
|
2393
|
+
* cache, its credential and its own error handling. Re-implementing that here would give the SQL
|
|
2394
|
+
* path a second, subtly different resolver.
|
|
2395
|
+
*/
|
|
2396
|
+
resolveSource?: (connectionId: string) => Promise<{
|
|
2397
|
+
rows: Record<string, unknown>[];
|
|
2398
|
+
name?: string;
|
|
2399
|
+
} | null>;
|
|
2400
|
+
/** Whether a panel-authored `raw` statement may be executed. Mirrors the controller's config. */
|
|
2401
|
+
allowRawQueries?: boolean;
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Run SQL over already-fetched results.
|
|
2405
|
+
*
|
|
2406
|
+
* @param input - Connection, the pushed query, and the clamped row budget.
|
|
2407
|
+
* @param options - The engine seam, the source resolver, and the raw-query gate.
|
|
2408
|
+
* @returns Rows, or a typed error.
|
|
2409
|
+
*
|
|
2410
|
+
* @example
|
|
2411
|
+
* ```ts
|
|
2412
|
+
* // A duckdb source joining two REST connections:
|
|
2413
|
+
* // config = { kind: 'sql.duckdb', mode: 'wasm', sourceConnectionIds: ['orders', 'customers'] }
|
|
2414
|
+
* await executeDuckDb(input, { engine, resolveSource })
|
|
2415
|
+
* ```
|
|
2416
|
+
*/
|
|
2417
|
+
declare function executeDuckDb(input: ConnectorExecuteInput, options: ExecuteDuckDbOptions): Promise<ConnectorExecuteOutput>;
|
|
2418
|
+
/**
|
|
2419
|
+
* Turn a connection name or id into a safe, unique SQL table name.
|
|
2420
|
+
*
|
|
2421
|
+
* Registered table names are not merely quoted — in most engines they also name a registered buffer,
|
|
2422
|
+
* where quoting protects nothing. So the name is *sanitized to an identifier*, not escaped: anything
|
|
2423
|
+
* outside `[A-Za-z0-9_]` becomes `_`, a leading digit gains a prefix, and collisions get a numeric
|
|
2424
|
+
* suffix so two sources called "Data" stay distinct.
|
|
2425
|
+
*
|
|
2426
|
+
* @param raw - The connection's display name or id.
|
|
2427
|
+
* @param taken - Names already assigned, as a map of sourceId → name.
|
|
2428
|
+
* @returns A safe, unused table name.
|
|
2429
|
+
*
|
|
2430
|
+
* @example
|
|
2431
|
+
* ```ts
|
|
2432
|
+
* uniqueTableName('Sales (EU)', new Map()) // → 'Sales__EU_'
|
|
2433
|
+
* uniqueTableName('2024', new Map()) // → 't_2024'
|
|
2434
|
+
* ```
|
|
2435
|
+
*/
|
|
2436
|
+
declare function uniqueTableName(raw: string, taken: Map<string, string>): string;
|
|
2437
|
+
|
|
2438
|
+
/**
|
|
2439
|
+
* A dependency-free RFC 4180 CSV reader.
|
|
2440
|
+
*
|
|
2441
|
+
* Rule 8 forbids a runtime dependency, so PapaParse is not an option — but "split on commas" is not
|
|
2442
|
+
* a CSV parser either, and the difference shows up on the first export anyone actually has: a
|
|
2443
|
+
* quoted field containing a comma, a newline inside a quoted address, a doubled `""` escape, or a
|
|
2444
|
+
* UTF-8 BOM that turns the first column's name into `id` and makes every lookup by that name
|
|
2445
|
+
* silently miss.
|
|
2446
|
+
*
|
|
2447
|
+
* This is a proper character-scanning parser. It is ~90 lines because CSV genuinely is that, and
|
|
2448
|
+
* every one of the cases above has a test.
|
|
2449
|
+
*
|
|
2450
|
+
* @module
|
|
2451
|
+
*/
|
|
2452
|
+
/** Options for {@link parseCsv}. */
|
|
2453
|
+
interface ParseCsvOptions {
|
|
2454
|
+
/** Field separator. Omit to sniff — see {@link sniffDelimiter}. */
|
|
2455
|
+
delimiter?: string;
|
|
2456
|
+
/** Quote character. Default `"`. */
|
|
2457
|
+
quote?: string;
|
|
2458
|
+
/** First (non-skipped) row is a header. Default `true`. */
|
|
2459
|
+
hasHeader?: boolean;
|
|
2460
|
+
/** Rows to drop before the header — banner lines above the table are common in exports. */
|
|
2461
|
+
skipRows?: number;
|
|
2462
|
+
}
|
|
2463
|
+
/** The parsed table. */
|
|
2464
|
+
interface ParsedCsv {
|
|
2465
|
+
/** Column names, either from the header row or synthesized as `column_1`, `column_2`, … */
|
|
2466
|
+
columns: string[];
|
|
2467
|
+
/** Records keyed by column name, values still as text. */
|
|
2468
|
+
rows: Record<string, string>[];
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* Guess the field separator from the first meaningful line.
|
|
2472
|
+
*
|
|
2473
|
+
* Counts candidates **outside quotes only**, so a comma inside `"Smith, John"` does not vote for
|
|
2474
|
+
* comma in a semicolon-separated European export — which is the case that makes naive sniffers pick
|
|
2475
|
+
* the wrong character and produce a one-column table.
|
|
2476
|
+
*
|
|
2477
|
+
* @param text - The file's text.
|
|
2478
|
+
* @returns The most likely delimiter. Falls back to `,`.
|
|
2479
|
+
*
|
|
2480
|
+
* @example
|
|
2481
|
+
* ```ts
|
|
2482
|
+
* sniffDelimiter('a;b;c\n"x, y";2;3') // → ';'
|
|
2483
|
+
* ```
|
|
2484
|
+
*/
|
|
2485
|
+
declare function sniffDelimiter(text: string): string;
|
|
2486
|
+
/**
|
|
2487
|
+
* Parse CSV text into records.
|
|
2488
|
+
*
|
|
2489
|
+
* @param text - The file contents.
|
|
2490
|
+
* @param options - Delimiter, quote, header and skip settings.
|
|
2491
|
+
* @returns Columns and records.
|
|
2492
|
+
*
|
|
2493
|
+
* @example
|
|
2494
|
+
* ```ts
|
|
2495
|
+
* parseCsv('id,name\n1,"Smith, John"\n')
|
|
2496
|
+
* // → { columns: ['id','name'], rows: [{ id: '1', name: 'Smith, John' }] }
|
|
2497
|
+
* ```
|
|
2498
|
+
*/
|
|
2499
|
+
declare function parseCsv(text: string, options?: ParseCsvOptions): ParsedCsv;
|
|
2500
|
+
/**
|
|
2501
|
+
* Convert text cells into JSON-ish values.
|
|
2502
|
+
*
|
|
2503
|
+
* CSV has no types, so a reader must infer them — but **only where inference is safe**. A value is
|
|
2504
|
+
* numeric only if it round-trips exactly through `String(Number(v))`, which is what keeps a zip code
|
|
2505
|
+
* `01234`, a phone number `+15551234`, and a 20-digit account id from being silently mangled into
|
|
2506
|
+
* `1234`, `15551234`, and a float that lost its last four digits.
|
|
2507
|
+
*
|
|
2508
|
+
* @param rows - Text records from {@link parseCsv}.
|
|
2509
|
+
* @returns Records with numbers, booleans and nulls where they are unambiguous.
|
|
2510
|
+
*
|
|
2511
|
+
* @example
|
|
2512
|
+
* ```ts
|
|
2513
|
+
* coerceCsvValues([{ n: '42', zip: '01234', ok: 'true', gap: '' }])
|
|
2514
|
+
* // → [{ n: 42, zip: '01234', ok: true, gap: null }]
|
|
2515
|
+
* ```
|
|
2516
|
+
*/
|
|
2517
|
+
declare function coerceCsvValues(rows: Record<string, string>[]): Record<string, unknown>[];
|
|
2518
|
+
|
|
2519
|
+
/**
|
|
2520
|
+
* JSONPath extraction — finding the row array inside a response envelope.
|
|
2521
|
+
*
|
|
2522
|
+
* Almost no API returns a bare array. It returns `{data: {items: [...], next: "..."}}`, and the
|
|
2523
|
+
* whole difference between a working REST source and a one-row table containing the word `[object
|
|
2524
|
+
* Object]` is knowing where to look. `rootPath` is that knowledge.
|
|
2525
|
+
*
|
|
2526
|
+
* ## A deliberate subset, and why
|
|
2527
|
+
*
|
|
2528
|
+
* This implements the *dotted-path* subset of JSONPath — `$.data.items`, `data.items`,
|
|
2529
|
+
* `results[0].rows`, `a["odd key"].b` — plus the `[*]` wildcard, and nothing else. No filter
|
|
2530
|
+
* expressions, no recursive descent, no script evaluation.
|
|
2531
|
+
*
|
|
2532
|
+
* That is not laziness. A full JSONPath engine is a third-party runtime dependency (rule 8 forbids
|
|
2533
|
+
* it) *and* the expression-evaluating half of the grammar is an injection surface pointed at the
|
|
2534
|
+
* response of a network call. The subset covers every shape the survey found in real connector
|
|
2535
|
+
* configs, and the parts left out are the parts that could bite.
|
|
2536
|
+
*
|
|
2537
|
+
* @module
|
|
2538
|
+
*/
|
|
2539
|
+
/** One step in a parsed path. */
|
|
2540
|
+
type Step = {
|
|
2541
|
+
kind: 'key';
|
|
2542
|
+
key: string;
|
|
2543
|
+
} | {
|
|
2544
|
+
kind: 'index';
|
|
2545
|
+
index: number;
|
|
2546
|
+
} | {
|
|
2547
|
+
kind: 'wildcard';
|
|
2548
|
+
};
|
|
2549
|
+
/**
|
|
2550
|
+
* Parse a dotted/bracketed path into steps.
|
|
2551
|
+
*
|
|
2552
|
+
* @param path - e.g. `$.data.items[0]`, `data["odd key"]`, `rows[*].cells`.
|
|
2553
|
+
* @returns The steps, or `null` if the expression uses syntax this subset does not implement.
|
|
2554
|
+
*
|
|
2555
|
+
* @example
|
|
2556
|
+
* ```ts
|
|
2557
|
+
* parseJsonPath('$.data.items[0]')
|
|
2558
|
+
* // → [{kind:'key',key:'data'},{kind:'key',key:'items'},{kind:'index',index:0}]
|
|
2559
|
+
* ```
|
|
2560
|
+
*/
|
|
2561
|
+
declare function parseJsonPath(path: string): Step[] | null;
|
|
2562
|
+
/**
|
|
2563
|
+
* Read a value out of parsed JSON at `path`.
|
|
2564
|
+
*
|
|
2565
|
+
* @param input - The parsed response body.
|
|
2566
|
+
* @param path - A dotted/bracketed path. Empty or `'$'` returns `input` unchanged.
|
|
2567
|
+
* @returns The value at the path, or `undefined` if any step is missing.
|
|
2568
|
+
*
|
|
2569
|
+
* @example
|
|
2570
|
+
* ```ts
|
|
2571
|
+
* extractJsonPath({ data: { items: [1, 2] } }, 'data.items') // → [1, 2]
|
|
2572
|
+
* extractJsonPath({ pages: [{ rows: [1] }, { rows: [2] }] }, 'pages[*].rows') // → [[1],[2]]
|
|
2573
|
+
* ```
|
|
2574
|
+
*/
|
|
2575
|
+
declare function extractJsonPath(input: unknown, path: string | undefined): unknown;
|
|
2576
|
+
/**
|
|
2577
|
+
* Extract the row array from a response, with a documented fallback.
|
|
2578
|
+
*
|
|
2579
|
+
* When no `rootPath` is configured, an envelope is still the common case, so this looks for the
|
|
2580
|
+
* conventional wrappers (`data`, `items`, `results`, `records`, `rows`, `value`) before giving up.
|
|
2581
|
+
* **The fallback only fires when the top level is not already an array** — a source that returned
|
|
2582
|
+
* exactly what we wanted is never second-guessed.
|
|
2583
|
+
*
|
|
2584
|
+
* @param body - The parsed response body.
|
|
2585
|
+
* @param rootPath - The configured path, if any.
|
|
2586
|
+
* @returns The rows, and how they were found.
|
|
2587
|
+
*
|
|
2588
|
+
* @example
|
|
2589
|
+
* ```ts
|
|
2590
|
+
* extractRows({ items: [{ id: 1 }] }, undefined) // → { rows: [{id:1}], via: 'convention' }
|
|
2591
|
+
* ```
|
|
2592
|
+
*/
|
|
2593
|
+
declare function extractRows(body: unknown, rootPath: string | undefined): {
|
|
2594
|
+
rows: unknown;
|
|
2595
|
+
via: 'path' | 'convention' | 'root';
|
|
2596
|
+
missing?: boolean;
|
|
2597
|
+
};
|
|
2598
|
+
|
|
2599
|
+
/**
|
|
2600
|
+
* Compiling a `XenoQuery` into SQL.
|
|
2601
|
+
*
|
|
2602
|
+
* Used by the in-sandbox DuckDB bridge, and offered to any host resolver that would otherwise write
|
|
2603
|
+
* this itself. It is the highest-risk file in the package, so the rules are absolute:
|
|
2604
|
+
*
|
|
2605
|
+
* - **Every identifier is quoted and escaped.** A column called `"; DROP TABLE x; --` becomes the
|
|
2606
|
+
* literal identifier `"""; DROP TABLE x; --"`, which is a column that does not exist — an error,
|
|
2607
|
+
* never an execution.
|
|
2608
|
+
* - **Every literal is escaped or refused.** Strings double their quotes; numbers must be finite;
|
|
2609
|
+
* anything else becomes a parameterless `NULL`. There is no path by which a value reaches the
|
|
2610
|
+
* statement uninspected.
|
|
2611
|
+
* - **`raw` is never assembled here.** A panel-authored raw string is the family's injection
|
|
2612
|
+
* surface and is gated on `allowRawQueries` in the controller, upstream of this file.
|
|
2613
|
+
*
|
|
2614
|
+
* The output targets DuckDB, whose dialect is PostgreSQL-compatible for everything used here.
|
|
2615
|
+
*
|
|
2616
|
+
* @module
|
|
2617
|
+
*/
|
|
2618
|
+
|
|
2619
|
+
/**
|
|
2620
|
+
* Quote a SQL identifier.
|
|
2621
|
+
*
|
|
2622
|
+
* @param name - A table or column name from user data.
|
|
2623
|
+
* @returns The safely quoted identifier.
|
|
2624
|
+
*
|
|
2625
|
+
* @example
|
|
2626
|
+
* ```ts
|
|
2627
|
+
* quoteIdent('order') // → '"order"'
|
|
2628
|
+
* quoteIdent('a"; DROP b; --') // → '"a""; DROP b; --"' (one identifier, not a statement)
|
|
2629
|
+
* ```
|
|
2630
|
+
*/
|
|
2631
|
+
declare function quoteIdent(name: string): string;
|
|
2632
|
+
/**
|
|
2633
|
+
* Render a value as a SQL literal.
|
|
2634
|
+
*
|
|
2635
|
+
* @param value - Any value from a filter or parameter.
|
|
2636
|
+
* @returns A literal safe to concatenate.
|
|
2637
|
+
*
|
|
2638
|
+
* @example
|
|
2639
|
+
* ```ts
|
|
2640
|
+
* quoteLiteral("O'Brien") // → "'O''Brien'"
|
|
2641
|
+
* quoteLiteral(NaN) // → 'NULL' (a non-finite number is not a number)
|
|
2642
|
+
* ```
|
|
2643
|
+
*/
|
|
2644
|
+
declare function quoteLiteral(value: XenoValue | undefined): string;
|
|
2645
|
+
/** Is this a valid, safe table name to register with the engine? */
|
|
2646
|
+
declare function isSafeTableName(name: string): boolean;
|
|
2647
|
+
/** Options for {@link buildSelect}. */
|
|
2648
|
+
interface BuildSelectOptions {
|
|
2649
|
+
/** Table to read from. Must satisfy {@link isSafeTableName}. */
|
|
2650
|
+
table: string;
|
|
2651
|
+
/** The query to compile. */
|
|
2652
|
+
query: XenoQuery;
|
|
2653
|
+
/** Row ceiling. */
|
|
2654
|
+
limit: number;
|
|
2655
|
+
/** Row offset. */
|
|
2656
|
+
offset?: number;
|
|
2657
|
+
/** Columns known to exist. When supplied, unknown column ids are refused rather than emitted. */
|
|
2658
|
+
columns?: string[];
|
|
2659
|
+
}
|
|
2660
|
+
/** A compiled statement, or the reason it could not be compiled. */
|
|
2661
|
+
type BuildSelectResult = {
|
|
2662
|
+
sql: string;
|
|
2663
|
+
} | {
|
|
2664
|
+
error: string;
|
|
2665
|
+
};
|
|
2666
|
+
/**
|
|
2667
|
+
* Compile a query into a `SELECT`.
|
|
2668
|
+
*
|
|
2669
|
+
* @param options - Table, query, paging and the known column list.
|
|
2670
|
+
* @returns The statement, or an authored refusal.
|
|
2671
|
+
*
|
|
2672
|
+
* @example
|
|
2673
|
+
* ```ts
|
|
2674
|
+
* buildSelect({
|
|
2675
|
+
* table: 'orders',
|
|
2676
|
+
* query: { aggregates: [{ id: 'total', columnId: 'amount', fn: 'sum' }], groupBy: [{ columnId: 'region' }] },
|
|
2677
|
+
* limit: 100,
|
|
2678
|
+
* })
|
|
2679
|
+
* // → { sql: 'SELECT "region", sum("amount") AS "total" FROM "orders" GROUP BY "region" LIMIT 100' }
|
|
2680
|
+
* ```
|
|
2681
|
+
*/
|
|
2682
|
+
declare function buildSelect(options: BuildSelectOptions): BuildSelectResult;
|
|
2683
|
+
/** Render a filter group into a WHERE fragment. */
|
|
2684
|
+
declare function renderFilterGroup(group: XenoFilterGroup, unknown?: (id: string) => boolean): {
|
|
2685
|
+
sql: string;
|
|
2686
|
+
} | {
|
|
2687
|
+
error: string;
|
|
2688
|
+
};
|
|
2689
|
+
/**
|
|
2690
|
+
* Escape `%`, `_` and `\` so user text is matched literally inside a LIKE pattern.
|
|
2691
|
+
*
|
|
2692
|
+
* @param text - Raw user input.
|
|
2693
|
+
* @returns The escaped text, for use with `ESCAPE '\'`.
|
|
2694
|
+
*
|
|
2695
|
+
* @example
|
|
2696
|
+
* ```ts
|
|
2697
|
+
* escapeLikePattern('50%') // → '50\\%' — searches for the character %, not "anything"
|
|
2698
|
+
* ```
|
|
2699
|
+
*/
|
|
2700
|
+
declare function escapeLikePattern(text: string): string;
|
|
2701
|
+
|
|
2702
|
+
/**
|
|
2703
|
+
* The executor dispatcher — one function per connection kind, chosen by the discriminator.
|
|
2704
|
+
*
|
|
2705
|
+
* The `switch` below is exhaustive over `XenoConnectionConfig['kind']`, which is the entire reason
|
|
2706
|
+
* the config is a discriminated union rather than a flat bag. §6 L13 is a live bug produced by the
|
|
2707
|
+
* alternative: one surveyed connector branches on `rest` and lets *every other kind* fall through to
|
|
2708
|
+
* the SQL path, so a declared-but-unhandled `graphql` source dies with *"Database queries require
|
|
2709
|
+
* main process IPC"* — a message about a subsystem it never touched. Here a missing branch does not
|
|
2710
|
+
* compile.
|
|
2711
|
+
*
|
|
2712
|
+
* @module
|
|
2713
|
+
*/
|
|
2714
|
+
|
|
2715
|
+
/** Options for {@link createConnectorExecutor}. */
|
|
2716
|
+
interface CreateConnectorExecutorOptions {
|
|
2717
|
+
/** The host's transport seams. Every field optional; absence is a state, not a bug. */
|
|
2718
|
+
transport?: ConnectorTransport;
|
|
2719
|
+
/**
|
|
2720
|
+
* A host-side resolver for `sql.*` and the first-party `xeno.*` kinds.
|
|
2721
|
+
*
|
|
2722
|
+
* Also consulted as a **fallback** for any kind whose transport is missing, so a desktop host can
|
|
2723
|
+
* supply one resolver and get every kind rather than wiring three seams.
|
|
2724
|
+
*/
|
|
2725
|
+
hostResolver?: HostSqlResolver;
|
|
2726
|
+
/** Whether a panel-authored `raw` statement may execute. Default `false`. */
|
|
2727
|
+
allowRawQueries?: boolean;
|
|
2728
|
+
/** Fetch another connection's rows, for the in-sandbox SQL bridge. Supplied by the controller. */
|
|
2729
|
+
resolveSource?: (connectionId: string) => Promise<{
|
|
2730
|
+
rows: Record<string, unknown>[];
|
|
2731
|
+
name?: string;
|
|
2732
|
+
} | null>;
|
|
2733
|
+
}
|
|
2734
|
+
/**
|
|
2735
|
+
* Build the executor the controller calls for every non-`inline` source.
|
|
2736
|
+
*
|
|
2737
|
+
* `inline` never reaches here: it is resolved in the controller with no round-trip at all, which is
|
|
2738
|
+
* what makes the first `table` drag in the builder a live grid rather than a dead one.
|
|
2739
|
+
*
|
|
2740
|
+
* @param options - Transports, an optional host resolver, and the raw-query gate.
|
|
2741
|
+
* @returns An executor.
|
|
2742
|
+
*
|
|
2743
|
+
* @example
|
|
2744
|
+
* ```ts
|
|
2745
|
+
* const execute = createConnectorExecutor({
|
|
2746
|
+
* transport: { fetch: browserFetch(), readFile: hostReadFile },
|
|
2747
|
+
* hostResolver: ipcSqlResolver,
|
|
2748
|
+
* })
|
|
2749
|
+
* ```
|
|
2750
|
+
*/
|
|
2751
|
+
declare function createConnectorExecutor(options?: CreateConnectorExecutorOptions): (input: ConnectorExecuteInput) => Promise<ConnectorExecuteOutput>;
|
|
2752
|
+
|
|
2753
|
+
/**
|
|
2754
|
+
* The `PanelModule` — what a host registers and mounts.
|
|
2755
|
+
*
|
|
2756
|
+
* Ships a wired default (`connectorsPanel`) with the view mounted INSIDE the package, so `react` and
|
|
2757
|
+
* `react-dom` resolve to one copy. A host that calls `createRoot` against a view imported from a
|
|
2758
|
+
* `file:`-linked package gets two React copies and every hook throws *"Invalid hook call"*.
|
|
2759
|
+
*
|
|
2760
|
+
* @module
|
|
2761
|
+
*/
|
|
2762
|
+
|
|
2763
|
+
/** Everything a renderer needs: the controller plus the resolved config. */
|
|
2764
|
+
interface ConnectorsRenderContext {
|
|
2765
|
+
controller: ConnectorsController;
|
|
2766
|
+
config: {
|
|
2767
|
+
emptyHint?: string;
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2770
|
+
/** Options for {@link createConnectorsPanel}. */
|
|
2771
|
+
interface CreateConnectorsPanelOptions {
|
|
2772
|
+
/**
|
|
2773
|
+
* A complete host-side executor, replacing the built-in one entirely.
|
|
2774
|
+
*
|
|
2775
|
+
* Use this when the host has its own resolver for **every** kind. To keep the built-in protocol
|
|
2776
|
+
* handling and only add what the substrate allows, pass {@link transport} instead — that is the
|
|
2777
|
+
* ordinary case.
|
|
2778
|
+
*/
|
|
2779
|
+
execute?: ConnectorsHostBridge['execute'];
|
|
2780
|
+
/**
|
|
2781
|
+
* The host's transport seams: `fetch`, `readFile`, and an in-sandbox SQL engine.
|
|
2782
|
+
*
|
|
2783
|
+
* The panel owns the protocol — paging, extraction, parsing, error classification — and the host
|
|
2784
|
+
* owns the act. Every field is optional and **absence is meaningful**: no `fetch` means this
|
|
2785
|
+
* substrate has no network, which renders as a visible `unsupported` card rather than a source
|
|
2786
|
+
* that fails later.
|
|
2787
|
+
*/
|
|
2788
|
+
transport?: ConnectorTransport;
|
|
2789
|
+
/**
|
|
2790
|
+
* A host-side resolver for `sql.*` and the first-party `xeno.*` kinds, which no sandboxed panel
|
|
2791
|
+
* can serve. Also the fallback for any kind whose transport is absent.
|
|
2792
|
+
*/
|
|
2793
|
+
hostResolver?: HostSqlResolver;
|
|
2794
|
+
/**
|
|
2795
|
+
* What the substrate can do.
|
|
2796
|
+
*
|
|
2797
|
+
* **Derived from the transports when omitted**, which is the honest default: the controller's own
|
|
2798
|
+
* fallback assumes everything is available, and a source that reports healthy and then fails at
|
|
2799
|
+
* query time is precisely the "stub that fails later" the capability matrix forbids.
|
|
2800
|
+
*/
|
|
2801
|
+
facilities?: SubstrateFacilities;
|
|
2802
|
+
/** Override the renderer. Rarely needed — see the module doc. */
|
|
2803
|
+
render?: (root: HTMLElement, context: ConnectorsRenderContext) => () => void;
|
|
2804
|
+
}
|
|
2805
|
+
/**
|
|
2806
|
+
* Build the Connectors panel module.
|
|
2807
|
+
*
|
|
2808
|
+
* @param options - Executor, substrate facilities, optional renderer.
|
|
2809
|
+
* @returns The module.
|
|
2810
|
+
*/
|
|
2811
|
+
declare function createConnectorsPanel(options?: CreateConnectorsPanelOptions): PanelModule;
|
|
2812
|
+
/** The default Connectors panel module — view already wired. Register THIS. */
|
|
2813
|
+
declare const connectorsPanel: PanelModule;
|
|
2814
|
+
|
|
2815
|
+
/**
|
|
2816
|
+
* The `xeno.core.connectors` manifest.
|
|
2817
|
+
*
|
|
2818
|
+
* **The only panel in the data family holding a capability.** `table`, `chart`, `metrics`,
|
|
2819
|
+
* `calendar` and `fields` declare `storage.local` and nothing else *because this one declares
|
|
2820
|
+
* `net.fetch` and `fs.read`* — the seam concentrates every dangerous power in one auditable place.
|
|
2821
|
+
*
|
|
2822
|
+
* @module
|
|
2823
|
+
*/
|
|
2824
|
+
|
|
2825
|
+
/** The canonical manifest id. */
|
|
2826
|
+
declare const CONNECTORS_PANEL_ID = "xeno.core.connectors";
|
|
2827
|
+
/**
|
|
2828
|
+
* Commands an agent must never be able to invoke.
|
|
2829
|
+
*
|
|
2830
|
+
* `submit_credential` is the whole list: an agent that can call it can read the form it answers and
|
|
2831
|
+
* replay a credential. The SDK has no `agentVisible` field yet, so the panel enforces it — the
|
|
2832
|
+
* command is absent from the manifest entirely and reachable only through the view.
|
|
2833
|
+
*/
|
|
2834
|
+
declare const AGENT_HIDDEN_COMMANDS: readonly string[];
|
|
2835
|
+
/** The `xeno.core.connectors` manifest. */
|
|
2836
|
+
declare const connectorsManifest: PanelManifest;
|
|
2837
|
+
|
|
2838
|
+
/**
|
|
2839
|
+
* The credential form — the panel's half, and only the panel's half.
|
|
2840
|
+
*
|
|
2841
|
+
* The lock from §3 holds with one precision that is worth stating exactly, because it is the
|
|
2842
|
+
* difference between a rule people follow and a rule people quietly break: **the panel never
|
|
2843
|
+
* receives a *stored* secret, and holds an *entered* one only in transient component state that is
|
|
2844
|
+
* cleared at the moment of submission.** A user has to be able to type a password; they must never
|
|
2845
|
+
* be able to read one back.
|
|
2846
|
+
*
|
|
2847
|
+
* A counter-example is on the record. `xeno-hub`'s environment-secrets section reveals stored
|
|
2848
|
+
* secrets behind an eye-toggle. If this panel ever ships in Hub, **that affordance does not come
|
|
2849
|
+
* with it** — there is no code path here that can render a stored value, because no stored value
|
|
2850
|
+
* ever arrives.
|
|
2851
|
+
*
|
|
2852
|
+
* @module
|
|
2853
|
+
*/
|
|
2854
|
+
|
|
2855
|
+
/** Values entered into a form, before submission. */
|
|
2856
|
+
type CredentialDraft = Record<string, string | number | boolean>;
|
|
2857
|
+
/**
|
|
2858
|
+
* Should this field be shown, given the current draft?
|
|
2859
|
+
*
|
|
2860
|
+
* `showWhen` is what lets one descriptor serve "API key *or* username+password" without the panel
|
|
2861
|
+
* knowing which platform it is rendering — the property that keeps a vendor registry out of this
|
|
2862
|
+
* package entirely.
|
|
2863
|
+
*
|
|
2864
|
+
* @param field - The field to test.
|
|
2865
|
+
* @param draft - Values entered so far.
|
|
2866
|
+
* @returns Whether to render it.
|
|
2867
|
+
*
|
|
2868
|
+
* @example
|
|
2869
|
+
* ```ts
|
|
2870
|
+
* isFieldVisible(
|
|
2871
|
+
* { key: 'pw', label: 'Password', type: 'password', showWhen: { key: 'mode', equals: ['basic'] } },
|
|
2872
|
+
* { mode: 'basic' },
|
|
2873
|
+
* ) // → true
|
|
2874
|
+
* ```
|
|
2875
|
+
*/
|
|
2876
|
+
declare function isFieldVisible(field: XenoCredentialField, draft: CredentialDraft): boolean;
|
|
2877
|
+
/**
|
|
2878
|
+
* The fields a form is currently showing.
|
|
2879
|
+
*
|
|
2880
|
+
* @param spec - The host's form descriptor.
|
|
2881
|
+
* @param draft - Values entered so far.
|
|
2882
|
+
* @returns Visible fields, in declared order.
|
|
2883
|
+
*/
|
|
2884
|
+
declare function visibleFields(spec: XenoCredentialFormSpec, draft: CredentialDraft): XenoCredentialField[];
|
|
2885
|
+
/** A per-field validation result, keyed by field key. */
|
|
2886
|
+
type CredentialErrors = Record<string, string>;
|
|
2887
|
+
/**
|
|
2888
|
+
* Validate a draft against its descriptor.
|
|
2889
|
+
*
|
|
2890
|
+
* **Only visible fields are required.** A required field hidden by its own `showWhen` would
|
|
2891
|
+
* otherwise block submission with an error pointing at a control that is not on screen — which
|
|
2892
|
+
* looks, to the user, exactly like a broken form.
|
|
2893
|
+
*
|
|
2894
|
+
* @param spec - The form descriptor.
|
|
2895
|
+
* @param draft - Values entered so far.
|
|
2896
|
+
* @returns Messages by field key. Empty means valid.
|
|
2897
|
+
*
|
|
2898
|
+
* @example
|
|
2899
|
+
* ```ts
|
|
2900
|
+
* validateCredentialDraft({ …, fields: [{ key: 'token', label: 'Token', type: 'password', required: true }] }, {})
|
|
2901
|
+
* // → { token: 'Token is required.' }
|
|
2902
|
+
* ```
|
|
2903
|
+
*/
|
|
2904
|
+
declare function validateCredentialDraft(spec: XenoCredentialFormSpec, draft: CredentialDraft): CredentialErrors;
|
|
2905
|
+
/**
|
|
2906
|
+
* Drop values belonging to hidden fields.
|
|
2907
|
+
*
|
|
2908
|
+
* A user who fills in a password, switches the mode selector to "API key", and submits would
|
|
2909
|
+
* otherwise send the password too — a secret they believe they abandoned, travelling to a host that
|
|
2910
|
+
* will store it. Submitting only what is on screen is what makes "I changed my mind" mean it.
|
|
2911
|
+
*
|
|
2912
|
+
* @param spec - The form descriptor.
|
|
2913
|
+
* @param draft - Everything entered.
|
|
2914
|
+
* @returns Only the visible fields' values.
|
|
2915
|
+
*/
|
|
2916
|
+
declare function pruneHiddenValues(spec: XenoCredentialFormSpec, draft: CredentialDraft): CredentialDraft;
|
|
2917
|
+
/**
|
|
2918
|
+
* Is this descriptor usable?
|
|
2919
|
+
*
|
|
2920
|
+
* §6 L2: a form-mode credential with no fields must be **rejected at registration, not at click** —
|
|
2921
|
+
* a card that offers "Connect", opens an empty modal, and can never be completed is a dead end the
|
|
2922
|
+
* user cannot diagnose. A host validating its own descriptors at registration time turns that into
|
|
2923
|
+
* a developer-visible error instead.
|
|
2924
|
+
*
|
|
2925
|
+
* @param spec - The descriptor.
|
|
2926
|
+
* @returns A reason it is unusable, or `null`.
|
|
2927
|
+
*
|
|
2928
|
+
* @example
|
|
2929
|
+
* ```ts
|
|
2930
|
+
* credentialSpecProblem({ requestId: '1', connectionId: 'c', title: 'X', mode: 'form' })
|
|
2931
|
+
* // → 'A form credential needs at least one field.'
|
|
2932
|
+
* ```
|
|
2933
|
+
*/
|
|
2934
|
+
declare function credentialSpecProblem(spec: XenoCredentialFormSpec): string | null;
|
|
2935
|
+
|
|
2936
|
+
/**
|
|
2937
|
+
* The OAuth halves — loopback hardening, state, PKCE, and token refresh.
|
|
2938
|
+
*
|
|
2939
|
+
* ## Whose code is this?
|
|
2940
|
+
*
|
|
2941
|
+
* The panel's half of an OAuth flow is small and boring: render the redirect-URI hint verbatim, show
|
|
2942
|
+
* a button, wait for `credentialChanged`. The *host's* half is where every landmine in §6 lives —
|
|
2943
|
+
* and in `xeno-apps` the connectors panel **is** the host-side resolver, so this package is where
|
|
2944
|
+
* that logic belongs.
|
|
2945
|
+
*
|
|
2946
|
+
* Everything here is pure and injectable: no timers of its own, no global clock, no network. A host
|
|
2947
|
+
* that already owns a vault composes these functions into it; a host that does not gets a correct
|
|
2948
|
+
* implementation instead of the fourth independent one. Each landmine below was solved exactly once
|
|
2949
|
+
* somewhere in the ecosystem and nowhere twice — which is the definition of knowledge that should
|
|
2950
|
+
* live in one package.
|
|
2951
|
+
*
|
|
2952
|
+
* @module
|
|
2953
|
+
*/
|
|
2954
|
+
/**
|
|
2955
|
+
* The loopback redirect URI, **derived from the port actually bound**.
|
|
2956
|
+
*
|
|
2957
|
+
* §6 L3 is a mismatch that costs an afternoon every time: one implementation falls back to
|
|
2958
|
+
* `localhost:3000` when three environment variables are unset — silently registering a URI the
|
|
2959
|
+
* server never listens on — while another derives it correctly from the loopback port. Deriving is
|
|
2960
|
+
* right, but the fix that actually stops the bug is **rendering the exact string to the user**, so
|
|
2961
|
+
* the value they paste into the provider's console and the value the token exchange sends are the
|
|
2962
|
+
* same characters.
|
|
2963
|
+
*
|
|
2964
|
+
* `127.0.0.1` is deliberate, not stylistic: RFC 8252 §7.3 requires the literal IP rather than
|
|
2965
|
+
* `localhost`, because `localhost` can resolve to an address another process is listening on.
|
|
2966
|
+
*
|
|
2967
|
+
* @param port - The port the loopback server actually bound.
|
|
2968
|
+
* @param path - The callback path. Default `/callback`.
|
|
2969
|
+
* @returns The exact URI to register with the provider.
|
|
2970
|
+
*
|
|
2971
|
+
* @example
|
|
2972
|
+
* ```ts
|
|
2973
|
+
* redirectUriFor(8730) // → 'http://127.0.0.1:8730/callback'
|
|
2974
|
+
* ```
|
|
2975
|
+
*/
|
|
2976
|
+
declare function redirectUriFor(port: number, path?: string): string;
|
|
2977
|
+
/** A request arriving at the loopback callback server. */
|
|
2978
|
+
interface LoopbackRequest {
|
|
2979
|
+
/** The request target, e.g. `/callback?code=…&state=…`. */
|
|
2980
|
+
url: string;
|
|
2981
|
+
/** Request headers, any casing. */
|
|
2982
|
+
headers: Record<string, string | string[] | undefined>;
|
|
2983
|
+
method?: string;
|
|
2984
|
+
}
|
|
2985
|
+
/** The verdict on a loopback request. */
|
|
2986
|
+
type LoopbackVerdict = {
|
|
2987
|
+
ok: true;
|
|
2988
|
+
code: string;
|
|
2989
|
+
state: string;
|
|
2990
|
+
} | {
|
|
2991
|
+
ok: false;
|
|
2992
|
+
status: 404 | 400 | 403;
|
|
2993
|
+
reason: string;
|
|
2994
|
+
};
|
|
2995
|
+
/**
|
|
2996
|
+
* Validate a request hitting the one-shot loopback server.
|
|
2997
|
+
*
|
|
2998
|
+
* The engineering log's note on this is blunt: *"binding 127.0.0.1 is NOT enough."* A page on any
|
|
2999
|
+
* website can point a `<form>` or an `<img>` at `http://127.0.0.1:8730/callback?...`, and DNS
|
|
3000
|
+
* rebinding lets an attacker's domain resolve to 127.0.0.1 so the browser sends the request with
|
|
3001
|
+
* **their** hostname in the `Host` header. Binding the interface stops remote packets; it does not
|
|
3002
|
+
* stop the user's own browser being aimed at the port.
|
|
3003
|
+
*
|
|
3004
|
+
* The surveyed implementation is good on three counts — one-shot, no reflected parameters, 404s
|
|
3005
|
+
* non-callback paths — and **does not validate `Host`**. This adds it: the header must be a literal
|
|
3006
|
+
* loopback address with the expected port, so a rebound hostname is refused before the `code` is
|
|
3007
|
+
* ever read.
|
|
3008
|
+
*
|
|
3009
|
+
* @param request - The incoming request.
|
|
3010
|
+
* @param expected - The port and path the server bound.
|
|
3011
|
+
* @returns The extracted `code`/`state`, or a refusal with a status.
|
|
3012
|
+
*
|
|
3013
|
+
* @example
|
|
3014
|
+
* ```ts
|
|
3015
|
+
* validateLoopbackRequest(
|
|
3016
|
+
* { url: '/callback?code=a&state=b', headers: { host: 'evil.com:8730' } },
|
|
3017
|
+
* { port: 8730 },
|
|
3018
|
+
* )
|
|
3019
|
+
* // → { ok: false, status: 403, reason: 'unexpected Host header' }
|
|
3020
|
+
* ```
|
|
3021
|
+
*/
|
|
3022
|
+
declare function validateLoopbackRequest(request: LoopbackRequest, expected: {
|
|
3023
|
+
port: number;
|
|
3024
|
+
path?: string;
|
|
3025
|
+
}): LoopbackVerdict;
|
|
3026
|
+
/**
|
|
3027
|
+
* Is this `Host` header a literal loopback address on the expected port?
|
|
3028
|
+
*
|
|
3029
|
+
* Only the literal forms pass. **`localhost` is rejected on purpose**: it is a name, and a name is
|
|
3030
|
+
* exactly what DNS rebinding controls.
|
|
3031
|
+
*/
|
|
3032
|
+
declare function isLoopbackHost(host: string, port: number): boolean;
|
|
3033
|
+
/** One outstanding authorization attempt. */
|
|
3034
|
+
interface OAuthStateRecord {
|
|
3035
|
+
state: string;
|
|
3036
|
+
connectionId: string;
|
|
3037
|
+
/** PKCE verifier, held until the token exchange. */
|
|
3038
|
+
verifier?: string;
|
|
3039
|
+
/** Absolute epoch ms. */
|
|
3040
|
+
expiresAt: number;
|
|
3041
|
+
/** Anything the host needs on the way back. **Never a secret.** */
|
|
3042
|
+
meta?: Record<string, string>;
|
|
3043
|
+
}
|
|
3044
|
+
/** Options for {@link createOAuthStateStore}. */
|
|
3045
|
+
interface OAuthStateStoreOptions {
|
|
3046
|
+
now?: () => number;
|
|
3047
|
+
/** Time-to-live. Default 10 minutes, per the spec. */
|
|
3048
|
+
ttlMs?: number;
|
|
3049
|
+
/** Random state generator, injectable for tests. Must produce ≥192 bits of entropy. */
|
|
3050
|
+
randomState?: () => string;
|
|
3051
|
+
}
|
|
3052
|
+
/** An atomic, single-use OAuth state store. */
|
|
3053
|
+
interface OAuthStateStore {
|
|
3054
|
+
/** Mint a state for a connection. */
|
|
3055
|
+
issue(connectionId: string, options?: {
|
|
3056
|
+
verifier?: string;
|
|
3057
|
+
meta?: Record<string, string>;
|
|
3058
|
+
}): OAuthStateRecord;
|
|
3059
|
+
/**
|
|
3060
|
+
* **Atomically** take the record for a state. A second call with the same state returns `null`.
|
|
3061
|
+
*
|
|
3062
|
+
* There is deliberately no `peek`.
|
|
3063
|
+
*/
|
|
3064
|
+
consume(state: string): OAuthStateRecord | null;
|
|
3065
|
+
/** Drop expired records. Returns how many went. */
|
|
3066
|
+
sweep(): number;
|
|
3067
|
+
/** Outstanding count, for tests and diagnostics. */
|
|
3068
|
+
size(): number;
|
|
3069
|
+
}
|
|
3070
|
+
/**
|
|
3071
|
+
* A CSRF state store whose only read is a **consume**.
|
|
3072
|
+
*
|
|
3073
|
+
* §6 L11: one surveyed implementation *peeks* at the state to look up the connection and then
|
|
3074
|
+
* consumes it in a second call. Two concurrent callbacks both pass the peek, and the window between
|
|
3075
|
+
* the two calls is exactly the race a CSRF state exists to close. The invariant is *one atomic
|
|
3076
|
+
* consume, checked at the point of use* — so this store **has no peek to misuse**. Anything the
|
|
3077
|
+
* caller wanted from a peek is returned by the consume itself.
|
|
3078
|
+
*
|
|
3079
|
+
* @param options - Clock, TTL and randomness, all injectable.
|
|
3080
|
+
* @returns The store.
|
|
3081
|
+
*
|
|
3082
|
+
* @example
|
|
3083
|
+
* ```ts
|
|
3084
|
+
* const store = createOAuthStateStore()
|
|
3085
|
+
* const { state } = store.issue('conn-1', { verifier })
|
|
3086
|
+
* store.consume(state) // → the record
|
|
3087
|
+
* store.consume(state) // → null. Always.
|
|
3088
|
+
* ```
|
|
3089
|
+
*/
|
|
3090
|
+
declare function createOAuthStateStore(options?: OAuthStateStoreOptions): OAuthStateStore;
|
|
3091
|
+
/**
|
|
3092
|
+
* Base64url-encode bytes, without padding.
|
|
3093
|
+
*
|
|
3094
|
+
* Encoded by hand rather than through `btoa` or `Buffer`: this package targets the browser *and*
|
|
3095
|
+
* QuickJS *and* Node, `btoa` is absent in some of those, `Buffer` in others, and a panel that
|
|
3096
|
+
* assumes either one has quietly taken a runtime dependency on its substrate. Twelve lines removes
|
|
3097
|
+
* the question.
|
|
3098
|
+
*
|
|
3099
|
+
* @param bytes - Raw bytes.
|
|
3100
|
+
* @returns Unpadded base64url text, as RFC 7636 requires for a PKCE verifier.
|
|
3101
|
+
*
|
|
3102
|
+
* @example
|
|
3103
|
+
* ```ts
|
|
3104
|
+
* base64Url(new Uint8Array([255, 254, 253])) // → '__79'
|
|
3105
|
+
* ```
|
|
3106
|
+
*/
|
|
3107
|
+
declare function base64Url(bytes: Uint8Array): string;
|
|
3108
|
+
/** A PKCE verifier/challenge pair. */
|
|
3109
|
+
interface PkcePair {
|
|
3110
|
+
verifier: string;
|
|
3111
|
+
challenge: string;
|
|
3112
|
+
method: 'S256';
|
|
3113
|
+
}
|
|
3114
|
+
/**
|
|
3115
|
+
* Generate a PKCE pair.
|
|
3116
|
+
*
|
|
3117
|
+
* **S256 only.** `plain` is still in RFC 7636 and is worthless: the "challenge" is the verifier, so
|
|
3118
|
+
* anything that can read the authorization request can complete the exchange. Offering it as an
|
|
3119
|
+
* option would only ever be a way to pick the broken one.
|
|
3120
|
+
*
|
|
3121
|
+
* @param digest - SHA-256 implementation. Defaults to WebCrypto; injectable for tests.
|
|
3122
|
+
* @returns The pair.
|
|
3123
|
+
*
|
|
3124
|
+
* @example
|
|
3125
|
+
* ```ts
|
|
3126
|
+
* const { verifier, challenge } = await createPkcePair()
|
|
3127
|
+
* ```
|
|
3128
|
+
*/
|
|
3129
|
+
declare function createPkcePair(digest?: (input: Uint8Array) => Promise<Uint8Array>): Promise<PkcePair>;
|
|
3130
|
+
/** A stored OAuth credential. **Never rendered, never serialized into a document.** */
|
|
3131
|
+
interface StoredOAuthToken {
|
|
3132
|
+
accessToken: string;
|
|
3133
|
+
refreshToken?: string;
|
|
3134
|
+
/** Absolute epoch ms. Never a duration — see {@link mergeRefreshResponse}. */
|
|
3135
|
+
expiresAt?: number;
|
|
3136
|
+
tokenType?: string;
|
|
3137
|
+
scope?: string;
|
|
3138
|
+
}
|
|
3139
|
+
/** A token endpoint's response. */
|
|
3140
|
+
interface TokenEndpointResponse {
|
|
3141
|
+
access_token?: string;
|
|
3142
|
+
refresh_token?: string;
|
|
3143
|
+
/** Seconds from *now*, as OAuth defines it. */
|
|
3144
|
+
expires_in?: number;
|
|
3145
|
+
token_type?: string;
|
|
3146
|
+
scope?: string;
|
|
3147
|
+
}
|
|
3148
|
+
/** Clock skew allowance, ms. */
|
|
3149
|
+
declare const EXPIRY_SKEW_MS = 60000;
|
|
3150
|
+
/**
|
|
3151
|
+
* Merge a refresh response over the stored credential.
|
|
3152
|
+
*
|
|
3153
|
+
* **§6 L4, both halves:**
|
|
3154
|
+
*
|
|
3155
|
+
* 1. *Expiry is absolute epoch ms, with a 60 s skew.* A stored `expires_in` is a duration measured
|
|
3156
|
+
* from a moment nobody wrote down; after a restart it is unusable, and after a suspend it is a
|
|
3157
|
+
* lie. It is converted here, once, at the only point where "now" is known to be the right now.
|
|
3158
|
+
* 2. 🔴 *A response omitting `refresh_token` must **preserve** the prior one.* Most providers rotate
|
|
3159
|
+
* the refresh token on every use, but a large minority — and every provider using a
|
|
3160
|
+
* non-expiring refresh token — return only a new access token. Overwriting with `undefined`
|
|
3161
|
+
* destroys the only credential that can ever get another access token, and the failure does not
|
|
3162
|
+
* appear until the *next* refresh, long after the code that caused it.
|
|
3163
|
+
*
|
|
3164
|
+
* @param prior - What is stored today, if anything.
|
|
3165
|
+
* @param response - The token endpoint's answer.
|
|
3166
|
+
* @param now - Current epoch ms.
|
|
3167
|
+
* @returns The credential to store.
|
|
3168
|
+
*
|
|
3169
|
+
* @example
|
|
3170
|
+
* ```ts
|
|
3171
|
+
* mergeRefreshResponse({ accessToken: 'a', refreshToken: 'KEEP' }, { access_token: 'b' }, 0)
|
|
3172
|
+
* // → { accessToken: 'b', refreshToken: 'KEEP' }
|
|
3173
|
+
* ```
|
|
3174
|
+
*/
|
|
3175
|
+
declare function mergeRefreshResponse(prior: StoredOAuthToken | undefined, response: TokenEndpointResponse, now: number): StoredOAuthToken;
|
|
3176
|
+
/**
|
|
3177
|
+
* Does this credential need refreshing?
|
|
3178
|
+
*
|
|
3179
|
+
* @param token - The stored credential.
|
|
3180
|
+
* @param now - Current epoch ms.
|
|
3181
|
+
* @param skewMs - Safety margin. Default {@link EXPIRY_SKEW_MS}.
|
|
3182
|
+
* @returns Whether to refresh before using it.
|
|
3183
|
+
*
|
|
3184
|
+
* @example
|
|
3185
|
+
* ```ts
|
|
3186
|
+
* needsRefresh({ accessToken: 'a', expiresAt: 100_000 }, 40_000) // → true (inside the 60s skew)
|
|
3187
|
+
* ```
|
|
3188
|
+
*/
|
|
3189
|
+
declare function needsRefresh(token: StoredOAuthToken | undefined, now: number, skewMs?: number): boolean;
|
|
3190
|
+
/** Coordinates refreshes so N consumers produce one exchange. */
|
|
3191
|
+
interface RefreshCoordinator<T> {
|
|
3192
|
+
/**
|
|
3193
|
+
* Refresh `key`, or join the refresh already running for it.
|
|
3194
|
+
*
|
|
3195
|
+
* @param key - Credential reference.
|
|
3196
|
+
* @param run - Performs the exchange. Called at most once per concurrent burst.
|
|
3197
|
+
*/
|
|
3198
|
+
refresh(key: string, run: () => Promise<T>): Promise<T>;
|
|
3199
|
+
/** How many exchanges are in flight. */
|
|
3200
|
+
inFlight(): number;
|
|
3201
|
+
/** Refresh many keys, reporting **per key** — §6 L6. */
|
|
3202
|
+
refreshAll(keys: string[], run: (key: string) => Promise<T>): Promise<{
|
|
3203
|
+
key: string;
|
|
3204
|
+
ok: boolean;
|
|
3205
|
+
value?: T;
|
|
3206
|
+
error?: string;
|
|
3207
|
+
}[]>;
|
|
3208
|
+
}
|
|
3209
|
+
/**
|
|
3210
|
+
* Dedupe concurrent refreshes of the same credential.
|
|
3211
|
+
*
|
|
3212
|
+
* §6 L5a: five panels sharing one connection all notice the expiry in the same tick and all start a
|
|
3213
|
+
* refresh. With a rotating refresh token that is not merely wasteful — the first exchange
|
|
3214
|
+
* invalidates the token the other four are holding, so four of them fail and, depending on the
|
|
3215
|
+
* provider, the whole family is revoked for reuse. One exchange, four joiners.
|
|
3216
|
+
*
|
|
3217
|
+
* @returns The coordinator.
|
|
3218
|
+
*
|
|
3219
|
+
* @example
|
|
3220
|
+
* ```ts
|
|
3221
|
+
* const coordinator = createRefreshCoordinator<Token>()
|
|
3222
|
+
* await Promise.all([a, b, c].map(() => coordinator.refresh('$cred:1', exchange)))
|
|
3223
|
+
* // `exchange` ran exactly once.
|
|
3224
|
+
* ```
|
|
3225
|
+
*/
|
|
3226
|
+
declare function createRefreshCoordinator<T>(): RefreshCoordinator<T>;
|
|
3227
|
+
|
|
3228
|
+
/**
|
|
3229
|
+
* The Connectors panel view — connection list, health, and the credential form.
|
|
3230
|
+
*
|
|
3231
|
+
* Composes `@xenosystem/workbench/primitives`. **The host must import
|
|
3232
|
+
* `@xenosystem/workbench/primitives.css`** (or let the workbench inject it) or this renders unstyled with
|
|
3233
|
+
* no error.
|
|
3234
|
+
*
|
|
3235
|
+
* Two things this view deliberately does NOT have:
|
|
3236
|
+
*
|
|
3237
|
+
* - **No secret reveal.** `xeno-hub`'s `EnvironmentSecretsSection` exposes stored secrets behind an
|
|
3238
|
+
* eye-toggle; that affordance does not come with this panel. The panel never receives a stored
|
|
3239
|
+
* secret, so there is nothing to reveal.
|
|
3240
|
+
* - **No masked value round-trip.** A form field is uncontrolled-by-design local state that is
|
|
3241
|
+
* cleared at submit; nothing here reads a value back from a saved document.
|
|
3242
|
+
*
|
|
3243
|
+
* @module
|
|
3244
|
+
*/
|
|
3245
|
+
|
|
3246
|
+
/** Props for {@link ConnectorsPanelView}. */
|
|
3247
|
+
interface ConnectorsPanelViewProps {
|
|
3248
|
+
controller: ConnectorsController;
|
|
3249
|
+
/** Hint shown when no source is configured. */
|
|
3250
|
+
emptyHint?: string;
|
|
3251
|
+
}
|
|
3252
|
+
/** The Connectors panel view. */
|
|
3253
|
+
declare function ConnectorsPanelView({ controller, emptyHint, }: ConnectorsPanelViewProps): ReactNode;
|
|
3254
|
+
|
|
3255
|
+
export { AGENT_HIDDEN_COMMANDS, ALL_AGGREGATES, ALL_CHART_TYPES, type BuildChartOptions, type BuildSelectOptions, type BuildSelectResult, CHART_EMPTY_FILL, CHART_PALETTE_STEPS, CHART_PANEL_ID, CONNECTORS_PANEL_ID, type CellAddress, ChartController, type ChartControllerOptions, type ChartHostBridge, type ChartPanelState, ChartPanelView, type ChartPanelViewProps, type ChartRenderContext, type ChartRenderInput, type ChartRenderOptions, type ChartRenderer, type ChartViewState, type ColumnLayout, type ColumnWindow, ConnectorExecuteInput, ConnectorExecuteOutput, ConnectorTransport, ConnectorsController, type ConnectorsControllerOptions, type ConnectorsHostBridge, ConnectorsPanelState, ConnectorsPanelView, type ConnectorsPanelViewProps, type ConnectorsRenderContext, ConnectorsViewState, type CreateChartPanelOptions, type CreateConnectorExecutorOptions, type CreateConnectorsPanelOptions, type CreateFieldsPanelOptions, type CreateMetricsPanelOptions, type CreateTablePanelOptions, type CredentialDraft, type CredentialErrors, DEFAULT_COLUMN_WIDTH, DEFAULT_PAGE_SIZE, DEFAULT_RANGES, type DeltaDirection, EMPTY_LAYOUT, EXPIRY_SKEW_MS, type ExecuteError, FIELDS_PANEL_ID, FieldsController, type FieldsControllerOptions, type FieldsHostBridge, type FieldsPanelState, FieldsPanelView, type FieldsPanelViewProps, type FieldsRenderContext, type FieldsViewState, type HostSqlResolver, IMPLEMENTED_VIEW_MODES, type LoopbackRequest, type LoopbackVerdict, METRICS_PANEL_ID, MIN_COLUMN_WIDTH, MONO_FILL_PALETTE, MONO_PALETTE, MetricsController, type MetricsControllerOptions, type MetricsHostBridge, type MetricsPanelState, MetricsPanelView, type MetricsPanelViewProps, type MetricsRenderContext, type MetricsViewState, type NumericConstraints, type OAuthStateRecord, type OAuthStateStore, type OAuthStateStoreOptions, type ParseCsvOptions, type ParseResult, type ParsedCsv, type PkcePair, PushdownSplit, type RefreshCoordinator, type ResolvedColumn, SHELF_PRESETS, type StoredOAuthToken, SubstrateFacilities, TABLE_PANEL_ID, TableController, type TableControllerOptions, type TableHostBridge, type TablePanelState, TablePanelView, type TablePanelViewProps, type TableRenderContext, type TableViewMode, type TableViewState, type TokenEndpointResponse, type VirtualWindow, type XenoAlertLevel, type XenoCalculatedFieldRequest, type XenoCellEdit, type XenoChartData, type XenoChartDrillDown, type XenoChartEncoding, type XenoChartExportRequest, type XenoChartHighlight, type XenoChartSeries, type XenoChartType, XenoConnection, XenoConnectionStatus, XenoConnectorFetch, XenoConnectorFileReader, XenoConnectorQueryRequest, XenoCredentialField, XenoCredentialFormSpec, XenoCredentialResult, type XenoFieldStat, type XenoMetricAlert, type XenoMetricRange, type XenoMetricSpec, type XenoMetricThreshold, type XenoMetricValue, XenoResolverErrorCode, XenoRestPaging, type XenoRowActivate, type XenoShelfEntry, type XenoShelfSpec, XenoSqlEngine, applyPrecision, base64Url, breachSideOf, buildChartData, buildRestUrl, buildSelect, chartManifest, chartPanel, clamp, classifyHttpStatus, coerceCsvValues, columnIndexAt, columnsOf, compileForHost, computeColumnWindow, computeWindow, connectorsManifest, connectorsPanel, constraintsOfColumn, createChartPanel, createConnectorExecutor, createConnectorsPanel, createFieldsPanel, createMetricsPanel, createOAuthStateStore, createPkcePair, createRefreshCoordinator, createTablePanel, credentialSpecProblem, defaultAggregateFor, defaultEncoding, deltaToneOf, emptyFilterGroup, entityOf, escapeLikePattern, executeCsvFile, executeDuckDb, executeGraphql, executeJsonFile, executeRest, executeSql, extractJsonPath, extractRows, fieldsManifest, fieldsPanel, firstGraphqlError, frozenWidth, getChartRenderer, isCategorical, isFieldVisible, isFilled, isLeafFilter, isLoopbackHost, isSafeTableName, mergeRefreshResponse, metricsManifest, metricsPanel, moveCell, moveColumn, needsRefresh, nextPageToken, normalizeLayout, normalizeNumeric, offerableAggregates, offerableFilterOps, parseCsv, parseJsonBody, parseJsonPath, parseLinkHeader, parseNumeric, pruneHiddenValues, queriesEqual, quoteIdent, quoteLiteral, redirectUriFor, renderFilterGroup, resolveChartColors, resolveColumns, rowsForMark, scrollLeftToReveal, scrollTopToReveal, setChartRenderer, setColumnVisible, setColumnWidth, setFrozenCount, sniffDelimiter, suggestedShelfFor, tableManifest, tablePanel, toChartJsConfig, toExecuteError, totalWidth, uniqueTableName, unsupportedMessage, validateCredentialDraft, validateEncoding, validateLoopbackRequest, visibleFields };
|