@shortlink-org/portolan 0.2.1 → 0.2.2
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/cli/init.mjs +14 -6
- package/cli/init.test.mjs +8 -1
- package/cli/portolan.mjs +56 -7
- package/cli/portolan.test.mjs +47 -0
- package/package.json +7 -6
- package/plugins/extract-django/README.md +1 -1
- package/plugins/extract-django/extract_test.py +1 -1
- package/plugins/extract-django/openapi.py +4 -14
- package/plugins/extract-django/openapi_test.py +4 -4
- package/plugins/extract-go/options.schema.json +5 -0
- package/plugins/extract-project/options.schema.json +21 -0
- package/plugins/extract-ts/README.md +1 -1
- package/plugins/extract-ts/clients.ts +1 -1
- package/plugins/extract-ts/extract.test.ts +2 -2
- package/plugins/extract-ts/graphql.test.ts +2 -2
- package/plugins/extract-ts/openapi.test.ts +6 -7
- package/plugins/extract-ts/openapi.ts +3 -10
- package/plugins/portolan-go.wasm +0 -0
- package/public/404-cat-v1.webp +0 -0
- package/public/cat-about-v1.webp +0 -0
- package/public/cat-clear-skies-v1.webp +0 -0
- package/public/cat-no-changes-v1.webp +0 -0
- package/public/cat-onboarding-v1.webp +0 -0
- package/public/cat-trial-success-v1.webp +0 -0
- package/schema/portolan.schema.json +61 -0
- package/scripts/catalog-sources.mjs +12 -1
- package/scripts/catalog-sources.test.mjs +35 -0
- package/scripts/gen-likec4.mjs +242 -85
- package/scripts/gen-likec4.test.mjs +168 -14
- package/scripts/gen.mjs +5 -3
- package/scripts/host-plugins/fetch-git.mjs +22 -12
- package/scripts/host-plugins/fetch-git.test.mjs +25 -5
- package/scripts/local-api.mjs +363 -28
- package/scripts/local-api.test.mjs +195 -1
- package/scripts/plugin-host.mjs +14 -3
- package/scripts/plugin-host.test.mjs +8 -0
- package/scripts/schema.mjs +7 -0
- package/src/app/CatalogApp.tsx +19 -4
- package/src/app/CommandPalette.tsx +7 -2
- package/src/app/toast.tsx +11 -7
- package/src/catalog.test.ts +33 -0
- package/src/catalog.ts +190 -8
- package/src/components/CatIllustration.tsx +30 -0
- package/src/components/CommitLink.tsx +154 -0
- package/src/components/EditorLink.tsx +15 -8
- package/src/components/GrpcMethodReference.tsx +93 -0
- package/src/components/Markdown.tsx +17 -0
- package/src/components/MethodRows.tsx +8 -1
- package/src/components/PageHeader.test.ts +24 -0
- package/src/components/PageHeader.tsx +34 -0
- package/src/components/ShapeRows.tsx +12 -0
- package/src/components/SourceDoc.tsx +27 -12
- package/src/components/SourcePreview.tsx +124 -36
- package/src/enrich.test.ts +209 -2
- package/src/enrich.ts +284 -15
- package/src/er/RedisSchema.tsx +86 -5
- package/src/flow/FlowTable.tsx +19 -4
- package/src/flow/StepDetail.tsx +461 -101
- package/src/flow/StepRail.tsx +36 -13
- package/src/flow/answers-response.test.ts +100 -0
- package/src/flow/answers.ts +11 -8
- package/src/flow/chapters.ts +2 -1
- package/src/flow/labels.ts +10 -0
- package/src/flow/mermaid.test.ts +87 -3
- package/src/flow/mermaid.ts +26 -14
- package/src/index.css +132 -0
- package/src/landing/LandingPage.tsx +12 -8
- package/src/lib/api.test.ts +4 -6
- package/src/lib/build-info.test.ts +29 -0
- package/src/lib/build-info.ts +21 -0
- package/src/lib/data-model.ts +14 -0
- package/src/lib/flow-tree.ts +1 -1
- package/src/lib/github-catalog.test.ts +51 -0
- package/src/lib/github-catalog.ts +69 -0
- package/src/lib/grpc-reference.test.ts +72 -0
- package/src/lib/grpc-reference.ts +117 -0
- package/src/lib/local-api.ts +17 -4
- package/src/lib/product.ts +7 -0
- package/src/lib/queries.ts +16 -1
- package/src/lib/readme-assets.test.ts +19 -0
- package/src/lib/readme-assets.ts +34 -0
- package/src/lib/setup-info.test.ts +7 -0
- package/src/lib/setup-info.ts +5 -0
- package/src/lib/source-link.test.ts +106 -18
- package/src/lib/source-link.ts +22 -6
- package/src/merge.test.ts +55 -0
- package/src/merge.ts +30 -0
- package/src/pages/AdrDetail.tsx +3 -2
- package/src/pages/AdrIndex.tsx +10 -7
- package/src/pages/AggregatePage.tsx +34 -4
- package/src/pages/BlockPage.tsx +39 -32
- package/src/pages/Changes.tsx +7 -9
- package/src/pages/ContextMap.tsx +11 -2
- package/src/pages/ContextPage.tsx +11 -4
- package/src/pages/FlowIndex.tsx +16 -7
- package/src/pages/GraphPage.tsx +21 -7
- package/src/pages/Language.tsx +9 -9
- package/src/pages/NotFound.tsx +58 -15
- package/src/pages/Overview.tsx +6 -0
- package/src/pages/Problems.tsx +10 -8
- package/src/pages/RegistryIndex.tsx +10 -8
- package/src/pages/ServicePage.tsx +1 -1
- package/src/pages/Settings.tsx +382 -44
- package/src/routes.test.ts +2 -0
- package/src/routes.ts +2 -1
- package/src/selection/DetailPanel.tsx +213 -113
- package/src/testing/setup.ts +14 -0
package/src/flow/StepDetail.tsx
CHANGED
|
@@ -10,8 +10,16 @@ import { SourcePreviewButton } from "../components/SourcePreview";
|
|
|
10
10
|
import { flowRepoService } from "../lib/derive";
|
|
11
11
|
import { sourceLocation } from "../lib/source-link";
|
|
12
12
|
import { AdrNumber, StatusChip } from "../components/primitives";
|
|
13
|
+
import { ShapeRows } from "../components/ShapeRows";
|
|
14
|
+
import { shapeFor } from "../components/MethodRows";
|
|
13
15
|
import { stepAnswer } from "./answers";
|
|
14
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
aggregatePath,
|
|
18
|
+
paths,
|
|
19
|
+
eventPath,
|
|
20
|
+
servicePath,
|
|
21
|
+
storePath,
|
|
22
|
+
} from "../routes";
|
|
15
23
|
|
|
16
24
|
function Label({ children }: { children: React.ReactNode }) {
|
|
17
25
|
return <div className="label mt-3 mb-1">{children}</div>;
|
|
@@ -156,42 +164,237 @@ function RpcDetail({ step, flow }: { step: Step; flow: Flow }) {
|
|
|
156
164
|
const answer = stepAnswer(index, step);
|
|
157
165
|
|
|
158
166
|
return (
|
|
159
|
-
|
|
160
|
-
|
|
167
|
+
<section
|
|
168
|
+
aria-label="RPC contract"
|
|
169
|
+
className="overflow-hidden rounded-card border shadow-xs border-line"
|
|
170
|
+
>
|
|
171
|
+
<header className="border-b px-3 py-2.5 border-line bg-surface">
|
|
172
|
+
<h2 className="label">RPC contract</h2>
|
|
173
|
+
<div className="mt-2">
|
|
174
|
+
<Ident block value={method} className="text-ink" />
|
|
175
|
+
</div>
|
|
176
|
+
</header>
|
|
161
177
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
<DetailSection title="Contract">
|
|
179
|
+
<dl className="grid grid-cols-[minmax(7rem,auto)_minmax(0,1fr)] gap-x-4 gap-y-2">
|
|
180
|
+
{answer ? (
|
|
181
|
+
<>
|
|
182
|
+
<dt className="mono text-muted">Answers with</dt>
|
|
183
|
+
<dd className="mono break-all text-ink">{answer}</dd>
|
|
184
|
+
</>
|
|
185
|
+
) : null}
|
|
186
|
+
<dt className="mono text-muted">Provider</dt>
|
|
187
|
+
<dd>
|
|
188
|
+
{provider && providerPath ? (
|
|
189
|
+
<Link to={providerPath} className="mono text-accent">
|
|
190
|
+
{provider.id} →
|
|
191
|
+
</Link>
|
|
192
|
+
) : (
|
|
193
|
+
<span className="mono inline-flex items-center gap-1.5 rounded-control border px-1.5 py-0.5 status-unresolved">
|
|
194
|
+
<AlertTriangle size={11} aria-hidden />
|
|
195
|
+
no provider found
|
|
196
|
+
</span>
|
|
197
|
+
)}
|
|
198
|
+
</dd>
|
|
199
|
+
{call ? (
|
|
200
|
+
<>
|
|
201
|
+
<dt className="mono text-muted">Declared by</dt>
|
|
202
|
+
<dd className="mono break-all text-ink">{call.peer}</dd>
|
|
203
|
+
</>
|
|
204
|
+
) : null}
|
|
205
|
+
</dl>
|
|
206
|
+
</DetailSection>
|
|
207
|
+
|
|
208
|
+
<DetailSection title="Source">
|
|
209
|
+
{call ? (
|
|
210
|
+
<SourceWhere where={call.source} flow={flow} structured />
|
|
211
|
+
) : step.line ? (
|
|
212
|
+
<SourceWhere where={step.line} flow={flow} structured />
|
|
213
|
+
) : (
|
|
214
|
+
<div className="mono text-muted">not recorded</div>
|
|
215
|
+
)}
|
|
216
|
+
</DetailSection>
|
|
217
|
+
|
|
218
|
+
{step.line ? (
|
|
219
|
+
<DetailSection title="Observed at">
|
|
220
|
+
<SourceWhere where={step.line} flow={flow} structured />
|
|
221
|
+
</DetailSection>
|
|
167
222
|
) : null}
|
|
223
|
+
</section>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
168
226
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
227
|
+
function DetailSection({
|
|
228
|
+
title,
|
|
229
|
+
meta,
|
|
230
|
+
children,
|
|
231
|
+
}: {
|
|
232
|
+
title: string;
|
|
233
|
+
meta?: React.ReactNode;
|
|
234
|
+
children: React.ReactNode;
|
|
235
|
+
}) {
|
|
236
|
+
return (
|
|
237
|
+
<section className="border-b border-line px-3 py-3 last:border-b-0">
|
|
238
|
+
<div className="mb-2 flex items-baseline justify-between gap-3">
|
|
239
|
+
<h3 className="label">{title}</h3>
|
|
240
|
+
{meta ? <span className="mono text-muted">{meta}</span> : null}
|
|
241
|
+
</div>
|
|
242
|
+
{children}
|
|
243
|
+
</section>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function ResponseDetail({ step, flow }: { step: Step; flow: Flow }) {
|
|
248
|
+
const response = step.http;
|
|
249
|
+
if (!response) {
|
|
250
|
+
return (
|
|
251
|
+
<>
|
|
252
|
+
<div className="mono text-[13px]">{step.label ?? "response"}</div>
|
|
253
|
+
<Label>Source</Label>
|
|
254
|
+
<Where step={step} flow={flow} />
|
|
255
|
+
</>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const cut = response.bodyRef?.lastIndexOf("/") ?? -1;
|
|
260
|
+
const interfaceId = cut >= 0 ? response.bodyRef!.slice(0, cut) : "";
|
|
261
|
+
const methodName = cut >= 0 ? response.bodyRef!.slice(cut + 1) : "";
|
|
262
|
+
const provider = response.bodyRef
|
|
263
|
+
? index.rpcProviderByMethod.get(response.bodyRef)
|
|
264
|
+
: undefined;
|
|
265
|
+
const provided = provider?.provides.find((item) => item.id === interfaceId);
|
|
266
|
+
const method = provided?.methods.find((item) => item.name === methodName);
|
|
267
|
+
const fields = provided
|
|
268
|
+
? shapeFor(provided, method?.response ?? response.body, method?.responseRef)
|
|
269
|
+
: (response.fields ?? null);
|
|
270
|
+
const providerPath = provider ? servicePath(provider.id) : null;
|
|
271
|
+
const error = response.outcome === "error";
|
|
272
|
+
const headerCount = response.contentType ? 1 : 0;
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<section
|
|
276
|
+
aria-label="HTTP response contract"
|
|
277
|
+
className="overflow-hidden rounded-card border shadow-xs"
|
|
278
|
+
style={{
|
|
279
|
+
borderColor: error ? "var(--response-error)" : "var(--border)",
|
|
280
|
+
}}
|
|
281
|
+
>
|
|
282
|
+
<header
|
|
283
|
+
className="border-b border-line px-3 py-2.5"
|
|
284
|
+
style={{
|
|
285
|
+
background: error ? "var(--response-error-bg)" : "var(--surface)",
|
|
286
|
+
}}
|
|
287
|
+
>
|
|
288
|
+
<div className="flex items-center justify-between gap-3">
|
|
289
|
+
<h2 className="label">Response contract</h2>
|
|
290
|
+
<span
|
|
291
|
+
className="mono rounded-[4px] border px-1.5 py-px"
|
|
292
|
+
style={{
|
|
293
|
+
borderColor: error
|
|
294
|
+
? "var(--response-error)"
|
|
295
|
+
: "var(--status-verified)",
|
|
296
|
+
color: error ? "var(--response-error)" : "var(--status-verified)",
|
|
297
|
+
}}
|
|
298
|
+
>
|
|
299
|
+
{error ? "error path" : "success"}
|
|
300
|
+
</span>
|
|
178
301
|
</div>
|
|
179
|
-
|
|
302
|
+
<div className="mt-2 flex flex-wrap items-baseline gap-2">
|
|
303
|
+
<span className="mono text-sm font-medium text-ink">
|
|
304
|
+
HTTP {response.status ?? "status unknown"}
|
|
305
|
+
</span>
|
|
306
|
+
<span className="mono text-muted">{response.body ?? "response"}</span>
|
|
307
|
+
</div>
|
|
308
|
+
</header>
|
|
180
309
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
310
|
+
<DetailSection title="Headers" meta={headerCount}>
|
|
311
|
+
{headerCount > 0 ? (
|
|
312
|
+
<dl className="grid grid-cols-[minmax(7rem,auto)_minmax(0,1fr)] gap-x-4 gap-y-1.5">
|
|
313
|
+
<dt className="mono text-muted">Content-Type</dt>
|
|
314
|
+
<dd className="mono break-all text-ink">{response.contentType}</dd>
|
|
315
|
+
</dl>
|
|
316
|
+
) : (
|
|
317
|
+
<div className="mono text-muted">no explicit headers detected</div>
|
|
318
|
+
)}
|
|
319
|
+
</DetailSection>
|
|
320
|
+
|
|
321
|
+
<DetailSection
|
|
322
|
+
title="Body"
|
|
323
|
+
meta={
|
|
324
|
+
fields
|
|
325
|
+
? `${fields.length} field${fields.length === 1 ? "" : "s"}`
|
|
326
|
+
: undefined
|
|
327
|
+
}
|
|
328
|
+
>
|
|
329
|
+
<div className="flex flex-col gap-3">
|
|
330
|
+
<dl className="grid grid-cols-[minmax(7rem,auto)_minmax(0,1fr)] gap-x-4 gap-y-1.5">
|
|
331
|
+
<dt className="mono text-muted">Schema</dt>
|
|
332
|
+
<dd className="mono break-all text-ink">
|
|
333
|
+
{response.body ?? "unknown"}
|
|
334
|
+
</dd>
|
|
335
|
+
{response.encoding ? (
|
|
336
|
+
<>
|
|
337
|
+
<dt className="mono text-muted">Encoding</dt>
|
|
338
|
+
<dd className="mono text-ink">{response.encoding}</dd>
|
|
339
|
+
</>
|
|
340
|
+
) : null}
|
|
341
|
+
</dl>
|
|
342
|
+
|
|
343
|
+
{fields ? (
|
|
344
|
+
<div className="overflow-x-auto rounded-control border border-line bg-canvas px-2.5 py-2">
|
|
345
|
+
{fields.length > 0 ? (
|
|
346
|
+
<ShapeRows fields={fields} enums={provided?.enums} showHeader />
|
|
347
|
+
) : (
|
|
348
|
+
<div className="mono text-muted">empty message</div>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
) : null}
|
|
352
|
+
</div>
|
|
353
|
+
</DetailSection>
|
|
354
|
+
|
|
355
|
+
{response.bodyRef ? (
|
|
356
|
+
<DetailSection title="Contract lineage">
|
|
357
|
+
<dl className="grid grid-cols-[minmax(7rem,auto)_minmax(0,1fr)] gap-x-4 gap-y-1.5">
|
|
358
|
+
<dt className="mono text-muted">RPC response</dt>
|
|
359
|
+
<dd>
|
|
360
|
+
<Ident block value={response.bodyRef} className="text-ink" />
|
|
361
|
+
</dd>
|
|
362
|
+
{provider && providerPath ? (
|
|
363
|
+
<>
|
|
364
|
+
<dt className="mono text-muted">Provider</dt>
|
|
365
|
+
<dd>
|
|
366
|
+
<Link to={providerPath} className="mono text-accent">
|
|
367
|
+
{provider.id} →
|
|
368
|
+
</Link>
|
|
369
|
+
</dd>
|
|
370
|
+
</>
|
|
371
|
+
) : null}
|
|
372
|
+
</dl>
|
|
373
|
+
</DetailSection>
|
|
374
|
+
) : null}
|
|
375
|
+
|
|
376
|
+
{response.warning ? (
|
|
377
|
+
<DetailSection title="Diagnostics" meta="1 issue">
|
|
378
|
+
<div
|
|
379
|
+
className="flex items-start gap-2 rounded-control border px-2.5 py-2"
|
|
380
|
+
style={{
|
|
381
|
+
borderColor: "var(--response-error)",
|
|
382
|
+
color: "var(--response-error)",
|
|
383
|
+
background: "var(--response-error-bg)",
|
|
384
|
+
}}
|
|
385
|
+
>
|
|
386
|
+
<AlertTriangle size={13} aria-hidden className="mt-0.5 shrink-0" />
|
|
387
|
+
<span>{response.warning}</span>
|
|
388
|
+
</div>
|
|
389
|
+
</DetailSection>
|
|
390
|
+
) : null}
|
|
391
|
+
|
|
392
|
+
{step.line ? (
|
|
393
|
+
<DetailSection title="Observed at">
|
|
394
|
+
<SourceWhere where={step.line} flow={flow} structured />
|
|
395
|
+
</DetailSection>
|
|
396
|
+
) : null}
|
|
397
|
+
</section>
|
|
195
398
|
);
|
|
196
399
|
}
|
|
197
400
|
|
|
@@ -202,68 +405,99 @@ const FRAME_KEYWORD: Record<StepFrame["kind"], string> = {
|
|
|
202
405
|
};
|
|
203
406
|
|
|
204
407
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* on every path, and nothing else on this panel would say so.
|
|
408
|
+
* Where a step travels and the frames that allow it to run. Selecting a step
|
|
409
|
+
* from a graph gives no sense of its branch, so route and condition are one
|
|
410
|
+
* execution context rather than two loose lines above the contract.
|
|
209
411
|
*/
|
|
210
|
-
function
|
|
412
|
+
function ExecutionContext({ step, flow }: { step: Step; flow: Flow }) {
|
|
211
413
|
const frames = useMemo(
|
|
212
414
|
() => stepFrames(flow.steps).get(step.id) ?? [],
|
|
213
415
|
[flow, step.id],
|
|
214
416
|
);
|
|
215
|
-
if (frames.length === 0) return null;
|
|
216
|
-
|
|
217
417
|
const conditional = frames.some((f) => f.kind === "alt");
|
|
218
418
|
|
|
219
419
|
return (
|
|
220
|
-
<
|
|
221
|
-
<
|
|
222
|
-
|
|
420
|
+
<section className="mb-3 overflow-hidden rounded-card border shadow-xs border-line">
|
|
421
|
+
<header className="flex items-center justify-between gap-3 border-b px-3 py-2 border-line bg-surface">
|
|
422
|
+
<h2 className="label">Execution context</h2>
|
|
423
|
+
{conditional ? (
|
|
424
|
+
<span className="mono rounded-[4px] border px-1.5 py-px border-line-strong text-muted">
|
|
425
|
+
conditional
|
|
426
|
+
</span>
|
|
427
|
+
) : null}
|
|
428
|
+
</header>
|
|
429
|
+
|
|
430
|
+
<div className="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-3 px-3 py-2.5">
|
|
431
|
+
<div className="min-w-0">
|
|
432
|
+
<div className="label mb-1">From</div>
|
|
433
|
+
<div className="mono break-all text-ink">{step.from}</div>
|
|
434
|
+
</div>
|
|
435
|
+
<span className="mono text-muted" aria-hidden>
|
|
436
|
+
→
|
|
437
|
+
</span>
|
|
438
|
+
<div className="min-w-0">
|
|
439
|
+
<div className="label mb-1">To</div>
|
|
440
|
+
<div className="mono break-all text-ink">{step.to}</div>
|
|
441
|
+
</div>
|
|
223
442
|
</div>
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
?
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
{FRAME_KEYWORD[frame.kind]}
|
|
249
|
-
</span>
|
|
250
|
-
<span className="mono min-w-0 flex-1" title={text}>
|
|
251
|
-
{text}
|
|
252
|
-
</span>
|
|
253
|
-
{frame.terminal ? (
|
|
254
|
-
<span
|
|
255
|
-
className="mono shrink-0"
|
|
256
|
-
style={{ color: "var(--status-unresolved)" }}
|
|
257
|
-
title="This branch ends the flow — the steps drawn after it do not follow this one"
|
|
443
|
+
|
|
444
|
+
{frames.length > 0 ? (
|
|
445
|
+
<div className="border-t px-3 py-2.5 border-line">
|
|
446
|
+
<div className="mb-2 flex items-baseline justify-between gap-3">
|
|
447
|
+
<h3 className="label">
|
|
448
|
+
{conditional ? "Runs only when" : "Runs inside"}
|
|
449
|
+
</h3>
|
|
450
|
+
<span className="mono text-muted">
|
|
451
|
+
{frames.length} frame{frames.length === 1 ? "" : "s"}
|
|
452
|
+
</span>
|
|
453
|
+
</div>
|
|
454
|
+
<div className="flex flex-col gap-1.5">
|
|
455
|
+
{frames.map((frame, i) => {
|
|
456
|
+
const alt = frame.kind === "alt";
|
|
457
|
+
const text =
|
|
458
|
+
frame.kind === "parallel"
|
|
459
|
+
? [frame.title, `branch ${frame.branch}`]
|
|
460
|
+
.filter(Boolean)
|
|
461
|
+
.join(" · ")
|
|
462
|
+
: (frame.branch ?? frame.title ?? "");
|
|
463
|
+
return (
|
|
464
|
+
<div
|
|
465
|
+
key={`${frame.id}:${i}`}
|
|
466
|
+
className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded-control border px-2 py-1.5 border-line bg-canvas"
|
|
258
467
|
>
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
468
|
+
<span
|
|
469
|
+
className="mono shrink-0 rounded-[4px] border px-1.5 py-px uppercase"
|
|
470
|
+
style={{
|
|
471
|
+
borderColor: alt
|
|
472
|
+
? "var(--border-strong)"
|
|
473
|
+
: "var(--border)",
|
|
474
|
+
color: alt ? "var(--fg)" : "var(--fg-muted)",
|
|
475
|
+
}}
|
|
476
|
+
>
|
|
477
|
+
{FRAME_KEYWORD[frame.kind]}
|
|
478
|
+
</span>
|
|
479
|
+
<span
|
|
480
|
+
className="mono min-w-0 break-words text-ink"
|
|
481
|
+
title={text}
|
|
482
|
+
>
|
|
483
|
+
{text}
|
|
484
|
+
</span>
|
|
485
|
+
{frame.terminal ? (
|
|
486
|
+
<span
|
|
487
|
+
className="mono shrink-0"
|
|
488
|
+
style={{ color: "var(--status-unresolved)" }}
|
|
489
|
+
title="This branch ends the flow — the steps drawn after it do not follow this one"
|
|
490
|
+
>
|
|
491
|
+
ends flow
|
|
492
|
+
</span>
|
|
493
|
+
) : null}
|
|
494
|
+
</div>
|
|
495
|
+
);
|
|
496
|
+
})}
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
) : null}
|
|
500
|
+
</section>
|
|
267
501
|
);
|
|
268
502
|
}
|
|
269
503
|
|
|
@@ -276,24 +510,61 @@ function Frames({ step, flow }: { step: Step; flow: Flow }) {
|
|
|
276
510
|
* `file:line` in the repository this was built from opens on the line; a
|
|
277
511
|
* trace id or a path in another repository stays as it is, to copy.
|
|
278
512
|
*/
|
|
279
|
-
function SourceWhere({
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
513
|
+
function SourceWhere({
|
|
514
|
+
where,
|
|
515
|
+
flow,
|
|
516
|
+
structured = false,
|
|
517
|
+
}: {
|
|
518
|
+
where: string;
|
|
519
|
+
flow: Flow;
|
|
520
|
+
structured?: boolean;
|
|
521
|
+
}) {
|
|
522
|
+
const location = sourceLocation(
|
|
523
|
+
where,
|
|
524
|
+
flowRepoService(catalog, flow),
|
|
525
|
+
allRepos(catalog),
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const actions = (
|
|
529
|
+
<>
|
|
530
|
+
<SourcePreviewButton
|
|
531
|
+
location={location}
|
|
532
|
+
className="border px-2 py-1 border-line bg-canvas hover:bg-raised hover:no-underline"
|
|
533
|
+
/>
|
|
285
534
|
{location?.href ? (
|
|
286
535
|
<a
|
|
287
536
|
href={location.href}
|
|
288
537
|
target="_blank"
|
|
289
538
|
rel="noreferrer"
|
|
290
|
-
className="rounded-control text-accent hover:
|
|
539
|
+
className="mono inline-flex items-center rounded-control border px-2 py-1 border-line bg-canvas text-accent hover:bg-raised"
|
|
291
540
|
title="Open on the forge, at the built commit"
|
|
292
541
|
>
|
|
293
|
-
|
|
542
|
+
forge ↗
|
|
294
543
|
</a>
|
|
295
544
|
) : null}
|
|
296
|
-
<EditorLink
|
|
545
|
+
<EditorLink
|
|
546
|
+
location={location}
|
|
547
|
+
variant="text"
|
|
548
|
+
className="inline-flex items-center border px-2 py-1 border-line bg-canvas hover:bg-raised hover:no-underline"
|
|
549
|
+
/>
|
|
550
|
+
</>
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
if (structured) {
|
|
554
|
+
return (
|
|
555
|
+
<div className="flex min-w-0 flex-col gap-2">
|
|
556
|
+
<div className="mono min-w-0 break-all text-muted">
|
|
557
|
+
<Ident block value={where} className="text-muted" />
|
|
558
|
+
</div>
|
|
559
|
+
<div className="flex flex-wrap items-center gap-1.5">{actions}</div>
|
|
560
|
+
</div>
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return (
|
|
565
|
+
<div className="mono flex flex-wrap items-center gap-2 break-all text-muted">
|
|
566
|
+
<Ident block value={where} className="text-muted" />
|
|
567
|
+
{actions}
|
|
297
568
|
</div>
|
|
298
569
|
);
|
|
299
570
|
}
|
|
@@ -303,18 +574,100 @@ function Where({ step, flow }: { step: Step; flow: Flow }) {
|
|
|
303
574
|
return <SourceWhere where={step.line} flow={flow} />;
|
|
304
575
|
}
|
|
305
576
|
|
|
577
|
+
function StoreCallDetail({ step, flow }: { step: Step; flow: Flow }) {
|
|
578
|
+
const access = step.storeAccess!;
|
|
579
|
+
const store = index.storeById.get(access.store);
|
|
580
|
+
const keyspace = store?.keyspaces?.find(
|
|
581
|
+
(candidate) => candidate.pattern === access.keyspace,
|
|
582
|
+
);
|
|
583
|
+
const aggregateId = keyspace?.persists?.aggregate;
|
|
584
|
+
const toStore = storePath(access.store);
|
|
585
|
+
const toAggregate = aggregateId ? aggregatePath(aggregateId) : null;
|
|
586
|
+
|
|
587
|
+
return (
|
|
588
|
+
<section
|
|
589
|
+
aria-label="Store access"
|
|
590
|
+
className="overflow-hidden rounded-card border shadow-xs border-line"
|
|
591
|
+
>
|
|
592
|
+
<header className="border-b border-line bg-surface px-3 py-2.5">
|
|
593
|
+
<h2 className="label">Store access</h2>
|
|
594
|
+
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
595
|
+
{access.operation ? (
|
|
596
|
+
<span className="chip uppercase text-accent">
|
|
597
|
+
{access.operation}
|
|
598
|
+
</span>
|
|
599
|
+
) : null}
|
|
600
|
+
<code className="mono break-all text-ink">
|
|
601
|
+
{access.keyspace ?? access.method ?? step.label ?? "repository call"}
|
|
602
|
+
</code>
|
|
603
|
+
</div>
|
|
604
|
+
</header>
|
|
605
|
+
|
|
606
|
+
<DetailSection title="Target">
|
|
607
|
+
<dl className="grid grid-cols-[minmax(6rem,auto)_minmax(0,1fr)] gap-x-4 gap-y-2">
|
|
608
|
+
<dt className="mono text-muted">Store</dt>
|
|
609
|
+
<dd>
|
|
610
|
+
{toStore ? (
|
|
611
|
+
<Link to={toStore} className="mono text-accent">
|
|
612
|
+
{access.store} →
|
|
613
|
+
</Link>
|
|
614
|
+
) : (
|
|
615
|
+
<Ident value={access.store} className="text-muted" />
|
|
616
|
+
)}
|
|
617
|
+
</dd>
|
|
618
|
+
{access.method ? (
|
|
619
|
+
<>
|
|
620
|
+
<dt className="mono text-muted">Repository</dt>
|
|
621
|
+
<dd className="mono text-ink">{access.method}</dd>
|
|
622
|
+
</>
|
|
623
|
+
) : null}
|
|
624
|
+
{aggregateId ? (
|
|
625
|
+
<>
|
|
626
|
+
<dt className="mono text-muted">Aggregate</dt>
|
|
627
|
+
<dd>
|
|
628
|
+
{toAggregate ? (
|
|
629
|
+
<Link to={toAggregate} className="mono text-accent">
|
|
630
|
+
{aggregateId} →
|
|
631
|
+
</Link>
|
|
632
|
+
) : (
|
|
633
|
+
<Ident value={aggregateId} className="text-muted" />
|
|
634
|
+
)}
|
|
635
|
+
</dd>
|
|
636
|
+
</>
|
|
637
|
+
) : null}
|
|
638
|
+
{keyspace?.value ? (
|
|
639
|
+
<>
|
|
640
|
+
<dt className="mono text-muted">Value</dt>
|
|
641
|
+
<dd className="mono text-ink">{keyspace.value}</dd>
|
|
642
|
+
</>
|
|
643
|
+
) : null}
|
|
644
|
+
{keyspace?.ttl ? (
|
|
645
|
+
<>
|
|
646
|
+
<dt className="mono text-muted">TTL</dt>
|
|
647
|
+
<dd className="mono text-ink">{keyspace.ttl}</dd>
|
|
648
|
+
</>
|
|
649
|
+
) : null}
|
|
650
|
+
</dl>
|
|
651
|
+
</DetailSection>
|
|
652
|
+
|
|
653
|
+
<DetailSection title="Repository call">
|
|
654
|
+
<Where step={step} flow={flow} />
|
|
655
|
+
</DetailSection>
|
|
656
|
+
{access.source ? (
|
|
657
|
+
<DetailSection title="Redis client call">
|
|
658
|
+
<SourceWhere where={access.source} flow={flow} />
|
|
659
|
+
</DetailSection>
|
|
660
|
+
) : null}
|
|
661
|
+
</section>
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
306
665
|
export function StepDetailBody({ step, flow }: { step: Step; flow: Flow }) {
|
|
307
666
|
const decisions = step.ref ? (index.adrsByEvent.get(step.ref) ?? []) : [];
|
|
308
667
|
|
|
309
668
|
return (
|
|
310
669
|
<>
|
|
311
|
-
<
|
|
312
|
-
<span>{step.from}</span>
|
|
313
|
-
<span>→</span>
|
|
314
|
-
<span>{step.to}</span>
|
|
315
|
-
</div>
|
|
316
|
-
|
|
317
|
-
<Frames step={step} flow={flow} />
|
|
670
|
+
<ExecutionContext step={step} flow={flow} />
|
|
318
671
|
|
|
319
672
|
{/* A decision that names this step's event is the reason the step
|
|
320
673
|
looks the way it does. It belongs next to the step, not three
|
|
@@ -341,6 +694,10 @@ export function StepDetailBody({ step, flow }: { step: Step; flow: Flow }) {
|
|
|
341
694
|
<EventDetail step={step} flow={flow} />
|
|
342
695
|
) : step.kind === "rpc" ? (
|
|
343
696
|
<RpcDetail step={step} flow={flow} />
|
|
697
|
+
) : step.kind === "response" ? (
|
|
698
|
+
<ResponseDetail step={step} flow={flow} />
|
|
699
|
+
) : step.storeAccess ? (
|
|
700
|
+
<StoreCallDetail step={step} flow={flow} />
|
|
344
701
|
) : (
|
|
345
702
|
<>
|
|
346
703
|
<div className="mono text-[13px]">
|
|
@@ -369,7 +726,10 @@ export function StepDetailBody({ step, flow }: { step: Step; flow: Flow }) {
|
|
|
369
726
|
</>
|
|
370
727
|
) : null}
|
|
371
728
|
|
|
372
|
-
{step.line &&
|
|
729
|
+
{step.line &&
|
|
730
|
+
step.kind !== "call" &&
|
|
731
|
+
step.kind !== "rpc" &&
|
|
732
|
+
step.kind !== "response" ? (
|
|
373
733
|
<>
|
|
374
734
|
<Label>Observed at</Label>
|
|
375
735
|
<Where step={step} flow={flow} />
|