rapidoc 11.0.1 → 12.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +171 -54
  2. package/dist/rapidoc-flow-min.js +580 -0
  3. package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
  4. package/dist/rapidoc-min.js +645 -202
  5. package/dist/rapidoc-min.js.LICENSE.txt +381 -0
  6. package/package.json +57 -42
  7. package/src/README.md +150 -0
  8. package/src/components/README.md +91 -0
  9. package/src/components/api-request.ts +912 -0
  10. package/src/components/api-response.ts +514 -0
  11. package/src/components/json-tree.ts +171 -0
  12. package/src/components/schema-base-element.ts +329 -0
  13. package/src/components/schema-table.ts +475 -0
  14. package/src/components/schema-tree.ts +393 -0
  15. package/src/components/tag-input.ts +180 -0
  16. package/src/flow.js +3 -0
  17. package/src/index.js +6 -0
  18. package/src/oauth-receiver.ts +29 -0
  19. package/src/rapi-flow.ts +1178 -0
  20. package/src/rapidoc-mini.ts +603 -0
  21. package/src/rapidoc.ts +1854 -0
  22. package/src/styles/README.md +96 -0
  23. package/src/styles/auth-styles.js +318 -0
  24. package/src/styles/border-styles.js +24 -0
  25. package/src/styles/css-bundle.js +129 -0
  26. package/src/styles/custom-styles.js +8 -0
  27. package/src/styles/dialog-box-styles.js +102 -0
  28. package/src/styles/endpoint-styles.js +564 -0
  29. package/src/styles/flex-styles.js +16 -0
  30. package/src/styles/flow-styles.js +248 -0
  31. package/src/styles/font-styles.js +432 -0
  32. package/src/styles/info-styles.js +36 -0
  33. package/src/styles/input-styles.js +555 -0
  34. package/src/styles/layout-styles.js +396 -0
  35. package/src/styles/microlighter-styles.js +99 -0
  36. package/src/styles/nav-styles.js +633 -0
  37. package/src/styles/param-list-styles.js +261 -0
  38. package/src/styles/schema-styles.js +296 -0
  39. package/src/styles/tab-styles.js +106 -0
  40. package/src/styles/theme-tokens.js +582 -0
  41. package/src/templates/README.md +89 -0
  42. package/src/templates/advanced-search-dialog-template.js +94 -0
  43. package/src/templates/api-response-template.js +223 -0
  44. package/src/templates/callback-template.js +86 -0
  45. package/src/templates/code-samples-template.js +52 -0
  46. package/src/templates/components-template.js +86 -0
  47. package/src/templates/downloads-template.js +70 -0
  48. package/src/templates/endpoint-template.js +377 -0
  49. package/src/templates/expanded-endpoint-template.js +270 -0
  50. package/src/templates/focused-endpoint-template.js +146 -0
  51. package/src/templates/header-template.js +92 -0
  52. package/src/templates/logo-template.js +21 -0
  53. package/src/templates/main-body-template.js +102 -0
  54. package/src/templates/navbar-template.js +498 -0
  55. package/src/templates/overview-template.js +104 -0
  56. package/src/templates/request-body-template.js +673 -0
  57. package/src/templates/request-params-template.js +485 -0
  58. package/src/templates/security-scheme-template.js +1272 -0
  59. package/src/templates/server-template.js +153 -0
  60. package/src/utils/README.md +146 -0
  61. package/src/utils/arazzo/README.md +119 -0
  62. package/src/utils/arazzo/components.js +15 -0
  63. package/src/utils/arazzo/graph.js +269 -0
  64. package/src/utils/arazzo/index.js +52 -0
  65. package/src/utils/arazzo/operations.js +106 -0
  66. package/src/utils/arazzo/parse.js +196 -0
  67. package/src/utils/arazzo/refs.js +40 -0
  68. package/src/utils/arazzo/svg.js +707 -0
  69. package/src/utils/auto-color-scheme.js +99 -0
  70. package/src/utils/bind-navbar.js +175 -0
  71. package/src/utils/color-utils.js +59 -0
  72. package/src/utils/common-utils.js +191 -0
  73. package/src/utils/curl-utils.js +126 -0
  74. package/src/utils/highlighter.js +419 -0
  75. package/src/utils/markdown-utils.js +110 -0
  76. package/src/utils/mcp/README.md +155 -0
  77. package/src/utils/mcp/common.js +166 -0
  78. package/src/utils/mcp/manifest.js +382 -0
  79. package/src/utils/mcp/project/server.ts +53 -0
  80. package/src/utils/mcp/project.js +88 -0
  81. package/src/utils/mcp/runtime.js +289 -0
  82. package/src/utils/mcp/schema.js +98 -0
  83. package/src/utils/mcp/workflow-runtime.js +374 -0
  84. package/src/utils/mcp/workflows.js +240 -0
  85. package/src/utils/mcp/zip.js +102 -0
  86. package/src/utils/mcp-generator.js +47 -0
  87. package/src/utils/mock-interceptor.js +557 -0
  88. package/src/utils/oauth-utils.js +87 -0
  89. package/src/utils/request-utils.js +264 -0
  90. package/src/utils/response-utils.js +203 -0
  91. package/src/utils/sanitize.js +57 -0
  92. package/src/utils/schema-utils.js +1954 -0
  93. package/src/utils/slugger.js +187 -0
  94. package/src/utils/spec-parser.js +941 -0
  95. package/src/utils/url-utils.js +246 -0
  96. package/src/utils/xml-utils.js +61 -0
