@w6w/ui 0.1.1 → 0.2.11

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +7 -5
  3. package/dist/AppPicker.d.ts +48 -2
  4. package/dist/CanvasLayer.d.ts +38 -0
  5. package/dist/{CodeBlock-Dot3Pd7P.js → CodeBlock-BqXDA35P.js} +2 -2
  6. package/dist/{CodeBlock-Dot3Pd7P.js.map → CodeBlock-BqXDA35P.js.map} +1 -1
  7. package/dist/{CodeBlock-CZu4TFSi.cjs → CodeBlock-CulfbnZD.cjs} +2 -2
  8. package/dist/{CodeBlock-CZu4TFSi.cjs.map → CodeBlock-CulfbnZD.cjs.map} +1 -1
  9. package/dist/ConfirmModal-Ln99S9NT.js +7919 -0
  10. package/dist/ConfirmModal-Ln99S9NT.js.map +1 -0
  11. package/dist/ConfirmModal-fjHW5rN-.cjs +14 -0
  12. package/dist/ConfirmModal-fjHW5rN-.cjs.map +1 -0
  13. package/dist/ConnectionHeader.d.ts +19 -0
  14. package/dist/Copyable-DA6eiU7w.cjs +2 -0
  15. package/dist/Copyable-DA6eiU7w.cjs.map +1 -0
  16. package/dist/{Copyable-1niV9gUa.js → Copyable-F4NTaG77.js} +91 -15
  17. package/dist/Copyable-F4NTaG77.js.map +1 -0
  18. package/dist/EditConnectionModal.d.ts +26 -0
  19. package/dist/JsonEditor.d.ts +8 -0
  20. package/dist/Markdown.d.ts +25 -0
  21. package/dist/ParamsForm.d.ts +7 -1
  22. package/dist/StepBuilderModal.d.ts +23 -5
  23. package/dist/WorkflowFlowEditor.d.ts +14 -3
  24. package/dist/app-pages.d.ts +5 -0
  25. package/dist/canvas-types.d.ts +36 -0
  26. package/dist/canvas-utils.d.ts +44 -0
  27. package/dist/code.cjs +1 -1
  28. package/dist/code.mjs +2 -2
  29. package/dist/components/AuthFieldsForm.d.ts +19 -1
  30. package/dist/components/BudgetMeter.d.ts +15 -0
  31. package/dist/components/CallableList.d.ts +58 -0
  32. package/dist/components/Combobox.d.ts +8 -1
  33. package/dist/components/ConnectionList.d.ts +43 -0
  34. package/dist/components/CredentialPicker.d.ts +35 -0
  35. package/dist/components/DataTable.d.ts +27 -0
  36. package/dist/components/EmptyState.d.ts +9 -0
  37. package/dist/components/ListItem.d.ts +16 -6
  38. package/dist/components/ReliabilityChart.d.ts +10 -0
  39. package/dist/components/ReliabilityList.d.ts +47 -0
  40. package/dist/components/icons.d.ts +1 -1
  41. package/dist/connection-app-meta.d.ts +32 -0
  42. package/dist/dynamic-fields.d.ts +60 -0
  43. package/dist/editor-theme.d.ts +24 -0
  44. package/dist/flow-fanout.d.ts +2 -2
  45. package/dist/flow-types.d.ts +18 -1
  46. package/dist/flow-utils.d.ts +10 -3
  47. package/dist/flow.cjs +2 -2
  48. package/dist/flow.cjs.map +1 -1
  49. package/dist/flow.d.ts +9 -0
  50. package/dist/flow.mjs +1519 -1138
  51. package/dist/flow.mjs.map +1 -1
  52. package/dist/index.cjs +1 -1
  53. package/dist/index.cjs.map +1 -1
  54. package/dist/index.d.ts +30 -2
  55. package/dist/index.mjs +2053 -1005
  56. package/dist/index.mjs.map +1 -1
  57. package/dist/note-markdown.d.ts +22 -0
  58. package/dist/provider.d.ts +73 -5
  59. package/dist/reliability-board-layout.d.ts +121 -0
  60. package/dist/resolve-params.d.ts +5 -0
  61. package/dist/run-visuals.d.ts +4 -1
  62. package/dist/step-problems.d.ts +34 -0
  63. package/dist/types.d.ts +117 -3
  64. package/dist/use-dynamic-fields.d.ts +23 -0
  65. package/package.json +5 -3
  66. package/src/styles/_budget-meter.scss +46 -0
  67. package/src/styles/_callable-list.scss +69 -0
  68. package/src/styles/_canvas.scss +318 -0
  69. package/src/styles/_connections.scss +88 -0
  70. package/src/styles/_data-table.scss +55 -0
  71. package/src/styles/_empty-state.scss +21 -0
  72. package/src/styles/_flow.scss +11 -0
  73. package/src/styles/_fx.scss +23 -0
  74. package/src/styles/_layout.scss +10 -0
  75. package/src/styles/_lists.scss +4 -0
  76. package/src/styles/_markdown.scss +20 -0
  77. package/src/styles/_reliability.scss +339 -0
  78. package/src/styles/_tester.scss +1 -14
  79. package/src/styles.css +1142 -17
  80. package/src/styles.scss +8 -0
  81. package/dist/ConfirmModal-8avl9Nv8.cjs +0 -11
  82. package/dist/ConfirmModal-8avl9Nv8.cjs.map +0 -1
  83. package/dist/ConfirmModal-CD-P8mhN.js +0 -6335
  84. package/dist/ConfirmModal-CD-P8mhN.js.map +0 -1
  85. package/dist/Copyable-1niV9gUa.js.map +0 -1
  86. package/dist/Copyable-CYZU1o4X.cjs +0 -2
  87. package/dist/Copyable-CYZU1o4X.cjs.map +0 -1
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The note editor's Markdown round-trip. A note is edited in place as a `contentEditable` block
3
+ * (no textarea); these two functions convert between its stored Markdown and that editable DOM.
4
+ *
5
+ * The editable model is line-based and covers exactly what the note toolbar writes: a heading
6
+ * level per line (`#`…`######`) and `**bold**` runs. Everything else (links, lists, `code`, …) is
7
+ * kept as literal text while editing and rendered by `Markdown` when not, so a line the toolbar
8
+ * never touched round-trips byte-for-byte.
9
+ *
10
+ * The DOM is built with `createElement` + text nodes only — note content is imported,
11
+ * attacker-controllable text and must never reach `innerHTML`.
12
+ */
13
+ /** Build the editable DOM for `content` into `root` (its children are replaced). */
14
+ export declare function renderEditable(root: HTMLElement, content: string): void;
15
+ /** Serialize the editable DOM under `root` back to Markdown. */
16
+ export declare function editableToMarkdown(root: HTMLElement): string;
17
+ /**
18
+ * The content handed to `Markdown` for display. A note is typed line by line, so a single
19
+ * newline must show as a line break — Markdown would fold it into a space. A soft break between
20
+ * two non-empty lines becomes a hard break (two trailing spaces); the stored content is untouched.
21
+ */
22
+ export declare function noteDisplayMarkdown(content: string): string;
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { ActionDef, ApiCallRecord, AppSummary, AuthDef, ConnectionSummary, FunctionDetail, FunctionSummary, SavedTest, SubscriptionSummary, ThemeMode, TriggerSummary, WorkflowDetail, WorkflowSummary } from './types.ts';
2
+ import { ActionDef, ApiCallRecord, AppSummary, AuthDef, ConnectionBinding, ConnectionSummary, CreateConnectionBody, CredentialDependents, CredentialSummary, CredentialTestResult, FunctionDetail, FunctionSummary, ResolveFieldsBody, ResolveFieldsResult, SavedTest, SubscriptionSummary, ThemeMode, TriggerSummary, WorkflowDetail, WorkflowSummary } from './types.ts';
3
3
  /**
4
4
  * A single tester-run summary from the unified `run_log` ledger, keyed to a
5
5
  * connection. Thin by design — the full result payload stays server-side in
@@ -111,6 +111,8 @@ export interface ListAppsPageOptions {
111
111
  q?: string;
112
112
  /** Server-side category filter (e.g. `"ai"`). */