@@ -0,0 +1,1178 @@
1
+ import { css, html, LitElement, nothing } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+ import { unsafeSVG } from "lit/directives/unsafe-svg.js";
4
+
5
+ import ThemeTokens, {
6
+ DEFAULT_THEME,
7
+ applyCustomTheme,
8
+ normalizeTheme,
9
+ } from "~/styles/theme-tokens";
10
+ import FlowStyles from "~/styles/flow-styles";
11
+ import { AutoColorSchemeController } from "~/utils/auto-color-scheme";
12
+ import workflowRuntimeSource from "~/utils/mcp/workflow-runtime.js?raw";
13
+ import {
14
+ parseDocument,
15
+ validateArazzo,
16
+ indexOperations,
17
+ indexResolvedSpec,
18
+ buildFlowGraph,
19
+ renderFlowDiagram,
20
+ escapeXml,
21
+ } from "~/utils/arazzo/index.js";
22
+ import { buildMcpManifest } from "~/utils/mcp/manifest.js";
23
+ import { createMcpWorkflowRuntime } from "~/utils/mcp/workflow-runtime.js";
24
+
25
+ let instanceCount = 0;
26
+
27
+ /**
28
+ * <rapi-flow>: draws the workflows of an Arazzo document. Display only; it calls no API.
29
+ */
30
+ @customElement("rapi-flow")
31
+ export default class RapiFlow extends LitElement {
32
+ @property({ type: String, attribute: "arazzo-url" }) arazzoUrl = "";
33
+ // Replaces the URL of the first OpenAPI source description
34
+ @property({ type: String, attribute: "spec-url" }) specUrl = "";
35
+ @property({ type: String, attribute: "workflow-id" }) workflowId = "";
36
+ // id of a <rapi-doc> on the same page; clicking a step scrolls it to the operation
37
+ @property({ type: String, attribute: "for" }) for = "";
38
+ @property({ type: String, reflect: true }) theme = DEFAULT_THEME;
39
+ @property({ type: String, attribute: "color-scheme", reflect: true }) colorScheme = "dark";
40
+ // "expanded" shows each step's inputs and actions in its box; "compact" only the operation
41
+ @property({ type: String, reflect: true }) view = "expanded";
42
+ // "false" hides the Compact / Expanded switch
43
+ @property({ type: String, attribute: "show-view-toggle" }) showViewToggle = "true";
44
+ // "false" hides the Run button
45
+ @property({ type: String, attribute: "allow-run" }) allowRun = "true";
46
+ // Base URL the run sends requests to, instead of the server a tied <rapi-doc> selected or the spec's
47
+ @property({ type: String, attribute: "server-url" }) serverUrl = "";
48
+
49
+ @state() private doc: any = null;
50
+ @state() private sources: Map<string, any> = new Map();
51
+ @state() private problems: any[] = [];
52
+ @state() private loading = false;
53
+ @state() private loadError = "";
54
+ @state() private selectedStepId = "";
55
+ // The OpenAPI documents this element loaded itself, by source name, for runs without a <rapi-doc>
56
+ private specObjects: Map<string, any> = new Map();
57
+ // Running a workflow: the inputs form, the step whose request is in flight, and a log
58
+ @state() private runState: "idle" | "running" | "done" = "idle";
59
+ @state() private runInputs: Record<string, string> = {};
60
+ /**
61
+ * Status codes a mock answers with during a run, by step id, `*` for every other step; each a
62
+ * list such as "503, 200". A tied <rapi-doc>'s in-browser mock gets them per operation; any other
63
+ * server gets `Prefer: code=<status>` on the request, which `rapidoc mock` honours.
64
+ */
65
+ @property({ type: Object, attribute: false }) statusCodes: Record<string, string> = {};
66
+ @state() private mockStrategy = "cycle";
67
+ // Credentials typed into the run panel, by security scheme id, when no <rapi-doc> supplies them
68
+ @state() private manualCredentials: Record<string, string> = {};
69
+ @state() private runningStepId = "";
70
+ @state() private trace: any[] = [];
71
+ @state() private runResult: any = null;
72
+ @state() private runProblem = "";
73
+
74
+ private loadId = 0;
75
+ // The <rapi-doc> named by `for` while it supplies the first OpenAPI source (no spec-url)
76
+ private specDoc: any = null;
77
+ private onSpecLoaded = () => this.useRapiDocSpec();
78
+ // Step whose box, lines and output rows are highlighted while the pointer is over one of them
79
+ private linkedStepId = "";
80
+ private idPrefix = `rf${++instanceCount}`;
81
+ _autoColorScheme = new AutoColorSchemeController(this);
82
+
83
+ static styles = [
84
+ ThemeTokens,
85
+ FlowStyles,
86
+ css`
87
+ /* The element paints its own background, so it keeps its content off the edges; a page
88
+ can set rapi-flow { padding: 0 } */
89
+ :host {
90
+ display: block;
91
+ padding: 1rem 1.25rem;
92
+ color: var(--foreground);
93
+ background: var(--background);
94
+ font-family: var(--font-sans);
95
+ font-size: var(--font-size-regular);
96
+ line-height: var(--line-height);
97
+ }
98
+ .flow-header {
99
+ display: flex;
100
+ flex-wrap: wrap;
101
+ align-items: baseline;
102
+ gap: 0.5rem 1rem;
103
+ margin-bottom: 0.25rem;
104
+ }
105
+ .flow-title {
106
+ margin: 0;
107
+ font-size: var(--h4-font-size);
108
+ font-weight: var(--heading-weight);
109
+ color: var(--heading-color);
110
+ }
111
+ .flow-id {
112
+ font-family: var(--font-mono);
113
+ font-size: var(--font-size-small);
114
+ color: var(--muted-foreground);
115
+ }
116
+ .flow-controls {
117
+ display: flex;
118
+ flex-wrap: wrap;
119
+ align-items: center;
120
+ gap: 0.5rem;
121
+ margin-left: auto;
122
+ }
123
+ .view-toggle {
124
+ display: inline-flex;
125
+ border: 1px solid var(--input);
126
+ border-radius: var(--radius);
127
+ overflow: hidden;
128
+ }
129
+ .view-toggle button {
130
+ font: inherit;
131
+ font-size: var(--font-size-small);
132
+ color: var(--muted-foreground);
133
+ background: var(--input-background);
134
+ border: 0;
135
+ padding: 0.25rem 0.625rem;
136
+ cursor: pointer;
137
+ }
138
+ .view-toggle button + button {
139
+ border-left: 1px solid var(--input);
140
+ }
141
+ .view-toggle button[aria-pressed="true"] {
142
+ color: var(--primary-foreground);
143
+ background: var(--primary);
144
+ }
145
+ .view-toggle button:focus-visible {
146
+ outline: 2px solid var(--ring);
147
+ outline-offset: -2px;
148
+ }
149
+ select {
150
+ font: inherit;
151
+ font-size: var(--font-size-small);
152
+ color: var(--foreground);
153
+ background: var(--input-background);
154
+ border: 1px solid var(--input);
155
+ border-radius: var(--radius);
156
+ padding: 0.25rem 0.5rem;
157
+ }
158
+ .flow-description {
159
+ margin: 0 0 0.75rem;
160
+ color: var(--muted-foreground);
161
+ font-size: var(--font-size-description);
162
+ }
163
+ .flow-diagram {
164
+ overflow-x: auto;
165
+ }
166
+ .flow-message {
167
+ color: var(--muted-foreground);
168
+ font-size: var(--font-size-description);
169
+ }
170
+ .flow-problems {
171
+ margin: 0.75rem 0 0;
172
+ padding-left: 1.25rem;
173
+ font-size: var(--font-size-small);
174
+ color: var(--destructive);
175
+ }
176
+ .run-button {
177
+ font: inherit;
178
+ font-size: var(--font-size-small);
179
+ color: var(--primary-foreground);
180
+ background: var(--primary);
181
+ border: 1px solid var(--primary);
182
+ border-radius: var(--radius);
183
+ padding: 0.25rem 0.75rem;
184
+ cursor: pointer;
185
+ }
186
+ .run-button:disabled {
187
+ opacity: 0.6;
188
+ cursor: default;
189
+ }
190
+ .run-clear {
191
+ font: inherit;
192
+ font-size: var(--font-size-small);
193
+ color: var(--muted-foreground);
194
+ background: var(--input-background);
195
+ border: 1px solid var(--input);
196
+ border-radius: var(--radius);
197
+ padding: 0.25rem 0.75rem;
198
+ cursor: pointer;
199
+ }
200
+ .flow-run {
201
+ margin-top: 1rem;
202
+ font-size: var(--font-size-description);
203
+ }
204
+ .flow-run h3 {
205
+ margin: 1rem 0 0.5rem;
206
+ font-size: var(--h5-font-size);
207
+ font-weight: var(--heading-weight);
208
+ color: var(--heading-color);
209
+ }
210
+ .flow-run h4 {
211
+ margin: 0.5rem 0 0.25rem;
212
+ font-size: var(--font-size-small);
213
+ font-weight: var(--heading-weight);
214
+ color: var(--muted-foreground);
215
+ }
216
+ .flow-run pre {
217
+ margin: 0;
218
+ padding: 0.5rem;
219
+ overflow-x: auto;
220
+ background: var(--muted);
221
+ border-radius: var(--radius);
222
+ font-family: var(--font-mono);
223
+ font-size: var(--font-size-small);
224
+ white-space: pre-wrap;
225
+ word-break: break-word;
226
+ }
227
+ .flow-run .muted {
228
+ color: var(--muted-foreground);
229
+ }
230
+ .run-inputs {
231
+ container-type: inline-size;
232
+ margin: 0 0 0.75rem;
233
+ padding: 0.5rem 0.75rem;
234
+ border: 1px solid var(--border);
235
+ border-radius: var(--radius);
236
+ background: var(--card);
237
+ }
238
+ .run-inputs h3 {
239
+ margin: 0.25rem 0 0.5rem;
240
+ font-size: var(--font-size-small);
241
+ font-weight: var(--heading-weight);
242
+ color: var(--muted-foreground);
243
+ }
244
+ .run-inputs p {
245
+ margin: 0.25rem 0 0.5rem;
246
+ }
247
+ /*
248
+ * One field per row, laid out like <rapi-doc>'s parameter rows: a right-aligned two-line label
249
+ * (name over type) and a control as tall as the label: 1.25em + 2 x 0.75em + 2px border.
250
+ */
251
+ .run-field {
252
+ display: grid;
253
+ grid-template-columns: minmax(8rem, 14rem) minmax(0, 1fr);
254
+ column-gap: 1rem;
255
+ align-items: center;
256
+ margin: 0.5rem 0;
257
+ font-size: var(--font-size-small);
258
+ }
259
+ .run-field-label {
260
+ display: flex;
261
+ flex-direction: column;
262
+ min-width: 0;
263
+ text-align: right;
264
+ }
265
+ .run-field-name,
266
+ .run-field-type {
267
+ margin: 1px 0;
268
+ line-height: 1.35;
269
+ overflow-wrap: anywhere;
270
+ }
271
+ .run-field-name {
272
+ color: var(--foreground);
273
+ font-family: var(--font-mono);
274
+ }
275
+ .run-field-type {
276
+ color: var(--muted-foreground);
277
+ font-family: var(--font-sans);
278
+ }
279
+ .run-field-required {
280
+ color: var(--destructive);
281
+ }
282
+ .run-inputs .run-field :is(input, select) {
283
+ font-family: var(--font-mono);
284
+ line-height: 1.25;
285
+ padding: 0.75em;
286
+ }
287
+ @container (max-width: 28rem) {
288
+ .run-field {
289
+ grid-template-columns: minmax(0, 1fr);
290
+ row-gap: 0.25rem;
291
+ }
292
+ .run-field-label {
293
+ flex-direction: row;
294
+ flex-wrap: wrap;
295
+ gap: 0.5rem;
296
+ text-align: start;
297
+ }
298
+ }
299
+ .run-inputs input,
300
+ .run-inputs select {
301
+ font: inherit;
302
+ font-size: var(--font-size-small);
303
+ color: var(--foreground);
304
+ background: var(--input-background);
305
+ border: 1px solid var(--input);
306
+ border-radius: var(--radius);
307
+ padding: 0.25rem 0.5rem;
308
+ min-width: 0;
309
+ }
310
+ .run-mock {
311
+ margin-top: 0.75rem;
312
+ }
313
+ .run-mock summary {
314
+ cursor: pointer;
315
+ font-weight: 600;
316
+ }
317
+ .run-mock summary .muted {
318
+ font-weight: 400;
319
+ margin-left: 0.5rem;
320
+ }
321
+ .run-mock-rows {
322
+ margin-top: 0.25rem;
323
+ }
324
+ .run-inputs .run-mock-strategy {
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 0.5rem;
328
+ margin-top: 0.5rem;
329
+ }
330
+ .run-mock-strategy select {
331
+ flex: 0 0 auto;
332
+ width: auto;
333
+ }
334
+ .run-actions {
335
+ display: flex;
336
+ gap: 0.5rem;
337
+ margin-top: 0.75rem;
338
+ }
339
+ .run-trace {
340
+ margin: 0;
341
+ padding: 0;
342
+ list-style: none;
343
+ }
344
+ .run-trace li {
345
+ border: 1px solid var(--border);
346
+ border-radius: var(--radius);
347
+ margin-bottom: 0.375rem;
348
+ background: var(--card);
349
+ }
350
+ .run-trace li.trace-failure {
351
+ border-color: var(--destructive);
352
+ }
353
+ .run-trace summary {
354
+ display: flex;
355
+ flex-wrap: wrap;
356
+ gap: 0.25rem 0.75rem;
357
+ align-items: baseline;
358
+ padding: 0.375rem 0.75rem;
359
+ cursor: pointer;
360
+ }
361
+ .run-trace details[open] summary {
362
+ border-bottom: 1px solid var(--border);
363
+ }
364
+ .run-trace details > :not(summary) {
365
+ margin-left: 0.75rem;
366
+ margin-right: 0.75rem;
367
+ }
368
+ .run-trace details > pre:last-child {
369
+ margin-bottom: 0.75rem;
370
+ }
371
+ .trace-step {
372
+ font-weight: 600;
373
+ }
374
+ .trace-request {
375
+ font-family: var(--font-mono);
376
+ font-size: var(--font-size-small);
377
+ color: var(--muted-foreground);
378
+ word-break: break-all;
379
+ }
380
+ .trace-success .trace-outcome {
381
+ color: var(--green);
382
+ }
383
+ .trace-failure .trace-outcome,
384
+ .run-problem,
385
+ .run-error {
386
+ color: var(--destructive);
387
+ }
388
+ .flow-problems .warning {
389
+ color: var(--muted-foreground);
390
+ }
391
+ code,
392
+ pre {
393
+ font-family: var(--font-mono);
394
+ font-size: var(--font-size-small);
395
+ }
396
+ .flow-legend {
397
+ display: flex;
398
+ flex-wrap: wrap;
399
+ gap: 0.25rem 1rem;
400
+ margin: 0.75rem 0 0;
401
+ padding: 0;
402
+ list-style: none;
403
+ font-size: var(--font-size-xs);
404
+ color: var(--muted-foreground);
405
+ }
406
+ .flow-legend li {
407
+ display: flex;
408
+ align-items: center;
409
+ gap: 0.375rem;
410
+ }
411
+ .flow-legend svg {
412
+ overflow: visible;
413
+ }
414
+ `,
415
+ ];
416
+
417
+ connectedCallback() {
418
+ super.connectedCallback();
419
+ this.theme = normalizeTheme(this.theme).theme;
420
+ if (!this.getAttribute("theme")) this.setAttribute("theme", this.theme);
421
+ if (!"dark, light, system, auto,".includes(`${this.colorScheme},`)) this.colorScheme = "dark";
422
+ if (!this.getAttribute("color-scheme")) this.setAttribute("color-scheme", this.colorScheme);
423
+ applyCustomTheme(this, this.theme);
424
+ // Reconnected after a load: listen to the <rapi-doc> again
425
+ if (this.doc) {
426
+ this.watchRapiDoc();
427
+ this.useRapiDocSpec();
428
+ }
429
+ }
430
+
431
+ disconnectedCallback() {
432
+ super.disconnectedCallback();
433
+ this.specDoc?.removeEventListener("spec-loaded", this.onSpecLoaded);
434
+ this.specDoc = null;
435
+ }
436
+
437
+ updated(changed) {
438
+ if (changed.has("theme")) {
439
+ const normalized = normalizeTheme(this.theme).theme;
440
+ if (normalized !== this.theme) this.theme = normalized;
441
+ applyCustomTheme(this, this.theme);
442
+ }
443
+ if (changed.has("arazzoUrl") || changed.has("specUrl") || changed.has("for")) this.load();
444
+ // A redrawn diagram has lost the highlight classes
445
+ this.linkedStepId = "";
446
+ }
447
+
448
+ /** Loads the Arazzo document and its OpenAPI sources. Fires `flow-loaded` when done. */
449
+ async load() {
450
+ const id = ++this.loadId;
451
+ if (!this.arazzoUrl) {
452
+ this.doc = null;
453
+ return;
454
+ }
455
+ this.loading = true;
456
+ this.loadError = "";
457
+ try {
458
+ const arazzoUrl = new URL(this.arazzoUrl, document.baseURI).href;
459
+ const doc = parseDocument(await this.fetchText(arazzoUrl));
460
+ // With `for` and no spec-url, the first OpenAPI source is the spec <rapi-doc> loads
461
+ this.watchRapiDoc();
462
+ const problems = validateArazzo(doc);
463
+ const sources = new Map();
464
+ const specObjects = new Map();
465
+ const openapiSources = (doc.sourceDescriptions || []).filter(
466
+ (s) => s?.name && s?.url && (!s.type || s.type === "openapi"),
467
+ );
468
+ await Promise.all(
469
+ openapiSources.map(async (s, i) => {
470
+ if (i === 0 && this.specDoc) return;
471
+ const url = i === 0 && this.specUrl ? this.specUrl : s.url;
472
+ try {
473
+ const text = await this.fetchText(
474
+ new URL(url, i === 0 && this.specUrl ? document.baseURI : arazzoUrl).href,
475
+ );
476
+ const openapi = parseDocument(text);
477
+ sources.set(s.name, indexOperations(openapi));
478
+ specObjects.set(s.name, openapi);
479
+ } catch (err) {
480
+ problems.push({
481
+ level: "warning",
482
+ path: `sourceDescriptions.${s.name}`,
483
+ message: `could not load ${url}: ${err.message}`,
484
+ });
485
+ }
486
+ }),
487
+ );
488
+ if (id !== this.loadId) return;
489
+ this.doc = doc;
490
+ this.sources = sources;
491
+ this.specObjects = specObjects;
492
+ this.problems = problems;
493
+ this.resetRun();
494
+ this.useRapiDocSpec();
495
+ this.dispatchEvent(
496
+ new CustomEvent("flow-loaded", {
497
+ detail: { doc, problems },
498
+ bubbles: true,
499
+ composed: true,
500
+ }),
501
+ );
502
+ } catch (err) {
503
+ if (id !== this.loadId) return;
504
+ this.doc = null;
505
+ this.loadError = err.message;
506
+ } finally {
507
+ if (id === this.loadId) this.loading = false;
508
+ }
509
+ }
510
+
511
+ private async fetchText(url: string) {
512
+ const resp = await fetch(url);
513
+ if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
514
+ return resp.text();
515
+ }
516
+
517
+ /** The parsed Arazzo document, for a <rapi-doc> that exports workflow tools; null until loaded. */
518
+ get arazzoDocument(): any {
519
+ return this.doc;
520
+ }
521
+
522
+ /** The text of workflow-runtime.js, for the project a tied <rapi-doc> exports. */
523
+ get workflowRuntimeSource(): string {
524
+ return workflowRuntimeSource;
525
+ }
526
+
527
+ private get workflowIds(): string[] {
528
+ return (this.doc?.workflows || []).map((w) => w?.workflowId).filter(Boolean);
529
+ }
530
+
531
+ private get graph() {
532
+ if (!this.doc) return null;
533
+ const ids = this.workflowIds;
534
+ const wanted = ids.includes(this.workflowId) ? this.workflowId : ids[0];
535
+ return buildFlowGraph(this.doc, wanted, { sources: this.sources });
536
+ }
537
+
538
+ /** Follows the <rapi-doc> named by `for`, unless spec-url names the spec to load instead. */
539
+ private watchRapiDoc() {
540
+ const rapiDoc = this.specUrl ? null : this.findRapiDoc();
541
+ if (rapiDoc === this.specDoc) return;
542
+ this.specDoc?.removeEventListener("spec-loaded", this.onSpecLoaded);
543
+ this.specDoc = rapiDoc;
544
+ rapiDoc?.addEventListener("spec-loaded", this.onSpecLoaded);
545
+ }
546
+
547
+ /**
548
+ * Indexes the spec the <rapi-doc> has processed as the first OpenAPI source. Runs after a load
549
+ * and on each `spec-loaded`, since <rapi-doc> often finishes after <rapi-flow>. Until then,
550
+ * steps of that source show their operationId.
551
+ */
552
+ private useRapiDocSpec() {
553
+ const first = (this.doc?.sourceDescriptions || []).find(
554
+ (s) => s?.name && s?.url && (!s.type || s.type === "openapi"),
555
+ );
556
+ const spec = this.specDoc?.resolvedSpec;
557
+ if (!first || !spec) return;
558
+ this.sources = new Map(this.sources).set(first.name, indexResolvedSpec(spec));
559
+ }
560
+
561
+ private findRapiDoc(): any {
562
+ if (!this.for) return null;
563
+ const root = this.getRootNode() as Document | ShadowRoot;
564
+ return root.getElementById?.(this.for) || document.getElementById(this.for);
565
+ }
566
+
567
+ private selectStep(stepId: string) {
568
+ const graph = this.graph;
569
+ const step = graph?.steps.find((s) => s.stepId === stepId);
570
+ if (!step) return;
571
+ this.selectedStepId = stepId;
572
+ const op = step.operation;
573
+ this.dispatchEvent(
574
+ new CustomEvent("step-click", {
575
+ detail: {
576
+ workflowId: graph.workflowId,
577
+ stepId,
578
+ operation: op,
579
+ workflowRef: step.workflowId,
580
+ },
581
+ bubbles: true,
582
+ composed: true,
583
+ }),
584
+ );
585
+ if (step.kind === "workflow" && this.workflowIds.includes(step.workflowId)) {
586
+ this.workflowId = step.workflowId;
587
+ return;
588
+ }
589
+ const rapiDoc = this.findRapiDoc();
590
+ if (!op || !rapiDoc?.resolvedSpec) return;
591
+ const target = (rapiDoc.resolvedSpec.tags || [])
592
+ .flatMap((t) => t.paths || [])
593
+ .find(
594
+ (p) =>
595
+ (op.operationId && p.operationId === op.operationId) ||
596
+ (p.method === op.method && p.path === op.path),
597
+ );
598
+ if (target?.elementId) rapiDoc.scrollToPath(target.elementId);
599
+ }
600
+
601
+ private onDiagramEvent(e: Event) {
602
+ if (e instanceof KeyboardEvent && e.key !== "Enter" && e.key !== " ") return;
603
+ const el = (e.target as Element)?.closest?.(".rf-jump, .rf-clickable");
604
+ if (!el) return;
605
+ e.preventDefault();
606
+ // A step name in the outputs box or a value line: bring that step into view, then select it
607
+ if (el.classList.contains("rf-jump")) {
608
+ const stepId = el.getAttribute("data-from-step") || "";
609
+ this.selectStep(stepId);
610
+ // Selecting redraws the diagram, so scroll to the new step box once it is drawn
611
+ this.updateComplete.then(() => {
612
+ const sel = `.rf-step[data-step-id="${CSS.escape(stepId)}"]`;
613
+ const box = this.renderRoot.querySelector(sel);
614
+ if (box) this.scrollToCenter(box.getBoundingClientRect());
615
+ });
616
+ return;
617
+ }
618
+ this.selectStep(el.getAttribute("data-step-id") || "");
619
+ }
620
+
621
+ /**
622
+ * Highlights the step under the pointer together with its value lines and its rows in the
623
+ * outputs box (or, from an output row, the step it comes from). The diagram is an SVG string,
624
+ * so the classes are set on its elements directly.
625
+ */
626
+ private onDiagramHover(e: Event) {
627
+ const el =
628
+ e.type === "mouseleave"
629
+ ? null
630
+ : (e.target as Element)?.closest?.("[data-step-id], [data-from-step]");
631
+ const id = el?.getAttribute("data-step-id") || el?.getAttribute("data-from-step") || "";
632
+ if (id === this.linkedStepId) return;
633
+ this.linkedStepId = id;
634
+ const svg = this.renderRoot.querySelector(".flow-diagram svg");
635
+ if (!svg) return;
636
+ svg.querySelectorAll(".rf-linked").forEach((n) => n.classList.remove("rf-linked"));
637
+ if (!id) return;
638
+ const sel = CSS.escape(id);
639
+ svg
640
+ .querySelectorAll(`[data-step-id="${sel}"], [data-from-step="${sel}"]`)
641
+ .forEach((n) => n.classList.add("rf-linked"));
642
+ }
643
+
644
+ /**
645
+ * Scrolls the nearest scrolling ancestor (or the window) so a rect is centered. Used instead of
646
+ * scrollIntoView, which does not move SVG groups in every browser.
647
+ */
648
+ private scrollToCenter(rect: DOMRect) {
649
+ // Walk up through shadow roots too, in case <rapi-flow> sits inside another component
650
+ const up = (node: Element) =>
651
+ node.parentElement || ((node.getRootNode() as ShadowRoot).host as Element) || null;
652
+ let el = up(this);
653
+ while (el) {
654
+ const { overflowY } = getComputedStyle(el);
655
+ if (/(auto|scroll)/.test(overflowY) && el.scrollHeight > el.clientHeight) break;
656
+ el = up(el);
657
+ }
658
+ const view = el ? el.getBoundingClientRect() : { top: 0, height: window.innerHeight };
659
+ const top = rect.top - view.top - (view.height - Math.min(rect.height, view.height)) / 2;
660
+ (el || window).scrollBy({ top, behavior: "smooth" });
661
+ }
662
+
663
+ private setView(view: string) {
664
+ if (view === this.view) return;
665
+ this.view = view;
666
+ this.dispatchEvent(
667
+ new CustomEvent("view-change", { detail: { view }, bubbles: true, composed: true }),
668
+ );
669
+ }
670
+
671
+ /** Back to the form, with the shown workflow's input defaults filled in. */
672
+ private resetRun() {
673
+ this.runState = "idle";
674
+ this.runningStepId = "";
675
+ this.trace = [];
676
+ this.runResult = null;
677
+ this.runProblem = "";
678
+ const defaults = {};
679
+ for (const i of this.graph?.inputDetails || []) {
680
+ if (i.default !== undefined) {
681
+ defaults[i.name] = typeof i.default === "string" ? i.default : JSON.stringify(i.default);
682
+ }
683
+ }
684
+ this.runInputs = defaults;
685
+ }
686
+
687
+ /** The credentials a tied <rapi-doc> holds, by security scheme id, as the runtime takes them. */
688
+ private credentialsFromRapiDoc(rapiDoc) {
689
+ const credentials = {};
690
+ for (const s of rapiDoc?.resolvedSpec?.securitySchemes || []) {
691
+ const value = s?.finalKeyValue;
692
+ if (!value || !s.securitySchemeId) continue;
693
+ if (s.type === "apiKey") credentials[s.securitySchemeId] = value;
694
+ else if (/^basic /i.test(value)) credentials[s.securitySchemeId] = atob(value.slice(6));
695
+ else credentials[s.securitySchemeId] = value.replace(/^(bearer|mac)\s+/i, "");
696
+ }
697
+ return credentials;
698
+ }
699
+
700
+ /**
701
+ * The runtime and tool that run the shown workflow, or why it cannot run. The OpenAPI document
702
+ * is the tied <rapi-doc>'s, else the first source this element loaded; requests go to
703
+ * `server-url`, else the server the <rapi-doc> selected, else the spec's; credentials are the
704
+ * <rapi-doc>'s.
705
+ */
706
+ private prepareRun(graph) {
707
+ const rapiDoc = this.findRapiDoc();
708
+ const first = (this.doc.sourceDescriptions || []).find(
709
+ (s) => s?.name && (!s.type || s.type === "openapi"),
710
+ );
711
+ const spec = this.specDoc?.resolvedSpec || this.specObjects.get(first?.name);
712
+ if (!spec) return { problem: "the OpenAPI document of the first source is not loaded" };
713
+ const problems = [];
714
+ const manifest = buildMcpManifest(spec, {
715
+ arazzo: this.doc,
716
+ sources: Object.fromEntries(this.specObjects),
717
+ onProblem: (p) => problems.push(p),
718
+ });
719
+ const tool = manifest.tools.find((t) => t.workflow === graph.workflowId);
720
+ if (!tool) {
721
+ const why = problems.find((p) => p.message.includes(`workflow ${graph.workflowId} `));
722
+ return {
723
+ problem: why
724
+ ? why.message.replace(/^workflow \S+ is left out of the MCP export: /, "")
725
+ : "this workflow cannot be run",
726
+ };
727
+ }
728
+ const baseUrl = this.serverUrl || rapiDoc?.selectedServer?.computedUrl;
729
+ const runtime = createMcpWorkflowRuntime(manifest, {
730
+ credentials: { ...this.typedCredentials(), ...this.credentialsFromRapiDoc(rapiDoc) },
731
+ baseUrls: baseUrl ? { [first.name]: baseUrl } : {},
732
+ });
733
+ return { runtime, tool };
734
+ }
735
+
736
+ /** The form's values as the workflow's inputs, by each input's type. */
737
+ private coerceInputs(graph) {
738
+ const inputs = {};
739
+ for (const i of graph.inputDetails) {
740
+ const raw = this.runInputs[i.name];
741
+ if (raw === undefined || raw === "") continue;
742
+ const type = String(i.type || "string");
743
+ let value: any = raw;
744
+ if (/^(integer|number)/.test(type)) value = Number(raw);
745
+ else if (type.startsWith("boolean")) value = raw === "true";
746
+ else if (/^(object|array)/.test(type)) {
747
+ try {
748
+ value = JSON.parse(raw);
749
+ } catch {
750
+ value = raw;
751
+ }
752
+ }
753
+ inputs[i.name] = value;
754
+ }
755
+ return inputs;
756
+ }
757
+
758
+ /** Runs the shown workflow in the browser, logging each step, then fires `workflow-run`. */
759
+ private async executeRun(graph) {
760
+ const prepared = this.prepareRun(graph);
761
+ this.runningStepId = "";
762
+ this.trace = [];
763
+ this.runResult = null;
764
+ this.runProblem = "";
765
+ if (!prepared.runtime) {
766
+ this.runProblem = prepared.problem;
767
+ this.runState = "done";
768
+ return;
769
+ }
770
+ const inputs = this.coerceInputs(graph);
771
+ this.runState = "running";
772
+ // Status codes for this run: the in-browser mock reads them from the <rapi-doc> (set for the
773
+ // run only), any other server gets them as Prefer: code=
774
+ const rapiDoc = this.findRapiDoc();
775
+ const mocked = rapiDoc?.mockServer === "true";
776
+ const codes = this.statusCodesByStep(graph);
777
+ const anyList = Object.values(codes).some((c) => c.includes(","));
778
+ const previous = mocked &&
779
+ Object.keys(codes).length && {
780
+ code: rapiDoc.mockServerStatusCode,
781
+ strategy: rapiDoc.mockServerStatusStrategy,
782
+ };
783
+ if (previous) {
784
+ const byOperation = {};
785
+ for (const step of graph.steps) {
786
+ const op = step.operation;
787
+ if (op && codes[step.stepId])
788
+ byOperation[`${op.method.toUpperCase()} ${op.path}`] = codes[step.stepId];
789
+ }
790
+ if (codes["*"]) byOperation["*"] = codes["*"];
791
+ rapiDoc.mockServerStatusCode = byOperation;
792
+ rapiDoc.mockServerStatusStrategy = anyList ? this.mockStrategy : "first";
793
+ await rapiDoc.updateComplete;
794
+ }
795
+ const pick = (list: string, attempt: number) => {
796
+ const items = list
797
+ .split(",")
798
+ .map((c) => c.trim())
799
+ .filter(Boolean);
800
+ if (!items.length) return "";
801
+ if (this.mockStrategy === "random" && items.length > 1)
802
+ return items[Math.floor(Math.random() * items.length)];
803
+ return items[(attempt - 1) % items.length];
804
+ };
805
+ const result = await prepared.runtime.callTool(prepared.tool.name, inputs, {
806
+ onStepStart: (e) => (this.runningStepId = e.stepId),
807
+ onStep: (e) => {
808
+ this.runningStepId = "";
809
+ this.trace = [...this.trace, { ...e, mocked }];
810
+ },
811
+ requestHeaders: mocked
812
+ ? undefined
813
+ : (e) => {
814
+ const code = pick(codes[e.stepId] || codes["*"] || "", e.attempt);
815
+ return code ? { Prefer: `code=${code}` } : undefined;
816
+ },
817
+ });
818
+ if (previous) {
819
+ rapiDoc.mockServerStatusCode = previous.code;
820
+ rapiDoc.mockServerStatusStrategy = previous.strategy;
821
+ }
822
+ this.runResult = result;
823
+ this.runState = "done";
824
+ this.dispatchEvent(
825
+ new CustomEvent("workflow-run", {
826
+ detail: { workflowId: graph.workflowId, inputs, result, trace: this.trace },
827
+ bubbles: true,
828
+ composed: true,
829
+ }),
830
+ );
831
+ }
832
+
833
+ /**
834
+ * The security schemes of the loaded OpenAPI sources, for the credentials fields of a standalone
835
+ * element: `{ id, type, scheme, in, name }` each, one per scheme id (a credential applies to every
836
+ * source that names the scheme). Empty with a tied <rapi-doc>, which has its own.
837
+ */
838
+ private standaloneSchemes() {
839
+ if (this.findRapiDoc()) return [];
840
+ const byId = new Map();
841
+ for (const spec of this.specObjects.values()) {
842
+ const schemes = spec?.components?.securitySchemes || spec?.securityDefinitions || {};
843
+ for (const [id, s] of Object.entries(schemes) as [string, any][]) {
844
+ if (s && typeof s === "object" && s.type && !byId.has(id)) {
845
+ byId.set(id, { id, type: s.type, scheme: s.scheme, in: s.in, name: s.name });
846
+ }
847
+ }
848
+ }
849
+ return [...byId.values()];
850
+ }
851
+
852
+ /** The typed credentials as the runtime takes them: a string ("user:password" for Basic). */
853
+ private typedCredentials() {
854
+ const credentials = {};
855
+ for (const [id, value] of Object.entries(this.manualCredentials)) {
856
+ if (!value) continue;
857
+ const scheme = this.standaloneSchemes().find((s) => s.id === id);
858
+ const basic = scheme?.type === "http" && String(scheme.scheme).toLowerCase() === "basic";
859
+ credentials[id] = basic ? value : value.replace(/^bearer\s+/i, "");
860
+ }
861
+ return credentials;
862
+ }
863
+
864
+ /** One run-panel field: a two-line label (name over type) and its control, as <rapi-doc> lays out parameters. */
865
+ private field(name, type, control, { required = false, mono = true } = {}) {
866
+ return html`<label class="run-field">
867
+ <span class="run-field-label">
868
+ <span class="run-field-name" style=${mono ? "" : "font-family: var(--font-sans)"}>
869
+ ${required ? html`<span class="run-field-required" title="Required">*</span>` : nothing}${name}
870
+ </span>
871
+ ${type ? html`<span class="run-field-type">${type}</span>` : nothing}
872
+ </span>
873
+ ${control}
874
+ </label>`;
875
+ }
876
+
877
+ private renderCredentials() {
878
+ const schemes = this.standaloneSchemes();
879
+ if (!schemes.length) return nothing;
880
+ const hint = (s) =>
881
+ s.type === "apiKey"
882
+ ? `API key (${s.in || "header"} ${s.name})`
883
+ : s.type === "http" && String(s.scheme).toLowerCase() === "basic"
884
+ ? "user:password"
885
+ : "token";
886
+ return html`<h3>Credentials <span class="muted">kept in this page only</span></h3>
887
+ ${schemes.map((s) =>
888
+ this.field(
889
+ s.id,
890
+ s.type,
891
+ html`<input
892
+ type="password"
893
+ name=${`credential-${s.id}`}
894
+ autocomplete="off"
895
+ .value=${this.manualCredentials[s.id] ?? ""}
896
+ placeholder=${hint(s)}
897
+ @input=${(e) => (this.manualCredentials = { ...this.manualCredentials, [s.id]: e.target.value })}
898
+ />`,
899
+ ),
900
+ )}`;
901
+ }
902
+
903
+ /** The run's status codes with blanks dropped: `{ "*": "...", <stepId>: "..." }`. */
904
+ private statusCodesByStep(graph): Record<string, string> {
905
+ const out = {};
906
+ const stepIds = new Set(graph.steps.map((s) => s.stepId));
907
+ for (const [key, value] of Object.entries(this.statusCodes || {})) {
908
+ const codes = String(value ?? "").trim();
909
+ if (codes && (key === "*" || stepIds.has(key))) out[key] = codes;
910
+ }
911
+ return out;
912
+ }
913
+
914
+ /**
915
+ * Status codes to answer with, one row per step plus a default: for a tied <rapi-doc>'s
916
+ * in-browser mock, or for a server that honours Prefer: code= (rapidoc mock). Open when the
917
+ * in-browser mock is on; otherwise folded, since a real API ignores it.
918
+ */
919
+ private renderStatusCodes(graph) {
920
+ const mocked = this.findRapiDoc()?.mockServer === "true";
921
+ const set = (key, value) => (this.statusCodes = { ...this.statusCodes, [key]: value });
922
+ const anyList = Object.values(this.statusCodes || {}).some((c) => String(c).includes(","));
923
+ const steps = graph.steps.filter((s) => s.operation);
924
+ return html`<details class="run-mock" ?open=${mocked || Object.keys(this.statusCodesByStep(graph)).length > 0}>
925
+ <summary>
926
+ Mock Response Status
927
+ <span class="muted">${mocked ? "the in-browser mock answers with" : "sent as Prefer: code= (rapidoc mock)"}</span>
928
+ </summary>
929
+ <div class="run-mock-rows">
930
+ ${this.field(
931
+ "Every step",
932
+ "empty: success response",
933
+ html`<input
934
+ name="mock-status"
935
+ inputmode="numeric"
936
+ .value=${this.statusCodes?.["*"] ?? ""}
937
+ placeholder="e.g. 404, or 503, 200"
938
+ @input=${(e) => set("*", e.target.value)}
939
+ />`,
940
+ { mono: false },
941
+ )}
942
+ ${steps.map((s) =>
943
+ this.field(
944
+ s.stepId,
945
+ `${s.operation.method.toUpperCase()} ${s.operation.path}`,
946
+ html`<input
947
+ name=${`mock-status-${s.stepId}`}
948
+ inputmode="numeric"
949
+ .value=${this.statusCodes?.[s.stepId] ?? ""}
950
+ placeholder="Same as every step"
951
+ @input=${(e) => set(s.stepId, e.target.value)}
952
+ />`,
953
+ ),
954
+ )}
955
+ </div>
956
+ ${
957
+ anyList
958
+ ? html`<label class="run-mock-strategy">
959
+ <span class="muted">Several codes are used</span>
960
+ <select aria-label="How the codes are used" @change=${(e) => (this.mockStrategy = e.target.value)}>
961
+ <option value="cycle" ?selected=${this.mockStrategy === "cycle"}>in turn, per step</option>
962
+ <option value="random" ?selected=${this.mockStrategy === "random"}>at random</option>
963
+ </select>
964
+ </label>`
965
+ : nothing
966
+ }
967
+ </details>`;
968
+ }
969
+
970
+ private renderInputsForm(graph) {
971
+ const set = (name, value) => (this.runInputs = { ...this.runInputs, [name]: value });
972
+ const running = this.runState === "running";
973
+ return html`<form
974
+ class="run-inputs"
975
+ part="run-inputs"
976
+ @submit=${(e) => {
977
+ e.preventDefault();
978
+ if (!running) this.executeRun(graph);
979
+ }}
980
+ >
981
+ ${this.renderCredentials()}
982
+ ${graph.inputDetails.length ? html`<h3>Inputs</h3>` : html`<p class="muted">This workflow takes no inputs.</p>`}
983
+ ${graph.inputDetails.map((i) =>
984
+ this.field(
985
+ i.name,
986
+ i.type,
987
+ i.enum
988
+ ? html`<select name=${i.name} @change=${(e) => set(i.name, e.target.value)}>
989
+ <option value="" ?selected=${!this.runInputs[i.name]}></option>
990
+ ${i.enum.map(
991
+ (v) =>
992
+ html`<option value=${String(v)} ?selected=${String(v) === this.runInputs[i.name]}>${String(v)}</option>`,
993
+ )}
994
+ </select>`
995
+ : html`<input
996
+ name=${i.name}
997
+ .value=${this.runInputs[i.name] ?? ""}
998
+ placeholder=${i.description || ""}
999
+ ?required=${i.required}
1000
+ @input=${(e) => set(i.name, e.target.value)}
1001
+ />`,
1002
+ { required: i.required },
1003
+ ),
1004
+ )}
1005
+ ${this.renderStatusCodes(graph)}
1006
+ <div class="run-actions">
1007
+ <button type="submit" class="run-button" part="run-button" ?disabled=${running}>
1008
+ ${running ? "Running…" : "▶ Run workflow"}
1009
+ </button>
1010
+ ${this.runState === "done" ? html`<button type="button" class="run-clear" @click=${() => this.resetRun()}>Clear</button>` : nothing}
1011
+ </div>
1012
+ </form>`;
1013
+ }
1014
+
1015
+ private renderTraceEntry(e, n) {
1016
+ const pretty = (v) => {
1017
+ const text = typeof v === "string" ? v : JSON.stringify(v, null, 2);
1018
+ return text && text.length > 4000 ? `${text.slice(0, 4000)}…` : text;
1019
+ };
1020
+ return html`<li class="trace-${e.outcome}">
1021
+ <details ?open=${e.outcome === "failure"}>
1022
+ <summary>
1023
+ <span class="trace-step">${n + 1}. ${e.stepId}</span>
1024
+ ${e.attempt > 1 ? html`<span class="muted">attempt ${e.attempt}</span>` : nothing}
1025
+ <span class="trace-request">${e.request?.method} ${e.request?.url}</span>
1026
+ <span class="trace-status">${e.response ? `HTTP ${e.response.status}` : "no response"}${e.mocked ? html` <span class="muted">mocked</span>` : nothing}</span>
1027
+ <span class="trace-outcome">${e.outcome === "success" ? "✓" : "✗"}</span>
1028
+ ${e.failedCriterion ? html`<code>${e.failedCriterion}</code>` : nothing}
1029
+ ${e.action ? html`<span class="muted">→ ${e.action.type}${e.action.stepId ? ` ${e.action.stepId}` : ""}</span>` : nothing}
1030
+ </summary>
1031
+ ${e.request?.body !== undefined ? html`<h4>Request body</h4><pre>${pretty(e.request.body)}</pre>` : nothing}
1032
+ ${e.response ? html`<h4>Response</h4><pre>${pretty(e.response.body)}</pre>` : nothing}
1033
+ ${e.outputs && Object.keys(e.outputs).length ? html`<h4>Outputs</h4><pre>${pretty(e.outputs)}</pre>` : nothing}
1034
+ </details>
1035
+ </li>`;
1036
+ }
1037
+
1038
+ /** The run panel under the diagram: the inputs form and button, then the log and the result. */
1039
+ private renderRun(graph) {
1040
+ if (this.allowRun === "false") return nothing;
1041
+ return html`<section class="flow-run" part="run">
1042
+ <h3>Run</h3>
1043
+ ${this.renderInputsForm(graph)}
1044
+ ${
1045
+ this.trace.length
1046
+ ? html`<h4>Log</h4>
1047
+ <ol class="run-trace">${this.trace.map((e, n) => this.renderTraceEntry(e, n))}</ol>`
1048
+ : nothing
1049
+ }
1050
+ ${this.runProblem ? html`<p class="run-problem">Cannot run this workflow: ${this.runProblem}</p>` : nothing}
1051
+ ${
1052
+ this.runResult
1053
+ ? this.runResult.isError
1054
+ ? html`<pre class="run-error">${this.runResult.content[0].text}</pre>`
1055
+ : html`<h4>Outputs</h4><pre class="run-outputs">${this.runResult.content[0].text}</pre>`
1056
+ : nothing
1057
+ }
1058
+ </section>`;
1059
+ }
1060
+
1061
+ private renderViewToggle() {
1062
+ if (this.showViewToggle === "false") return nothing;
1063
+ const current = this.view === "compact" ? "compact" : "expanded";
1064
+ const button = (view, label) =>
1065
+ html`<button type="button" aria-pressed=${current === view ? "true" : "false"} @click=${() => this.setView(view)}>${label}</button>`;
1066
+ return html`<div class="view-toggle" part="view-toggle" role="group" aria-label="Diagram view">
1067
+ ${button("compact", "Compact")}${button("expanded", "Expanded")}
1068
+ </div>`;
1069
+ }
1070
+
1071
+ /** Lists only the kinds of lines the diagram drew. */
1072
+ private renderLegend(kinds: string[]) {
1073
+ if (!kinds.length) return nothing;
1074
+ const line = (cls, head, dashed = false) =>
1075
+ html`<svg width="28" height="10" aria-hidden="true"><path class=${cls} d="M1 5H23" stroke-dasharray=${dashed ? "5 4" : "none"}/><path class="rf-head-${head}" d="M22 1L28 5L22 9z"/></svg>`;
1076
+ const entries = {
1077
+ seq: html`${line("rf-seq", "seq")}next step`,
1078
+ data: html`${line("rf-data", "data", true)}value passed to a later step`,
1079
+ output: html`${line("rf-data rf-to-output", "output", true)}value passed to the outputs`,
1080
+ success: html`${line("rf-goto rf-success", "success")}goto on success`,
1081
+ failure: html`${line("rf-goto rf-failure", "failure")}goto on failure`,
1082
+ };
1083
+ return html`<ul class="flow-legend" part="legend" aria-label="Legend">
1084
+ ${kinds.map((k) => html`<li>${entries[k]}</li>`)}
1085
+ </ul>`;
1086
+ }
1087
+
1088
+ render() {
1089
+ if (this.loadError) {
1090
+ return html`<p class="flow-message" part="message">Could not load the workflows: ${this.loadError}</p>`;
1091
+ }
1092
+ if (!this.doc) {
1093
+ return this.loading
1094
+ ? html`<p class="flow-message" part="message">Loading workflows…</p>`
1095
+ : nothing;
1096
+ }
1097
+ const graph = this.graph;
1098
+ const ids = this.workflowIds;
1099
+ const view = this.view === "compact" ? "compact" : "expanded";
1100
+ const diagram = renderFlowDiagram(graph, {
1101
+ view,
1102
+ idPrefix: this.idPrefix,
1103
+ interactive: true,
1104
+ });
1105
+ let svg = diagram.svg;
1106
+ // The selected step and, during a run, the step whose request is in flight, as box classes
1107
+ const extraClasses = new Map<string, string[]>();
1108
+ if (this.selectedStepId) extraClasses.set(this.selectedStepId, ["rf-selected"]);
1109
+ if (this.runningStepId) {
1110
+ extraClasses.set(this.runningStepId, [
1111
+ ...(extraClasses.get(this.runningStepId) || []),
1112
+ "rf-running",
1113
+ ]);
1114
+ }
1115
+ for (const [id, classes] of extraClasses) {
1116
+ const stepId = escapeXml(id).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1117
+ svg = svg.replace(
1118
+ new RegExp(`class="rf-node rf-step( rf-clickable)?" data-step-id="${stepId}"`),
1119
+ (_, clickable) =>
1120
+ `class="rf-node rf-step${clickable || ""} ${classes.join(" ")}" data-step-id="${escapeXml(id)}"`,
1121
+ );
1122
+ }
1123
+ // Only problems a reader can't miss; the graph's authoring warnings are left to tooling
1124
+ const shown = this.problems.filter(
1125
+ (p) => p.level === "error" || p.path.startsWith("sourceDescriptions"),
1126
+ );
1127
+ return html`
1128
+ ${
1129
+ graph
1130
+ ? html`<div class="flow-header" part="header">
1131
+ <h2 class="flow-title" part="title">${graph.summary || graph.workflowId}</h2>
1132
+ ${graph.summary ? html`<span class="flow-id">${graph.workflowId}</span>` : nothing}
1133
+ <div class="flow-controls">
1134
+ ${
1135
+ ids.length > 1
1136
+ ? html`<select
1137
+ part="select"
1138
+ aria-label="Workflow"
1139
+ .value=${graph.workflowId}
1140
+ @change=${(e) => {
1141
+ this.workflowId = e.target.value;
1142
+ this.selectedStepId = "";
1143
+ this.resetRun();
1144
+ }}
1145
+ >
1146
+ ${ids.map((id) => html`<option value=${id} ?selected=${id === graph.workflowId}>${id}</option>`)}
1147
+ </select>`
1148
+ : nothing
1149
+ }
1150
+ ${this.renderViewToggle()}
1151
+ </div>
1152
+ </div>
1153
+ ${
1154
+ graph.description
1155
+ ? html`<p class="flow-description" part="description">${graph.description}</p>`
1156
+ : nothing
1157
+ }
1158
+ <div class="flow-diagram" part="diagram" @click=${this.onDiagramEvent} @keydown=${this.onDiagramEvent} @mouseover=${this.onDiagramHover} @mouseleave=${this.onDiagramHover}>
1159
+ ${unsafeSVG(svg)}
1160
+ </div>
1161
+ ${this.renderLegend(diagram.legend)}
1162
+ ${this.renderRun(graph)}
1163
+ `
1164
+ : html`<p class="flow-message" part="message">No workflow to show.</p>`
1165
+ }
1166
+ ${
1167
+ shown.length
1168
+ ? html`<ul class="flow-problems" part="problems">
1169
+ ${shown.map(
1170
+ (p) =>
1171
+ html`<li class=${p.level}>${p.path ? html`<code>${p.path}</code> ` : nothing}${p.message}</li>`,
1172
+ )}
1173
+ </ul>`
1174
+ : nothing
1175
+ }
1176
+ `;
1177
+ }
1178
+ }