113
113
  category?: string;
114
+ /** Server-side id allow-list (`GET /apps?ids=a,b`); `total` is then the filtered total. */
115
+ ids?: string[];
114
116
  /** Opaque pagination cursor from a prior {@link AppsPageResult.nextCursor}. */
115
117
  cursor?: string;
116
118
  /** Page size; a host may clamp this to its own bounds. */
@@ -119,12 +121,21 @@ export interface ListAppsPageOptions {
119
121
  compact?: boolean;
120
122
  /** Abort this request. Local request control only; never a wire field. */
121
123
  signal?: AbortSignal;
124
+ /** Snapshot id from a prior {@link AppsPageResult.snapshot}. Navigation normally uses `cursor` alone. */
125
+ snapshot?: string;
126
+ /** Absolute offset into the snapshot. */
127
+ offset?: number;
122
128
  }
123
129
  /** One page of apps, as returned by {@link W6WApi.listAppsPage}. */
124
130
  export interface AppsPageResult {
125
131
  apps: AppSummary[];
126
132
  /** Present unless this is the last page. */
127
133
  nextCursor?: string;
134
+ snapshot?: string;
135
+ total?: number;
136
+ offset?: number;
137
+ limit?: number;
138
+ expiresAt?: string;
128
139
  }
129
140
  export interface W6WApi {
130
141
  /**
@@ -165,15 +176,67 @@ export interface W6WApi {
165
176
  listAppsByIds?(ids: readonly string[], options?: {
166
177
  signal?: AbortSignal;
167
178
  }): Promise<AppSummary[]>;
179
+ /**
180
+ * OPTIONAL: the tenant's model routes, for the `model-route` step-param
181
+ * picker (`@w6w/model` `complete`). Absent, the param renders as a plain text
182
+ * input and this is never called. A `broken` route is still selectable.
183
+ * `project` is the editor's own project (`WorkflowProjectProvider`) when it
184
+ * has one — a host should list THAT project's routes, not guess one from its
185
+ * URL; `undefined` leaves the choice to the host.
186
+ */
187
+ listModelRoutes?(project?: string): Promise<{
188
+ name: string;
189
+ model: string;
190
+ status: "active" | "broken";
191
+ }[]>;
192
+ /**
193
+ * OPTIONAL: resolve the choices of `options: { source: "resolveFields" }`
194
+ * params from the current form (`POST` to the host). Absent, those params
195
+ * render as plain inputs and this is never called.
196
+ */
197
+ resolveActionFields?(appId: string, actionKey: string, body: ResolveFieldsBody, opts?: {
198
+ signal?: AbortSignal;
199
+ }): Promise<ResolveFieldsResult>;
168
200
  /** Load auth methods declared by an app's manifest, with availability flags. */
169
201
  getAppAuth(appId: string): Promise<AuthDef[]>;
170
202
  /** Create a non-OAuth connection with a user-supplied credential. */
171
- createConnection(appId: string, body: {
172
- authKey: string;
173
- credential: Record<string, unknown>;
203
+ createConnection(appId: string, body: CreateConnectionBody): Promise<ConnectionSummary>;
204
+ /**
205
+ * Rename a connection and/or replace some of its credential fields (`PATCH /connections/:id`).
206
+ * Send only the fields being changed: the server merges a partial `credential` over the stored
207
+ * one. Optional: when absent, `EditConnectionModal` shows no Save button (a one-line notice
208
+ * instead) and hosts should not offer an Edit action. It never throws for the absence.
209
+ */
210
+ updateConnection?(id: string, body: {
174
211
  displayName?: string;
175
- profile?: Record<string, unknown>;
212
+ binding?: ConnectionBinding;
213
+ credentialId?: string;
214
+ credential?: Record<string, unknown>;
176
215
  }): Promise<ConnectionSummary>;
216
+ /** List stored credentials, optionally narrowed (`GET /credentials`). */
217
+ listCredentials?(filter?: {
218
+ contract?: string;
219
+ appId?: string;
220
+ authKey?: string;
221
+ }): Promise<CredentialSummary[]>;
222
+ /** Store a new credential (`POST /credentials`). `source` is the auth's field-value record. */
223
+ createCredential?(body: {
224
+ name: string;
225
+ contract: string;
226
+ source: Record<string, unknown>;
227
+ }): Promise<CredentialSummary>;
228
+ getCredential?(id: string): Promise<CredentialSummary>;
229
+ /** Replace a credential's secret (`POST /credentials/:id/rotate`). */
230
+ rotateCredential?(id: string, body: {
231
+ source: Record<string, unknown>;
232
+ }): Promise<CredentialSummary>;
233
+ /** Disable (`/disable`) or re-enable (`/enable`) a credential. */
234
+ setCredentialStatus?(id: string, status: "active" | "disabled"): Promise<CredentialSummary>;
235
+ /** Mint-level health check; distinct from a connection's app-access test. */
236
+ testCredential?(id: string): Promise<CredentialTestResult>;
237
+ credentialDependents?(id: string): Promise<CredentialDependents>;
238
+ /** Rejects with `ApiError` code `credential_in_use` (body.error.dependents) when in use. */
239
+ deleteCredential?(id: string): Promise<void>;
177
240
  /**
178
241
  * Start an OAuth 2.0 flow. Server builds the provider's authorize URL and
179
242
  * returns it; the caller opens it in a popup and awaits the server's
@@ -412,6 +475,11 @@ export declare function W6WUIProvider({ api, theme, children }: W6WUIProviderPro
412
475
  * `<W6WUIProvider>` — the common mistake is forgetting to wrap the app root.
413
476
  */
414
477
  export declare function useW6WApi(): W6WApi;
478
+ /**
479
+ * Like {@link useW6WApi} but returns `undefined` outside a provider instead of
480
+ * throwing — for optional enhancements (e.g. the `model-route` picker).
481
+ */
482
+ export declare function useOptionalW6WApi(): W6WApi | undefined;
415
483
  /** Scopes test-invokes under it to `project`; the editor wraps its body with this. */
416
484
  export declare function WorkflowProjectProvider({ project, children, }: {
417
485
  project?: string;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * How the reliability board arranges itself — the decisions that are arithmetic
3
+ * rather than markup, kept pure (no React, no DOM) so they test without a DOM
4
+ * and so `@w6w/embed` can import them beside the components.
5
+ *
6
+ * **None of these decide a STATE.** Every state read below arrived already
7
+ * decided by the server; nothing here compares a ratio, applies a threshold to
8
+ * call data, or invents a fifth word. What these decide is where a row is PUT,
9
+ * which is presentation.
10
+ */
11
+ /** The four words the server uses for a health verdict. */
12
+ export type ReliabilityViewState = "ok" | "degraded" | "down" | "unknown";
13
+ /**
14
+ * The row shape the reliability components render — owned by `@w6w/ui` so it
15
+ * imports nothing from the SDK. The SDK's `ReliabilityService` assigns to it
16
+ * structurally; the generic functions below keep the caller's richer row type.
17
+ */
18
+ export interface ReliabilityServiceView {
19
+ appId: string;
20
+ displayName: string;
21
+ /** Inline icon fields — the board never asks a catalog for them. */
22
+ iconSrc?: string;
23
+ iconSrcDark?: string;
24
+ brandColor?: string;
25
+ /** "Did YOUR calls succeed". */
26
+ state: ReliabilityViewState;
27
+ calls: number;
28
+ errors: {
29
+ e4xx: number;
30
+ e429: number;
31
+ eAuth: number;
32
+ e5xx: number;
33
+ };
34
+ p95Ms: number | null;
35
+ ongoingOutageId: string | null;
36
+ /** One entry per calendar day, oldest first, exactly as the server sent it. */
37
+ uptime: readonly {
38
+ day: string;
39
+ state: ReliabilityViewState;
40
+ }[];
41
+ /** "What does the vendor say about itself"; `null` when no public check exists. */
42
+ vendorStatus: {
43
+ state: ReliabilityViewState;
44
+ message: string | null;
45
+ } | null;
46
+ /** The server's tier for the row — read, never re-derived. */
47
+ attention: "now" | "recent" | "none";
48
+ }
49
+ /** One point of the performance scatter — a service with both a call volume and a measured p95. */
50
+ export interface PerformancePoint {
51
+ appId: string;
52
+ displayName: string;
53
+ calls: number;
54
+ p95Ms: number;
55
+ state: ReliabilityViewState;
56
+ /** Total errors across all four kinds — drives the point's size, not its position. */
57
+ errorCount: number;
58
+ }
59
+ /**
60
+ * Every service with at least one timed call, as a (calls, p95) point — what
61
+ * the performance chart plots. Excludes `p95Ms: null`: there is no Y position
62
+ * for a service with nothing timed.
63
+ */
64
+ export declare function performancePoints(services: readonly ReliabilityServiceView[]): PerformancePoint[];
65
+ /**
66
+ * Does this service want the reader's attention? READS the tier the server
67
+ * sent (`attention`), never re-derives it from `state` or `vendorStatus`: the
68
+ * server's `boardRank` is the one implementation, and a second copy here would
69
+ * drift. `now` and `recent` both count; they differ in tense only.
70
+ */
71
+ export declare function needsAttention(service: Pick<ReliabilityServiceView, "attention">): boolean;
72
+ /** The board in its three groups, each keeping the server's own ordering. */
73
+ export interface BoardPartition<T extends ReliabilityServiceView = ReliabilityServiceView> {
74
+ /** Broken right now, on either signal. */
75
+ now: T[];
76
+ /** Clear now, but this window holds a failed stretch or an open advisory. */
77
+ recent: T[];
78
+ /** Everything else, in the server's recency order. */
79
+ clear: T[];
80
+ }
81
+ /**
82
+ * Group the board without reordering within any group. Bucketing is not
83
+ * sorting: each group comes out in the order it arrived. An unrecognised
84
+ * `attention` value falls to `clear` rather than throwing — a client can be
85
+ * older than the server that answered it.
86
+ */
87
+ export declare function partitionBoard<T extends ReliabilityServiceView>(services: readonly T[]): BoardPartition<T>;
88
+ /**
89
+ * The uptime strip's cells for one service — the server's own array, in the
90
+ * order it arrived (oldest first). A passthrough on purpose: no sort, no
91
+ * reverse, no gap filling. The strip has no date axis, so a reversed array is
92
+ * visually plausible and would silently move an outage to the wrong end.
93
+ */
94
+ export declare function uptimeDays(service: Pick<ReliabilityServiceView, "uptime">): {
95
+ day: string;
96
+ state: ReliabilityViewState;
97
+ }[];
98
+ /** Compact mode shows at most this many needs-attention cards, `now` filling first. */
99
+ export declare const ATTENTION_DISPLAY_CAP = 3;
100
+ /** Compact mode shows this many table rows unless the caller passes `limit`. */
101
+ export declare const RECENT_DISPLAY_CAP = 5;
102
+ /** What the board shows and what a display cap hid. */
103
+ export interface BoardDisplay<T extends ReliabilityServiceView = ReliabilityServiceView> {
104
+ /** TRUE total of attention rows (the whole input, not the capped slice). */
105
+ attentionCount: number;
106
+ shownNow: T[];
107
+ shownRecent: T[];
108
+ hiddenAttentionCount: number;
109
+ /** Table rows in the server's order, every attention tier included. */
110
+ shownTable: T[];
111
+ hiddenTableCount: number;
112
+ }
113
+ /**
114
+ * Apply the display caps to the complete board. Compact: ≤ 3 attention cards
115
+ * (`now` first, `recent` tops up) and `limit ?? 5` table rows; otherwise
116
+ * uncapped. Hidden counts are computed from the full input, so they are real.
117
+ */
118
+ export declare function displayBoard<T extends ReliabilityServiceView>(services: readonly T[], opts?: {
119
+ compact?: boolean;
120
+ limit?: number;
121
+ }): BoardDisplay<T>;
@@ -38,6 +38,11 @@ export interface ResolveScope {
38
38
  output?: unknown;
39
39
  meta?: StepMeta;
40
40
  }>;
41
+ /** `nodes.<id>…` root — the SAME object as `steps` (one state, two roots). */
42
+ nodes?: Record<string, {
43
+ output?: unknown;
44
+ meta?: StepMeta;
45
+ }>;
41
46
  trigger: {
42
47
  event?: unknown;
43
48
  };
@@ -28,7 +28,10 @@ export interface StepExecution {
28
28
  * as the engine's own `RunState.steps`. */
29
29
  export interface RunState {
30
30
  status: RunStatus;
31
- steps: Record<string, StepExecution>;
31
+ /** Canonical per-node records (D2). */
32
+ nodes?: Record<string, StepExecution>;
33
+ /** Legacy alias of `nodes`; read as `nodes ?? steps` (un-upgraded server). */
34
+ steps?: Record<string, StepExecution>;
32
35
  }
33
36
  /**
34
37
  * A step node's rendered state — exactly the five {@link StepStatus} values
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Host-supplied per-step problems painted on the canvas. Pure — no React or
3
+ * @xyflow imports. The host owns detection; ui never infers a problem itself.
4
+ */
5
+ export interface StepProblem {
6
+ code: "unknown_connection";
7
+ connection: string;
8
+ }
9
+ /** Keyed by step id. */
10
+ export type StepProblems = Record<string, StepProblem>;
11
+ interface ProblemNode {
12
+ id: string;
13
+ data: {
14
+ stepProblem?: StepProblem;
15
+ };
16
+ }
17
+ /**
18
+ * Stamp `data.stepProblem` from `problems`, clearing it for steps no longer in
19
+ * the map. Returns the SAME node object when nothing changed.
20
+ */
21
+ export declare function applyStepProblems<N extends ProblemNode>(nodes: N[], problems: StepProblems | undefined): N[];
22
+ export declare function stepProblemTooltip(p: StepProblem): string;
23
+ /** True while the step still points at the connection the host reports missing. */
24
+ export declare function isConnectionBlocked(step: {
25
+ uses: {
26
+ connection?: string | null;
27
+ };
28
+ }, problem: StepProblem | undefined): boolean;
29
+ /** The Setup tab's Connection button label — exactly five outcomes. */
30
+ export declare function connectionButtonLabel(connection: string | undefined | null, conns: {
31
+ id: string;
32
+ displayName?: string;
33
+ }[] | null, failed: boolean): string;
34
+ export {};
package/dist/types.d.ts CHANGED
@@ -22,6 +22,11 @@ export interface AppSummary {
22
22
  maturity?: string;
23
23
  visibility?: string;
24
24
  successor?: string;
25
+ /** Model-provider designation; absent when the app does not declare one. */
26
+ modelProvider?: {
27
+ modelTypes: string[];
28
+ modelFamilies?: string[];
29
+ };
25
30
  /**
26
31
  * True when the calling tenant has zero-credential access flagged for this
27
32
  * app — usable with no connection step at all (server-computed per caller;
@@ -68,6 +73,8 @@ export interface AuthField {
68
73
  required?: boolean;
69
74
  default?: unknown;
70
75
  hint?: string;
76
+ /** Masks the input like a `secret` field whatever its `type` (e.g. a `json` key file). */
77
+ secret?: boolean;
71
78
  }
72
79
  /**
73
80
  * One declared parameter of an action, as returned by GET /apps/:id (the
@@ -119,7 +126,7 @@ export interface ActionParam {
119
126
  * Constrained choices. Renders as a single-select dropdown for `select` (or any
120
127
  * param with options), and as a multi-select pill picker for `multiselect`.
121
128
  */
122
- options?: ParamOption[];
129
+ options?: ParamOption[] | DynamicOptions;
123
130
  /** Type-specific render/behavior options. */
124
131
  config?: ParamConfig;
125
132
  /**
@@ -146,6 +153,34 @@ export interface ActionParam {
146
153
  export interface ParamOption {
147
154
  value: string | number;
148
155
  label: string;
156
+ description?: string;
157
+ }
158
+ /** Host-resolved options (mirrors @w6w/types `DynamicOptions`). */
159
+ export interface DynamicOptions {
160
+ source: string;
161
+ searchable?: boolean;
162
+ cache?: "session" | "none";
163
+ refreshOn?: string[];
164
+ }
165
+ /** True for the form-resolved arm: a non-array object with `source === "resolveFields"`. */
166
+ export declare function isFormResolvedOptions(o: unknown): o is DynamicOptions;
167
+ /** Body of `W6WApi.resolveActionFields`. */
168
+ export interface ResolveFieldsBody {
169
+ connectionId: string;
170
+ version?: string;
171
+ reason: "init" | "change";
172
+ form: Record<string, unknown>;
173
+ previous?: Record<string, unknown>;
174
+ changed?: string[];
175
+ fields: string[];
176
+ unresolved: string[];
177
+ }
178
+ /** Result of `W6WApi.resolveActionFields`. */
179
+ export interface ResolveFieldsResult {
180
+ fields: Record<string, {
181
+ options?: ParamOption[];
182
+ error?: string;
183
+ }>;
149
184
  }
150
185
  /**
151
186
  * The element schema for a `type: "array"` param. Either a scalar list
@@ -184,6 +219,8 @@ export interface ParamCondition {
184
219
  export interface ParamConfig {
185
220
  /** Render as a multi-line textarea. Implied by `type: "text"`. */
186
221
  multiline?: boolean;
222
+ /** With options: accept a value not in the list (Combobox `forceSelection` off). */
223
+ allowCustom?: boolean;
187
224
  }
188
225
  /** Summary of an action an app exposes, as returned by GET /apps/:id. */
189
226
  export interface ActionDef {
@@ -267,16 +304,83 @@ export interface WorkflowDetail {
267
304
  name: string;
268
305
  displayName?: string;
269
306
  description?: string;
270
- steps: WorkflowStepSummary[];
307
+ /** The canonical (manifest v2) step list. */
308
+ nodes?: WorkflowStepSummary[];
309
+ /** The legacy (v1) step list; a definition carries one or the other. */
310
+ steps?: WorkflowStepSummary[];
311
+ }
312
+ /** A `serviceAccount` auth's declaration: the credential contract it accepts and its binding. */
313
+ export interface ServiceAccountConfig {
314
+ /** Credential contract ids this auth accepts; the first one is used to list/create. */
315
+ accepts: string[];
316
+ /** Scopes the binding requests by default (shown read-only; the server applies them). */
317
+ scopes: string[];
318
+ /** Whether a connection must/may name an impersonated user (`delegatedUser`). */
319
+ impersonation: "none" | "optional" | "required";
320
+ /** Extra per-connection (binding) fields; each lands in `binding.settings`. */
321
+ bindingFields?: AuthField[];
322
+ }
323
+ export type CredentialStatus = "active" | "disabled" | "revoked";
324
+ /** A stored credential as the API lists it. Never carries the secret payload. */
325
+ export interface CredentialSummary {
326
+ id: string;
327
+ name: string;
328
+ contract: string;
329
+ status: CredentialStatus | string;
330
+ revision: number;
331
+ /** Non-secret display values (e.g. the service account's email). */
332
+ display: Record<string, string>;
333
+ private: boolean;
334
+ connectionCount: number;
335
+ createdAt: string;
336
+ updatedAt: string;
337
+ }
338
+ /** The connections that use a credential. */
339
+ export interface CredentialDependents {
340
+ connections: Array<{
341
+ id: string;
342
+ appId: string;
343
+ authKey: string;
344
+ displayName?: string | null;
345
+ }>;
271
346
  }
347
+ export interface CredentialTestResult {
348
+ ok: boolean;
349
+ code?: string;
350
+ message?: string;
351
+ }
352
+ /** Per-connection settings applied on top of a shared credential. */
353
+ export interface ConnectionBinding {
354
+ scopes?: string[];
355
+ /** The user the credential impersonates (Google domain-wide delegation). */
356
+ delegatedUser?: string;
357
+ settings?: Record<string, string | number | boolean>;
358
+ }
359
+ /** Body of `createConnection`: an inline credential XOR a reference to a stored one. */
360
+ export type CreateConnectionBody = {
361
+ authKey: string;
362
+ credential: Record<string, unknown>;
363
+ credentialId?: never;
364
+ displayName?: string;
365
+ profile?: Record<string, unknown>;
366
+ } | {
367
+ authKey: string;
368
+ credentialId: string;
369
+ credential?: never;
370
+ binding?: ConnectionBinding;
371
+ displayName?: string;
372
+ profile?: never;
373
+ };
272
374
  /** Auth method declaration as exposed by an app's manifest. */
273
375
  export interface AuthDef {
274
376
  key: string;
275
- type: "oauth2" | "apiKey" | "basic" | "bearer" | "custom" | "tenantAuth" | "jit" | string;
377
+ type: "oauth2" | "apiKey" | "basic" | "bearer" | "custom" | "tenantAuth" | "jit" | "serviceAccount" | string;
276
378
  displayName?: string;
277
379
  description?: string;
278
380
  connectionLabel?: string;
279
381
  fields?: AuthField[];
382
+ /** Present when `type === "serviceAccount"`: what credential it accepts and how it binds. */
383
+ serviceAccount?: ServiceAccountConfig;
280
384
  /** OAuth2 endpoints (only present when type === "oauth2"). */
281
385
  oauth2?: Record<string, unknown>;
282
386
  /**
@@ -314,6 +418,8 @@ export interface ConnectionSummary {
314
418
  id: string;
315
419
  appId: string;
316
420
  authKey: string;
421
+ /** The stored credential this connection uses, when it has one. */
422
+ credentialId?: string | null;
317
423
  /** The owning tenant. The server sends `tenant`/`subject`, never `owner`. */
318
424
  tenant?: string;
319
425
  /** The owning subject (the partner's user id). */
@@ -328,6 +434,14 @@ export interface ConnectionSummary {
328
434
  profile?: Record<string, unknown>;
329
435
  createdAt?: string;
330
436
  updatedAt?: string;
437
+ /**
438
+ * True for the server's synthetic "no credential needed" rows. They have no stored credential,
439
+ * so the server 404s get/test/delete on their ids; the connection UI offers nothing on them.
440
+ * Detect with this flag only, never an id prefix.
441
+ */
442
+ builtIn?: boolean;
443
+ /** The last test result: `false` = failed, `null`/absent = never tested. */
444
+ lastTestOk?: boolean | null;
331
445
  }
332
446
  /** A saved set of action-test input values, scoped to a connection + action. */
333
447
  export interface SavedTest {
@@ -0,0 +1,23 @@
1
+ import { DynamicContext } from './dynamic-fields.ts';
2
+ import { ActionParam, ParamOption } from './types.ts';
3
+ /** Per-field state the form renders from. */
4
+ export interface DynamicFieldState {
5
+ options?: ParamOption[];
6
+ loading: boolean;
7
+ error?: string;
8
+ /** Why the field is free text (no connection / unresolved refreshOn). */
9
+ hint?: string;
10
+ /** Flag for a kept value not in the returned list. */
11
+ notInList?: string;
12
+ }
13
+ export interface UseDynamicFields {
14
+ fields: Record<string, DynamicFieldState>;
15
+ /** A pick committed `value` for `key` (select/Combobox/checkbox): one change call if any field refreshes on it. */
16
+ commit(key: string, value: unknown): void;
17
+ /** Focus entered `key`: remember the pre-edit form (the `previous` of a later blur commit). */
18
+ focus(key: string): void;
19
+ /** Focus left `key`: commit a text edit made since focus (never per keystroke). */
20
+ blur(key: string): void;
21
+ retry(key: string): void;
22
+ }
23
+ export declare function useDynamicFields(params: ActionParam[], values: Record<string, unknown>, dynamic: DynamicContext | undefined, onChange: (next: Record<string, unknown>) => void): UseDynamicFields;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@w6w/ui",
3
- "version": "0.1.1",
3
+ "version": "0.2.11",
4
4
  "description": "React components for w6w — central API management. Build plugins, apps and integrations on them.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://w6w.io",
@@ -73,7 +73,8 @@
73
73
  "@uiw/react-codemirror": "^4.23.7",
74
74
  "@w6w/expr": "^0.1.0",
75
75
  "@xyflow/react": "^12.3.6",
76
- "prism-react-renderer": "^2.4.1"
76
+ "prism-react-renderer": "^2.4.1",
77
+ "react-markdown": "^10.1.0"
77
78
  },
78
79
  "devDependencies": {
79
80
  "@biomejs/biome": "^1.9.4",
@@ -98,8 +99,9 @@
98
99
  "build:css": "node scripts/build-css.mjs",
99
100
  "check:css": "node scripts/build-css.mjs --check",
100
101
  "typecheck": "tsc -b --noEmit",
101
- "test": "node --conditions=w6w-source --import ./src/test-jsx-loader.mjs --test src/__tests__/*.test.ts src/components/__tests__/*.test.ts",
102
+ "test": "node --max-old-space-size=2048 --conditions=w6w-source --import ./src/test-jsx-loader.mjs --test --test-concurrency=4 --test-timeout=120000 src/__tests__/*.test.ts src/components/__tests__/*.test.ts",
102
103
  "test:picker-layout": "bash test/picker-layout/run.sh",
104
+ "test:canvas-layer": "bash test/canvas-layer/run.sh",
103
105
  "test:expr-template": "bash test/expr-template/run.sh",
104
106
  "test:action-test-form": "bash test/action-test-form/run.sh",
105
107
  "test:host-isolation": "bash test/host-isolation/run.sh",
@@ -0,0 +1,46 @@
1
+ /* BudgetMeter — usage-vs-limit bar with a reserved segment.
2
+ *
3
+ * Token-only: every colour is a --w6w-* custom property, so a host restyles
4
+ * the meter (including the over-limit state) by overriding tokens, never by
5
+ * targeting a hex value here. */
6
+ .w6w-budget-meter {
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: var(--w6w-sp-1);
10
+ }
11
+ .w6w-budget-meter-head {
12
+ display: flex;
13
+ align-items: baseline;
14
+ justify-content: space-between;
15
+ gap: var(--w6w-sp-2);
16
+ color: var(--w6w-muted);
17
+ }
18
+ .w6w-budget-meter-label {
19
+ color: var(--w6w-text);
20
+ }
21
+ .w6w-budget-meter-value {
22
+ font-variant-numeric: tabular-nums;
23
+ }
24
+ .w6w-budget-meter-track {
25
+ display: flex;
26
+ width: 100%;
27
+ height: 8px;
28
+ overflow: hidden;
29
+ background: var(--w6w-panel-2);
30
+ border: 1px solid var(--w6w-border);
31
+ border-radius: var(--w6w-radius);
32
+ }
33
+ .w6w-budget-meter-used {
34
+ height: 100%;
35
+ background: var(--w6w-accent);
36
+ }
37
+ .w6w-budget-meter-reserved {
38
+ height: 100%;
39
+ background: var(--w6w-health-degraded);
40
+ }
41
+ .w6w-budget-meter-over .w6w-budget-meter-used {
42
+ background: var(--w6w-danger);
43
+ }
44
+ .w6w-budget-meter-over .w6w-budget-meter-value {
45
+ color: var(--w6w-danger);
46
+ }
@@ -0,0 +1,69 @@
1
+ .w6w-callable-list {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--w6w-sp-4);
5
+ }
6
+ .w6w-callable-list-head {
7
+ display: flex;
8
+ justify-content: space-between;
9
+ align-items: flex-start;
10
+ gap: var(--w6w-sp-4);
11
+ }
12
+ .w6w-callable-list-heading {
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: var(--w6w-sp-1);
16
+ min-width: 0;
17
+ }
18
+ .w6w-callable-list-title {
19
+ margin: 0;
20
+ }
21
+ .w6w-callable-list-description {
22
+ margin: 0;
23
+ }
24
+ .w6w-callable-list-toggle {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: var(--w6w-sp-2);
28
+ cursor: pointer;
29
+ }
30
+ .w6w-callable-list-actions {
31
+ display: flex;
32
+ gap: var(--w6w-sp-2);
33
+ flex-shrink: 0;
34
+ white-space: nowrap;
35
+ }
36
+ .w6w-callable-list-body {
37
+ border: 1px solid var(--w6w-border);
38
+ border-radius: 8px;
39
+ background: var(--w6w-panel);
40
+ overflow: hidden;
41
+ }
42
+ .w6w-callable-list-main {
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: var(--w6w-sp-0-5);
46
+ min-width: 0;
47
+ text-align: left;
48
+ }
49
+ .w6w-callable-list-open {
50
+ all: unset;
51
+ cursor: pointer;
52
+ display: block;
53
+ width: 100%;
54
+ }
55
+ .w6w-callable-list-trail {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ gap: var(--w6w-sp-2);
59
+ }
60
+ .w6w-callable-list-badge {
61
+ padding: var(--w6w-sp-0-5) var(--w6w-sp-2);
62
+ border-radius: 999px;
63
+ border: 1px solid var(--w6w-border);
64
+ color: var(--w6w-muted);
65
+ font-size: var(--w6w-fs-xs);
66
+ }
67
+ .w6w-callable-list-row-desc {
68
+ display: block;
69
+ }