@kingsleyweb/bun-ui 0.0.0-stage → 3.0.0-rc.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/LICENSE +21 -0
- package/README.md +80 -2
- package/app/docs/App.tsx +72 -0
- package/app/docs/api/client.ts +303 -0
- package/app/docs/api/contract.ts +24 -0
- package/app/docs/api/docs.ts +38 -0
- package/app/docs/api/errors.ts +144 -0
- package/app/docs/boot.tsx +58 -0
- package/app/docs/components/Badge.tsx +65 -0
- package/app/docs/components/Button.tsx +37 -0
- package/app/docs/components/Card.tsx +41 -0
- package/app/docs/components/ConfirmDialog.tsx +173 -0
- package/app/docs/components/CopyButton.tsx +68 -0
- package/app/docs/components/Dialog.tsx +230 -0
- package/app/docs/components/EmptyState.tsx +34 -0
- package/app/docs/components/ErrorView.tsx +70 -0
- package/app/docs/components/Field.tsx +93 -0
- package/app/docs/components/JsonEditor.tsx +151 -0
- package/app/docs/components/JsonView.tsx +338 -0
- package/app/docs/components/KeyValue.tsx +63 -0
- package/app/docs/components/ProblemBanner.tsx +116 -0
- package/app/docs/components/ScreenErrorBoundary.tsx +141 -0
- package/app/docs/components/Spinner.tsx +33 -0
- package/app/docs/components/Table.tsx +33 -0
- package/app/docs/components/Tabs.tsx +226 -0
- package/app/docs/components/classNames.ts +4 -0
- package/app/docs/components/components.css +691 -0
- package/app/docs/components/copy.ts +89 -0
- package/app/docs/components/fieldContext.ts +29 -0
- package/app/docs/components/inputValues.ts +142 -0
- package/app/docs/components/inputs.tsx +387 -0
- package/app/docs/components/jsonParse.ts +338 -0
- package/app/docs/components/listKeys.ts +30 -0
- package/app/docs/config.ts +86 -0
- package/app/docs/context.ts +33 -0
- package/app/docs/embed.ts +74 -0
- package/app/docs/env.d.ts +2 -0
- package/app/docs/format.ts +9 -0
- package/app/docs/hooks/useUrlTab.ts +16 -0
- package/app/docs/main.tsx +11 -0
- package/app/docs/queryClient.ts +54 -0
- package/app/docs/router.tsx +194 -0
- package/app/docs/routing.ts +194 -0
- package/app/docs/screens/docs/document.ts +27 -0
- package/app/docs/screens/docs/http/OperationView.tsx +407 -0
- package/app/docs/screens/docs/http/Sidebar.tsx +190 -0
- package/app/docs/screens/docs/http/TryItPanel.tsx +603 -0
- package/app/docs/screens/docs/http/http.css +405 -0
- package/app/docs/screens/docs/http/index.tsx +395 -0
- package/app/docs/screens/docs/http/model.ts +604 -0
- package/app/docs/screens/docs/http/tryIt.ts +455 -0
- package/app/docs/screens/docs/permissions.ts +6 -0
- package/app/docs/screens/docs/schema/Prose.tsx +43 -0
- package/app/docs/screens/docs/schema/SchemaTree.tsx +319 -0
- package/app/docs/screens/docs/schema/index.ts +30 -0
- package/app/docs/screens/docs/schema/resolve.ts +466 -0
- package/app/docs/screens/docs/schema/schema.css +137 -0
- package/app/docs/screens/docs/schema/skeleton.ts +92 -0
- package/app/docs/screens/docs/viewer.tsx +154 -0
- package/app/docs/screens/docs/ws/InlineText.tsx +25 -0
- package/app/docs/screens/docs/ws/PayloadSchema.tsx +39 -0
- package/app/docs/screens/docs/ws/format.ts +82 -0
- package/app/docs/screens/docs/ws/index.tsx +286 -0
- package/app/docs/screens/docs/ws/items.tsx +786 -0
- package/app/docs/screens/docs/ws/model.ts +1178 -0
- package/app/docs/screens/docs/ws/panels.tsx +440 -0
- package/app/docs/screens/docs/ws/ws.css +231 -0
- package/app/docs/shortcuts.ts +94 -0
- package/app/docs/styles/base.css +847 -0
- package/app/docs/styles/tokens.css +143 -0
- package/app/docs/theme.ts +56 -0
- package/app/kit/unauthorized.ts +222 -0
- package/dist/docs/assets/main-0rchxtv5.js +18 -0
- package/dist/docs/assets/main-0rchxtv5.js.map +99 -0
- package/dist/docs/assets/main-cqgyvs86.css +1 -0
- package/dist/docs/manifest.json +26 -0
- package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js +6 -0
- package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js.map +11 -0
- package/dist/jobs/assets/AddFlowDialog-n7gtc84v.css +1 -0
- package/dist/jobs/assets/added-cbwsxv3e.js +4 -0
- package/dist/jobs/assets/added-cbwsxv3e.js.map +11 -0
- package/dist/jobs/assets/bodies-yyw5hmrp.js +6 -0
- package/dist/jobs/assets/bodies-yyw5hmrp.js.map +10 -0
- package/dist/jobs/assets/channel-tgkvqm8y.js +4 -0
- package/dist/jobs/assets/channel-tgkvqm8y.js.map +10 -0
- package/dist/jobs/assets/channelChecks-s4yz4yhw.js +4 -0
- package/dist/jobs/assets/channelChecks-s4yz4yhw.js.map +10 -0
- package/dist/jobs/assets/index-02427smp.css +1 -0
- package/dist/jobs/assets/index-09z2cjft.js +4 -0
- package/dist/jobs/assets/index-09z2cjft.js.map +16 -0
- package/dist/jobs/assets/index-18bn0gvy.css +1 -0
- package/dist/jobs/assets/index-47pxmyxn.js +4 -0
- package/dist/jobs/assets/index-47pxmyxn.js.map +10 -0
- package/dist/jobs/assets/index-4e55z986.js +4 -0
- package/dist/jobs/assets/index-4e55z986.js.map +11 -0
- package/dist/jobs/assets/index-5c933424.js +4 -0
- package/dist/jobs/assets/index-5c933424.js.map +18 -0
- package/dist/jobs/assets/index-5kbyqspm.css +1 -0
- package/dist/jobs/assets/index-5ng37hk6.css +1 -0
- package/dist/jobs/assets/index-5w8w7fvc.css +1 -0
- package/dist/jobs/assets/index-71qs5qyx.css +1 -0
- package/dist/jobs/assets/index-btpg4r8b.css +1 -0
- package/dist/jobs/assets/index-d5r5n0mm.js +4 -0
- package/dist/jobs/assets/index-d5r5n0mm.js.map +23 -0
- package/dist/jobs/assets/index-e0yy2skq.css +1 -0
- package/dist/jobs/assets/index-hs60b25v.js +10 -0
- package/dist/jobs/assets/index-hs60b25v.js.map +62 -0
- package/dist/jobs/assets/index-k2smrr70.js +4 -0
- package/dist/jobs/assets/index-k2smrr70.js.map +9 -0
- package/dist/jobs/assets/index-q24jw3w9.js +4 -0
- package/dist/jobs/assets/index-q24jw3w9.js.map +32 -0
- package/dist/jobs/assets/index-qfb80xen.css +1 -0
- package/dist/jobs/assets/index-rtynx6wj.js +4 -0
- package/dist/jobs/assets/index-rtynx6wj.js.map +10 -0
- package/dist/jobs/assets/live-6k7s6358.js +4 -0
- package/dist/jobs/assets/live-6k7s6358.js.map +10 -0
- package/dist/jobs/assets/main-0jmeh6mq.js +6 -0
- package/dist/jobs/assets/main-0jmeh6mq.js.map +12 -0
- package/dist/jobs/assets/main-3yj6bv90.js +5 -0
- package/dist/jobs/assets/main-3yj6bv90.js.map +10 -0
- package/dist/jobs/assets/main-4g69fn3x.js +5 -0
- package/dist/jobs/assets/main-4g69fn3x.js.map +10 -0
- package/dist/jobs/assets/main-4gte3eqq.js +5 -0
- package/dist/jobs/assets/main-4gte3eqq.js.map +10 -0
- package/dist/jobs/assets/main-4j1fcevh.js +5 -0
- package/dist/jobs/assets/main-4j1fcevh.js.map +10 -0
- package/dist/jobs/assets/main-54qwkz28.css +1 -0
- package/dist/jobs/assets/main-5grjvsy7.js +5 -0
- package/dist/jobs/assets/main-5grjvsy7.js.map +11 -0
- package/dist/jobs/assets/main-5zbx60gq.js +5 -0
- package/dist/jobs/assets/main-5zbx60gq.js.map +11 -0
- package/dist/jobs/assets/main-9prgjhg4.js +5 -0
- package/dist/jobs/assets/main-9prgjhg4.js.map +10 -0
- package/dist/jobs/assets/main-a250ffk7.js +17 -0
- package/dist/jobs/assets/main-a250ffk7.js.map +115 -0
- package/dist/jobs/assets/main-d6ge7w08.js +5 -0
- package/dist/jobs/assets/main-d6ge7w08.js.map +10 -0
- package/dist/jobs/assets/main-gngzht43.js +5 -0
- package/dist/jobs/assets/main-gngzht43.js.map +10 -0
- package/dist/jobs/assets/main-hg0hrg8j.js +5 -0
- package/dist/jobs/assets/main-hg0hrg8j.js.map +10 -0
- package/dist/jobs/assets/main-hm1ng3yg.js +5 -0
- package/dist/jobs/assets/main-hm1ng3yg.js.map +12 -0
- package/dist/jobs/assets/main-jkvd92z9.js +5 -0
- package/dist/jobs/assets/main-jkvd92z9.js.map +10 -0
- package/dist/jobs/assets/main-jv5xqd13.js +5 -0
- package/dist/jobs/assets/main-jv5xqd13.js.map +21 -0
- package/dist/jobs/assets/main-jxre7w93.js +5 -0
- package/dist/jobs/assets/main-jxre7w93.js.map +23 -0
- package/dist/jobs/assets/main-kwmqkjfx.js +5 -0
- package/dist/jobs/assets/main-kwmqkjfx.js.map +11 -0
- package/dist/jobs/assets/main-n9cf6bxm.js +5 -0
- package/dist/jobs/assets/main-n9cf6bxm.js.map +11 -0
- package/dist/jobs/assets/main-pegp0ygb.js +5 -0
- package/dist/jobs/assets/main-pegp0ygb.js.map +10 -0
- package/dist/jobs/assets/main-qr573853.js +5 -0
- package/dist/jobs/assets/main-qr573853.js.map +12 -0
- package/dist/jobs/assets/main-rq97rdr4.js +4 -0
- package/dist/jobs/assets/main-rq97rdr4.js.map +22 -0
- package/dist/jobs/assets/main-szjdmydc.js +5 -0
- package/dist/jobs/assets/main-szjdmydc.js.map +13 -0
- package/dist/jobs/assets/main-tnbhrm9n.js +7 -0
- package/dist/jobs/assets/main-tnbhrm9n.js.map +11 -0
- package/dist/jobs/assets/main-ts19j4n0.js +5 -0
- package/dist/jobs/assets/main-ts19j4n0.js.map +25 -0
- package/dist/jobs/assets/main-w93x1dzs.js +5 -0
- package/dist/jobs/assets/main-w93x1dzs.js.map +10 -0
- package/dist/jobs/assets/sections-0ksdxs8m.css +1 -0
- package/dist/jobs/assets/sections-0y7w4fb4.js +4 -0
- package/dist/jobs/assets/sections-0y7w4fb4.js.map +10 -0
- package/dist/jobs/assets/sse-mn0t0fh7.js +5 -0
- package/dist/jobs/assets/sse-mn0t0fh7.js.map +11 -0
- package/dist/jobs/assets/sseChecks-t7tcm7ra.js +4 -0
- package/dist/jobs/assets/sseChecks-t7tcm7ra.js.map +10 -0
- package/dist/jobs/assets/validation-ysjfy072.js +4 -0
- package/dist/jobs/assets/validation-ysjfy072.js.map +9 -0
- package/dist/jobs/manifest.json +511 -0
- package/dts/docs/assets.d.ts +144 -0
- package/dts/docs/assets.d.ts.map +1 -0
- package/dts/docs/browser/config.d.ts +61 -0
- package/dts/docs/browser/config.d.ts.map +1 -0
- package/dts/docs/browser/index.d.ts +12 -0
- package/dts/docs/browser/index.d.ts.map +1 -0
- package/dts/docs/docsUi.d.ts +42 -0
- package/dts/docs/docsUi.d.ts.map +1 -0
- package/dts/docs/hosted.d.ts +22 -0
- package/dts/docs/hosted.d.ts.map +1 -0
- package/dts/docs/index.d.ts +18 -0
- package/dts/docs/index.d.ts.map +1 -0
- package/dts/docs/shell.d.ts +57 -0
- package/dts/docs/shell.d.ts.map +1 -0
- package/dts/docs/types.d.ts +137 -0
- package/dts/docs/types.d.ts.map +1 -0
- package/dts/index.d.ts +21 -0
- package/dts/index.d.ts.map +1 -0
- package/dts/jobs/assets.d.ts +157 -0
- package/dts/jobs/assets.d.ts.map +1 -0
- package/dts/jobs/browser/config.d.ts +67 -0
- package/dts/jobs/browser/config.d.ts.map +1 -0
- package/dts/jobs/browser/index.d.ts +10 -0
- package/dts/jobs/browser/index.d.ts.map +1 -0
- package/dts/jobs/browser/unauthorized.d.ts +9 -0
- package/dts/jobs/browser/unauthorized.d.ts.map +1 -0
- package/dts/jobs/index.d.ts +24 -0
- package/dts/jobs/index.d.ts.map +1 -0
- package/dts/jobs/jobsUi.d.ts +63 -0
- package/dts/jobs/jobsUi.d.ts.map +1 -0
- package/dts/jobs/shell.d.ts +77 -0
- package/dts/jobs/shell.d.ts.map +1 -0
- package/dts/jobs/types.d.ts +146 -0
- package/dts/jobs/types.d.ts.map +1 -0
- package/dts/kit/browser/shared.d.ts +10 -0
- package/dts/kit/browser/shared.d.ts.map +1 -0
- package/dts/kit/browser/unauthorized.d.ts +74 -0
- package/dts/kit/browser/unauthorized.d.ts.map +1 -0
- package/dts/kit/unauthorized.d.ts +47 -0
- package/dts/kit/unauthorized.d.ts.map +1 -0
- package/guides/docs.md +170 -0
- package/guides/jobs.md +1120 -0
- package/lib/docs/assets.ts +427 -0
- package/lib/docs/browser/config.ts +66 -0
- package/lib/docs/browser/index.ts +24 -0
- package/lib/docs/docsUi.ts +623 -0
- package/lib/docs/hosted.ts +31 -0
- package/lib/docs/index.ts +37 -0
- package/lib/docs/shell.ts +156 -0
- package/lib/docs/types.ts +149 -0
- package/lib/index.ts +27 -0
- package/lib/jobs/assets.ts +448 -0
- package/lib/jobs/browser/config.ts +65 -0
- package/lib/jobs/browser/index.ts +9 -0
- package/lib/jobs/browser/unauthorized.ts +11 -0
- package/lib/jobs/index.ts +41 -0
- package/lib/jobs/jobsUi.ts +709 -0
- package/lib/jobs/shell.ts +205 -0
- package/lib/jobs/types.ts +162 -0
- package/lib/kit/browser/shared.ts +11 -0
- package/lib/kit/browser/unauthorized.ts +114 -0
- package/lib/kit/unauthorized.ts +222 -0
- package/package.json +198 -4
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WebSocket reference's main pane for one item: a channel (with its
|
|
3
|
+
* try-it when a console is given), an operation, or a message (with its
|
|
4
|
+
* payload and, for an event, its try-it).
|
|
5
|
+
*
|
|
6
|
+
* The try-it deep-links a console the host app injects as {@link WsConsole}
|
|
7
|
+
* (the jobs UI passes its Events console); a standalone viewer passes none
|
|
8
|
+
* and no try-it renders.
|
|
9
|
+
*/
|
|
10
|
+
import type { ReactNode } from "react";
|
|
11
|
+
import type { SpecDocument } from "../../../api/docs";
|
|
12
|
+
import type { PermissionProbe } from "../permissions";
|
|
13
|
+
import type { LimitInfo } from "./format";
|
|
14
|
+
import type {
|
|
15
|
+
WsAddressOptions,
|
|
16
|
+
WsChannel,
|
|
17
|
+
WsDoc,
|
|
18
|
+
WsMessage,
|
|
19
|
+
WsOperation,
|
|
20
|
+
} from "./model";
|
|
21
|
+
import { useState } from "react";
|
|
22
|
+
import { Badge } from "../../../components/Badge";
|
|
23
|
+
import { Card } from "../../../components/Card";
|
|
24
|
+
import { CopyButton } from "../../../components/CopyButton";
|
|
25
|
+
import { Field } from "../../../components/Field";
|
|
26
|
+
import { TextInput } from "../../../components/inputs";
|
|
27
|
+
import { JsonView } from "../../../components/JsonView";
|
|
28
|
+
import { KeyValue } from "../../../components/KeyValue";
|
|
29
|
+
import { Table } from "../../../components/Table";
|
|
30
|
+
import { Link } from "../../../router";
|
|
31
|
+
import { Prose, SchemaTree } from "../schema";
|
|
32
|
+
import { InlineText } from "./InlineText";
|
|
33
|
+
import {
|
|
34
|
+
channelEventTypes,
|
|
35
|
+
channelPermissions,
|
|
36
|
+
channelsCarrying,
|
|
37
|
+
channelSubscribable,
|
|
38
|
+
eventPayloadSchema,
|
|
39
|
+
exampleEventPayload,
|
|
40
|
+
fillAddress,
|
|
41
|
+
itemPath,
|
|
42
|
+
operationsOn,
|
|
43
|
+
operationsUsing,
|
|
44
|
+
parameterProblem,
|
|
45
|
+
parameterSchemas,
|
|
46
|
+
tryProblems,
|
|
47
|
+
} from "./model";
|
|
48
|
+
import { CloseCodesPanel, LimitsPanel, RefusalsPanel } from "./panels";
|
|
49
|
+
import { PayloadSchema } from "./PayloadSchema";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A console the try-it cards deep-link (the jobs UI's Events console). The
|
|
53
|
+
* link builders answer `null` when the console cannot open what was asked.
|
|
54
|
+
*/
|
|
55
|
+
export interface WsConsole {
|
|
56
|
+
/** Whether the console can be opened at all. */
|
|
57
|
+
available: boolean;
|
|
58
|
+
/** Its name in the UI copy ("Events console"). */
|
|
59
|
+
label: string;
|
|
60
|
+
/** Why it cannot be opened, shown when `available` is false. Defaults to a generic sentence. */
|
|
61
|
+
unavailableReason?: ReactNode;
|
|
62
|
+
/** Why a filled channel still cannot open, when the link builder said no. Defaults to a generic sentence. */
|
|
63
|
+
noChannelReason?: (channel: string) => ReactNode;
|
|
64
|
+
/** The link subscribing the console to `channel`, filtered to `types` (`[]` would be all of them). */
|
|
65
|
+
channelLink: (channel: string, types: readonly string[]) => string | null;
|
|
66
|
+
/** The link opening the console on an event message's channel, filtered to its type. */
|
|
67
|
+
messageLink: (message: WsMessage, doc: WsDoc) => string | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Options of the panes, threaded from {@link WsReference}. */
|
|
71
|
+
export interface WsPaneOptions {
|
|
72
|
+
/** The console the try-it cards open, when the host has one. */
|
|
73
|
+
console?: WsConsole;
|
|
74
|
+
/** Answers "does the caller hold this permission" (the have/lack markers). */
|
|
75
|
+
probe?: PermissionProbe;
|
|
76
|
+
/** The host's contract for the try-it's address fields. */
|
|
77
|
+
address?: WsAddressOptions;
|
|
78
|
+
/** The docs section's path within the host app ("" for a standalone viewer). */
|
|
79
|
+
pathBase?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A permission with a have/lack marker when a probe is given, plain otherwise. */
|
|
83
|
+
export function PermissionMarker({
|
|
84
|
+
action,
|
|
85
|
+
probe,
|
|
86
|
+
}: {
|
|
87
|
+
action: string;
|
|
88
|
+
probe?: PermissionProbe;
|
|
89
|
+
}) {
|
|
90
|
+
if (probe === undefined) {
|
|
91
|
+
return (
|
|
92
|
+
<span
|
|
93
|
+
className="ws-permission"
|
|
94
|
+
data-testid={`ws-permission-${action}`}
|
|
95
|
+
>
|
|
96
|
+
<code>{action}</code> <Badge tone="neutral">permission</Badge>
|
|
97
|
+
</span>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
const held = probe(action);
|
|
101
|
+
return (
|
|
102
|
+
<span
|
|
103
|
+
className="ws-permission"
|
|
104
|
+
data-testid={`ws-permission-${action}`}
|
|
105
|
+
data-has={held === undefined ? "unknown" : String(held)}
|
|
106
|
+
>
|
|
107
|
+
<code>{action}</code>{" "}
|
|
108
|
+
{held === undefined ? (
|
|
109
|
+
<Badge tone="neutral">Not an action this UI knows</Badge>
|
|
110
|
+
) : (
|
|
111
|
+
<Badge tone={held ? "success" : "danger"}>
|
|
112
|
+
{held ? "You have this" : "You lack this"}
|
|
113
|
+
</Badge>
|
|
114
|
+
)}
|
|
115
|
+
</span>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Links to items by slug. */
|
|
120
|
+
function ItemLinks({
|
|
121
|
+
items,
|
|
122
|
+
pathBase = "",
|
|
123
|
+
empty = "None.",
|
|
124
|
+
}: {
|
|
125
|
+
items: readonly {
|
|
126
|
+
/** The item's slug. */
|
|
127
|
+
slug: string;
|
|
128
|
+
/** Its label. */
|
|
129
|
+
label: string;
|
|
130
|
+
}[];
|
|
131
|
+
/** The docs section's path within the host app. */
|
|
132
|
+
pathBase?: string;
|
|
133
|
+
empty?: string;
|
|
134
|
+
}) {
|
|
135
|
+
if (items.length === 0) {
|
|
136
|
+
return <span className="muted">{empty}</span>;
|
|
137
|
+
}
|
|
138
|
+
return (
|
|
139
|
+
<ul className="ws-links">
|
|
140
|
+
{items.map((item) => (
|
|
141
|
+
<li key={item.slug}>
|
|
142
|
+
<Link to={itemPath(item.slug, "", pathBase)}>
|
|
143
|
+
<code>{item.label}</code>
|
|
144
|
+
</Link>
|
|
145
|
+
</li>
|
|
146
|
+
))}
|
|
147
|
+
</ul>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Why "try it" is not offered, when the console is not available. */
|
|
152
|
+
function ConsoleUnavailable({ console: target }: { console: WsConsole }) {
|
|
153
|
+
return (
|
|
154
|
+
<p
|
|
155
|
+
className="muted"
|
|
156
|
+
data-testid="ws-try-unavailable"
|
|
157
|
+
>
|
|
158
|
+
{target.unavailableReason ?? (
|
|
159
|
+
<>The {target.label} is not available here.</>
|
|
160
|
+
)}
|
|
161
|
+
</p>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Props of {@link ChannelTryIt}. */
|
|
166
|
+
interface ChannelTryItProps {
|
|
167
|
+
/** The document. */
|
|
168
|
+
doc: WsDoc;
|
|
169
|
+
/** The channel family. */
|
|
170
|
+
channel: WsChannel;
|
|
171
|
+
/** The console to open. */
|
|
172
|
+
console: WsConsole;
|
|
173
|
+
/** The host's contract for the address fields. */
|
|
174
|
+
address?: WsAddressOptions;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Try a channel: fill its address, then open the console subscribed to it. */
|
|
178
|
+
function ChannelTryIt({
|
|
179
|
+
doc,
|
|
180
|
+
channel,
|
|
181
|
+
console: target,
|
|
182
|
+
address,
|
|
183
|
+
}: ChannelTryItProps) {
|
|
184
|
+
const [values, setValues] = useState<Record<string, string>>({});
|
|
185
|
+
if (!target.available) {
|
|
186
|
+
return (
|
|
187
|
+
<Card title="Try it">
|
|
188
|
+
<ConsoleUnavailable console={target} />
|
|
189
|
+
</Card>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const filled = fillAddress(
|
|
193
|
+
channel.address,
|
|
194
|
+
values,
|
|
195
|
+
parameterSchemas(channel.parameters),
|
|
196
|
+
address,
|
|
197
|
+
);
|
|
198
|
+
const types = channelEventTypes(doc, channel);
|
|
199
|
+
const link = filled === null ? null : target.channelLink(filled, types);
|
|
200
|
+
const problems = tryProblems(channel, values, address);
|
|
201
|
+
return (
|
|
202
|
+
<Card title="Try it">
|
|
203
|
+
<div
|
|
204
|
+
className="ws-try"
|
|
205
|
+
data-testid="ws-try-channel"
|
|
206
|
+
>
|
|
207
|
+
<p className="muted">
|
|
208
|
+
Opens the {target.label} subscribed to this channel
|
|
209
|
+
{types.length > 0
|
|
210
|
+
? `, showing the ${types.length} event types it carries`
|
|
211
|
+
: ""}
|
|
212
|
+
.
|
|
213
|
+
</p>
|
|
214
|
+
{channel.parameters.length > 0 && (
|
|
215
|
+
<div className="ws-try-fields">
|
|
216
|
+
{channel.parameters.map((parameter) => {
|
|
217
|
+
const value = values[parameter.name] ?? "";
|
|
218
|
+
const problem = parameterProblem(
|
|
219
|
+
parameter.name,
|
|
220
|
+
value,
|
|
221
|
+
parameter.schema,
|
|
222
|
+
address,
|
|
223
|
+
);
|
|
224
|
+
return (
|
|
225
|
+
<Field
|
|
226
|
+
key={parameter.name}
|
|
227
|
+
label={parameter.name}
|
|
228
|
+
hint={
|
|
229
|
+
parameter.description && (
|
|
230
|
+
<InlineText text={parameter.description} />
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
error={problem}
|
|
234
|
+
>
|
|
235
|
+
<TextInput
|
|
236
|
+
value={value}
|
|
237
|
+
onChange={(next) =>
|
|
238
|
+
setValues((current) => ({
|
|
239
|
+
...current,
|
|
240
|
+
[parameter.name]: next,
|
|
241
|
+
}))
|
|
242
|
+
}
|
|
243
|
+
autoComplete="off"
|
|
244
|
+
spellCheck={false}
|
|
245
|
+
/>
|
|
246
|
+
</Field>
|
|
247
|
+
);
|
|
248
|
+
})}
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
<p className="ws-try-channel">
|
|
252
|
+
Channel:{" "}
|
|
253
|
+
{filled === null ? (
|
|
254
|
+
<span className="muted">fill in every parameter</span>
|
|
255
|
+
) : (
|
|
256
|
+
<code data-testid="ws-try-address">{filled}</code>
|
|
257
|
+
)}
|
|
258
|
+
</p>
|
|
259
|
+
{link === null ? (
|
|
260
|
+
<>
|
|
261
|
+
<span
|
|
262
|
+
className="btn btn-primary ws-disabled"
|
|
263
|
+
aria-disabled="true"
|
|
264
|
+
aria-describedby={`ws-try-reason-${channel.key}`}
|
|
265
|
+
data-testid="ws-try-disabled"
|
|
266
|
+
>
|
|
267
|
+
Open in the {target.label}
|
|
268
|
+
</span>
|
|
269
|
+
<ul
|
|
270
|
+
className="ws-try-reason muted"
|
|
271
|
+
id={`ws-try-reason-${channel.key}`}
|
|
272
|
+
data-testid="ws-try-reason"
|
|
273
|
+
>
|
|
274
|
+
{problems.length > 0 ? (
|
|
275
|
+
problems.map((problem) => <li key={problem}>{problem}</li>)
|
|
276
|
+
) : (
|
|
277
|
+
<li>
|
|
278
|
+
{target.noChannelReason?.(filled ?? channel.address) ?? (
|
|
279
|
+
<>
|
|
280
|
+
The {target.label} cannot open{" "}
|
|
281
|
+
<code>{filled ?? channel.address}</code>.
|
|
282
|
+
</>
|
|
283
|
+
)}
|
|
284
|
+
</li>
|
|
285
|
+
)}
|
|
286
|
+
</ul>
|
|
287
|
+
</>
|
|
288
|
+
) : (
|
|
289
|
+
<Link
|
|
290
|
+
to={link}
|
|
291
|
+
className="btn btn-primary"
|
|
292
|
+
data-testid="ws-try-link"
|
|
293
|
+
>
|
|
294
|
+
Open in the {target.label}
|
|
295
|
+
</Link>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
</Card>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Props of {@link ChannelPane}. */
|
|
303
|
+
interface ChannelPaneProps extends WsPaneOptions {
|
|
304
|
+
/** The document, read. */
|
|
305
|
+
doc: WsDoc;
|
|
306
|
+
/** The raw document, for `$ref`s in parameter schemas. */
|
|
307
|
+
root: SpecDocument;
|
|
308
|
+
/** The channel. */
|
|
309
|
+
channel: WsChannel;
|
|
310
|
+
/** Labels and meanings for the limits, when the embedding viewer knows them. */
|
|
311
|
+
limitsInfo?: Readonly<Record<string, LimitInfo>>;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** A channel: address, parameters, operations, messages, permission and try-it. */
|
|
315
|
+
export function ChannelPane({
|
|
316
|
+
doc,
|
|
317
|
+
root,
|
|
318
|
+
channel,
|
|
319
|
+
limitsInfo,
|
|
320
|
+
console: target,
|
|
321
|
+
probe,
|
|
322
|
+
address,
|
|
323
|
+
pathBase,
|
|
324
|
+
}: ChannelPaneProps) {
|
|
325
|
+
const operations = operationsOn(doc, channel.key);
|
|
326
|
+
const permissions = channelPermissions(doc, channel.key);
|
|
327
|
+
const messages = channel.messages.map((key) => {
|
|
328
|
+
const message = doc.messages.find((candidate) => candidate.key === key);
|
|
329
|
+
return { slug: message?.slug ?? `message-${key}`, label: key };
|
|
330
|
+
});
|
|
331
|
+
return (
|
|
332
|
+
<div
|
|
333
|
+
className="ws-pane"
|
|
334
|
+
data-testid={`ws-pane-${channel.slug}`}
|
|
335
|
+
>
|
|
336
|
+
<Card
|
|
337
|
+
title={
|
|
338
|
+
<>
|
|
339
|
+
{channel.title} <Badge tone="info">channel</Badge>
|
|
340
|
+
</>
|
|
341
|
+
}
|
|
342
|
+
>
|
|
343
|
+
<KeyValue
|
|
344
|
+
items={[
|
|
345
|
+
{
|
|
346
|
+
label: channel.isConnection ? "Path" : "Address",
|
|
347
|
+
value: (
|
|
348
|
+
<code data-testid="ws-channel-address">{channel.address}</code>
|
|
349
|
+
),
|
|
350
|
+
hint: channel.isConnection
|
|
351
|
+
? "The socket itself: control messages travel here."
|
|
352
|
+
: "A logical channel: subscribe with this name over the connection.",
|
|
353
|
+
},
|
|
354
|
+
channel.bindingMethod !== undefined && {
|
|
355
|
+
label: "Upgrade",
|
|
356
|
+
value: <code>{channel.bindingMethod}</code>,
|
|
357
|
+
},
|
|
358
|
+
permissions.length > 0 && {
|
|
359
|
+
label: permissions.length === 1 ? "Permission" : "Permissions",
|
|
360
|
+
value: (
|
|
361
|
+
<span className="ws-permissions">
|
|
362
|
+
{permissions.map((action) => (
|
|
363
|
+
<PermissionMarker
|
|
364
|
+
key={action}
|
|
365
|
+
action={action}
|
|
366
|
+
probe={probe}
|
|
367
|
+
/>
|
|
368
|
+
))}
|
|
369
|
+
</span>
|
|
370
|
+
),
|
|
371
|
+
},
|
|
372
|
+
]}
|
|
373
|
+
/>
|
|
374
|
+
{channel.description && <Prose text={channel.description} />}
|
|
375
|
+
</Card>
|
|
376
|
+
{channel.parameters.length > 0 && (
|
|
377
|
+
<Card title="Parameters">
|
|
378
|
+
<Table label="Parameters">
|
|
379
|
+
<thead>
|
|
380
|
+
<tr>
|
|
381
|
+
<th scope="col">Name</th>
|
|
382
|
+
<th scope="col">Description</th>
|
|
383
|
+
<th scope="col">Value</th>
|
|
384
|
+
</tr>
|
|
385
|
+
</thead>
|
|
386
|
+
<tbody>
|
|
387
|
+
{channel.parameters.map((parameter) => (
|
|
388
|
+
<tr key={parameter.name}>
|
|
389
|
+
<td>
|
|
390
|
+
<code>{`{${parameter.name}}`}</code>
|
|
391
|
+
</td>
|
|
392
|
+
<td>
|
|
393
|
+
{parameter.description && (
|
|
394
|
+
<InlineText text={parameter.description} />
|
|
395
|
+
)}
|
|
396
|
+
</td>
|
|
397
|
+
<td data-testid={`ws-parameter-schema-${parameter.name}`}>
|
|
398
|
+
{parameter.schema ? (
|
|
399
|
+
<SchemaTree
|
|
400
|
+
schema={parameter.schema}
|
|
401
|
+
root={root}
|
|
402
|
+
label={`Schema of {${parameter.name}}`}
|
|
403
|
+
/>
|
|
404
|
+
) : (
|
|
405
|
+
<span className="muted">Any string</span>
|
|
406
|
+
)}
|
|
407
|
+
</td>
|
|
408
|
+
</tr>
|
|
409
|
+
))}
|
|
410
|
+
</tbody>
|
|
411
|
+
</Table>
|
|
412
|
+
</Card>
|
|
413
|
+
)}
|
|
414
|
+
<Card title="Operations">
|
|
415
|
+
<ItemLinks
|
|
416
|
+
items={operations.map((operation) => ({
|
|
417
|
+
slug: operation.slug,
|
|
418
|
+
label: `${operation.action ?? "?"} ${operation.key}`,
|
|
419
|
+
}))}
|
|
420
|
+
pathBase={pathBase}
|
|
421
|
+
/>
|
|
422
|
+
</Card>
|
|
423
|
+
<Card title={`Messages (${messages.length})`}>
|
|
424
|
+
<ItemLinks
|
|
425
|
+
items={messages}
|
|
426
|
+
pathBase={pathBase}
|
|
427
|
+
/>
|
|
428
|
+
</Card>
|
|
429
|
+
{/* Only a channel there is something to subscribe to, and only with a console: the connection has no try-it. */}
|
|
430
|
+
{target !== undefined && channelSubscribable(channel) && (
|
|
431
|
+
<ChannelTryIt
|
|
432
|
+
key={channel.key}
|
|
433
|
+
doc={doc}
|
|
434
|
+
channel={channel}
|
|
435
|
+
console={target}
|
|
436
|
+
address={address}
|
|
437
|
+
/>
|
|
438
|
+
)}
|
|
439
|
+
{channel.isConnection && doc.limits && (
|
|
440
|
+
<LimitsPanel
|
|
441
|
+
limits={doc.limits}
|
|
442
|
+
extensionKey={doc.extensionKeys.limits}
|
|
443
|
+
limitsInfo={limitsInfo}
|
|
444
|
+
/>
|
|
445
|
+
)}
|
|
446
|
+
{channel.isConnection && doc.closeCodes.length > 0 && (
|
|
447
|
+
<CloseCodesPanel doc={doc} />
|
|
448
|
+
)}
|
|
449
|
+
{channel.isConnection && doc.refusals.length > 0 && (
|
|
450
|
+
<RefusalsPanel doc={doc} />
|
|
451
|
+
)}
|
|
452
|
+
</div>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Message links for ids, labelled by id. */
|
|
457
|
+
function messageLinks(doc: WsDoc, ids: readonly string[]) {
|
|
458
|
+
return ids.map((key) => ({
|
|
459
|
+
slug:
|
|
460
|
+
doc.messages.find((message) => message.key === key)?.slug ??
|
|
461
|
+
`message-${key}`,
|
|
462
|
+
label: key,
|
|
463
|
+
}));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** An operation: direction, channel, messages, reply and permission. */
|
|
467
|
+
export function OperationPane({
|
|
468
|
+
doc,
|
|
469
|
+
operation,
|
|
470
|
+
probe,
|
|
471
|
+
pathBase,
|
|
472
|
+
}: WsPaneOptions & {
|
|
473
|
+
doc: WsDoc;
|
|
474
|
+
operation: WsOperation;
|
|
475
|
+
}) {
|
|
476
|
+
const channel = doc.channels.find(
|
|
477
|
+
(candidate) => candidate.key === operation.channel,
|
|
478
|
+
);
|
|
479
|
+
const replyChannel = doc.channels.find(
|
|
480
|
+
(candidate) => candidate.key === operation.reply?.channel,
|
|
481
|
+
);
|
|
482
|
+
return (
|
|
483
|
+
<div
|
|
484
|
+
className="ws-pane"
|
|
485
|
+
data-testid={`ws-pane-${operation.slug}`}
|
|
486
|
+
>
|
|
487
|
+
<Card
|
|
488
|
+
title={
|
|
489
|
+
<>
|
|
490
|
+
{operation.title} <Badge tone="accent">operation</Badge>
|
|
491
|
+
</>
|
|
492
|
+
}
|
|
493
|
+
>
|
|
494
|
+
<KeyValue
|
|
495
|
+
items={[
|
|
496
|
+
{
|
|
497
|
+
label: "Direction",
|
|
498
|
+
value: operation.action && (
|
|
499
|
+
<span data-testid="ws-operation-action">
|
|
500
|
+
<Badge
|
|
501
|
+
tone={operation.action === "send" ? "info" : "success"}
|
|
502
|
+
>
|
|
503
|
+
{operation.action}
|
|
504
|
+
</Badge>{" "}
|
|
505
|
+
{operation.action === "send"
|
|
506
|
+
? "client → server"
|
|
507
|
+
: "server → client"}
|
|
508
|
+
</span>
|
|
509
|
+
),
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
label: "Channel",
|
|
513
|
+
value: channel && (
|
|
514
|
+
<Link to={itemPath(channel.slug, "", pathBase)}>
|
|
515
|
+
<code>{channel.address}</code>
|
|
516
|
+
</Link>
|
|
517
|
+
),
|
|
518
|
+
},
|
|
519
|
+
operation.permission !== undefined && {
|
|
520
|
+
label: "Permission",
|
|
521
|
+
value: (
|
|
522
|
+
<PermissionMarker
|
|
523
|
+
action={operation.permission}
|
|
524
|
+
probe={probe}
|
|
525
|
+
/>
|
|
526
|
+
),
|
|
527
|
+
},
|
|
528
|
+
operation.summary !== undefined && {
|
|
529
|
+
label: "Summary",
|
|
530
|
+
value: <InlineText text={operation.summary} />,
|
|
531
|
+
},
|
|
532
|
+
]}
|
|
533
|
+
/>
|
|
534
|
+
</Card>
|
|
535
|
+
<Card title={operation.action === "send" ? "Sends" : "Receives"}>
|
|
536
|
+
<ItemLinks
|
|
537
|
+
items={messageLinks(doc, operation.messages)}
|
|
538
|
+
pathBase={pathBase}
|
|
539
|
+
/>
|
|
540
|
+
</Card>
|
|
541
|
+
{operation.reply && (
|
|
542
|
+
<Card title="Reply">
|
|
543
|
+
<div data-testid="ws-operation-reply">
|
|
544
|
+
<p className="muted">
|
|
545
|
+
The server answers on{" "}
|
|
546
|
+
{replyChannel ? (
|
|
547
|
+
<Link to={itemPath(replyChannel.slug, "", pathBase)}>
|
|
548
|
+
<code>{replyChannel.address}</code>
|
|
549
|
+
</Link>
|
|
550
|
+
) : (
|
|
551
|
+
<code>{operation.reply.channel ?? "?"}</code>
|
|
552
|
+
)}{" "}
|
|
553
|
+
with one of:
|
|
554
|
+
</p>
|
|
555
|
+
<ItemLinks
|
|
556
|
+
items={messageLinks(doc, operation.reply.messages)}
|
|
557
|
+
pathBase={pathBase}
|
|
558
|
+
/>
|
|
559
|
+
</div>
|
|
560
|
+
</Card>
|
|
561
|
+
)}
|
|
562
|
+
</div>
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Props of {@link MessagePane}. */
|
|
567
|
+
interface MessagePaneProps extends WsPaneOptions {
|
|
568
|
+
/** The document, read. */
|
|
569
|
+
doc: WsDoc;
|
|
570
|
+
/** The raw document, for `$ref`s. */
|
|
571
|
+
root: SpecDocument;
|
|
572
|
+
/** The message. */
|
|
573
|
+
message: WsMessage;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** A message: name, summary, where it travels, its payload schema, examples and (for an event) try-it. */
|
|
577
|
+
export function MessagePane({
|
|
578
|
+
doc,
|
|
579
|
+
root,
|
|
580
|
+
message,
|
|
581
|
+
console: target,
|
|
582
|
+
pathBase,
|
|
583
|
+
}: MessagePaneProps) {
|
|
584
|
+
const channels = channelsCarrying(doc, message.key);
|
|
585
|
+
const operations = operationsUsing(doc, message.key);
|
|
586
|
+
const eventPayload = message.event
|
|
587
|
+
? eventPayloadSchema(message.payload, root)
|
|
588
|
+
: undefined;
|
|
589
|
+
const link =
|
|
590
|
+
target !== undefined && message.event !== null
|
|
591
|
+
? target.messageLink(message, doc)
|
|
592
|
+
: null;
|
|
593
|
+
return (
|
|
594
|
+
<div
|
|
595
|
+
className="ws-pane"
|
|
596
|
+
data-testid={`ws-pane-${message.slug}`}
|
|
597
|
+
>
|
|
598
|
+
<Card
|
|
599
|
+
title={
|
|
600
|
+
<>
|
|
601
|
+
{message.title ?? message.name}{" "}
|
|
602
|
+
<Badge tone={message.event ? "success" : "neutral"}>
|
|
603
|
+
{message.event
|
|
604
|
+
? `${message.event.kind} event`
|
|
605
|
+
: "control message"}
|
|
606
|
+
</Badge>
|
|
607
|
+
</>
|
|
608
|
+
}
|
|
609
|
+
>
|
|
610
|
+
<KeyValue
|
|
611
|
+
items={[
|
|
612
|
+
{
|
|
613
|
+
label: "Name",
|
|
614
|
+
value: <code data-testid="ws-message-name">{message.name}</code>,
|
|
615
|
+
},
|
|
616
|
+
message.summary !== undefined && {
|
|
617
|
+
label: "Summary",
|
|
618
|
+
value: <InlineText text={message.summary} />,
|
|
619
|
+
},
|
|
620
|
+
message.contentType !== undefined && {
|
|
621
|
+
label: "Content type",
|
|
622
|
+
value: <code>{message.contentType}</code>,
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
label: "Channels",
|
|
626
|
+
value: (
|
|
627
|
+
<ItemLinks
|
|
628
|
+
items={channels.map((channel) => ({
|
|
629
|
+
slug: channel.slug,
|
|
630
|
+
label: channel.address,
|
|
631
|
+
}))}
|
|
632
|
+
pathBase={pathBase}
|
|
633
|
+
/>
|
|
634
|
+
),
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
label: "Operations",
|
|
638
|
+
value: (
|
|
639
|
+
<ItemLinks
|
|
640
|
+
items={operations.map((operation) => ({
|
|
641
|
+
slug: operation.slug,
|
|
642
|
+
label: operation.key,
|
|
643
|
+
}))}
|
|
644
|
+
pathBase={pathBase}
|
|
645
|
+
/>
|
|
646
|
+
),
|
|
647
|
+
},
|
|
648
|
+
]}
|
|
649
|
+
/>
|
|
650
|
+
</Card>
|
|
651
|
+
{message.event && eventPayload !== undefined && (
|
|
652
|
+
<Card
|
|
653
|
+
title="Event payload"
|
|
654
|
+
className="ws-highlight"
|
|
655
|
+
>
|
|
656
|
+
<p className="muted">
|
|
657
|
+
<code>event.payload</code>: what this {message.event.kind} event
|
|
658
|
+
says. It arrives inside the envelope below.
|
|
659
|
+
</p>
|
|
660
|
+
<div data-testid="ws-event-payload">
|
|
661
|
+
<PayloadSchema
|
|
662
|
+
schema={eventPayload}
|
|
663
|
+
root={root}
|
|
664
|
+
label={`Payload of ${message.name}`}
|
|
665
|
+
/>
|
|
666
|
+
</div>
|
|
667
|
+
</Card>
|
|
668
|
+
)}
|
|
669
|
+
<Card title={message.event ? "Full envelope" : "Payload"}>
|
|
670
|
+
<div data-testid="ws-message-payload">
|
|
671
|
+
<PayloadSchema
|
|
672
|
+
schema={message.payload}
|
|
673
|
+
root={root}
|
|
674
|
+
label={`Schema of ${message.name}`}
|
|
675
|
+
depth={message.event ? 1 : 2}
|
|
676
|
+
/>
|
|
677
|
+
</div>
|
|
678
|
+
</Card>
|
|
679
|
+
{message.examples.length > 0 && (
|
|
680
|
+
<Card title={message.examples.length === 1 ? "Example" : "Examples"}>
|
|
681
|
+
{message.examples.map((example, index) => (
|
|
682
|
+
<MessageExample
|
|
683
|
+
// eslint-disable-next-line react/no-array-index-key -- the document's examples, in its order
|
|
684
|
+
key={index}
|
|
685
|
+
example={example}
|
|
686
|
+
index={index}
|
|
687
|
+
isEvent={message.event !== null}
|
|
688
|
+
/>
|
|
689
|
+
))}
|
|
690
|
+
</Card>
|
|
691
|
+
)}
|
|
692
|
+
{message.event && target !== undefined && (
|
|
693
|
+
<Card title="Try it">
|
|
694
|
+
{!target.available ? (
|
|
695
|
+
<ConsoleUnavailable console={target} />
|
|
696
|
+
) : link === null ? (
|
|
697
|
+
<p className="muted">No channel in this document carries it.</p>
|
|
698
|
+
) : (
|
|
699
|
+
<div className="ws-try">
|
|
700
|
+
<p className="muted">
|
|
701
|
+
Opens the {target.label} on every {message.event.kind}, showing
|
|
702
|
+
only <code>{message.event.type}</code> events.
|
|
703
|
+
</p>
|
|
704
|
+
<Link
|
|
705
|
+
to={link}
|
|
706
|
+
className="btn btn-primary"
|
|
707
|
+
data-testid="ws-try-link"
|
|
708
|
+
>
|
|
709
|
+
Watch {message.event.type} events
|
|
710
|
+
</Link>
|
|
711
|
+
</div>
|
|
712
|
+
)}
|
|
713
|
+
</Card>
|
|
714
|
+
)}
|
|
715
|
+
</div>
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** Props of {@link MessageExample}. */
|
|
720
|
+
interface MessageExampleProps {
|
|
721
|
+
/** The example, as the document gives it. */
|
|
722
|
+
example: WsMessage["examples"][number];
|
|
723
|
+
/** Its position, naming it when it has no name. */
|
|
724
|
+
index: number;
|
|
725
|
+
/** Whether the message is an event frame (so its `event.payload` is shown first). */
|
|
726
|
+
isEvent: boolean;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* One message example: its name and summary, then the frame. For an event,
|
|
731
|
+
* the frame's `event.payload` (what the event says) comes first, open and
|
|
732
|
+
* highlighted, and the whole frame below it; the copy button copies the
|
|
733
|
+
* whole frame, exactly as it travels.
|
|
734
|
+
*/
|
|
735
|
+
function MessageExample({ example, index, isEvent }: MessageExampleProps) {
|
|
736
|
+
const name = example.name ?? `Example ${index + 1}`;
|
|
737
|
+
const eventPayload = isEvent
|
|
738
|
+
? exampleEventPayload(example.payload)
|
|
739
|
+
: undefined;
|
|
740
|
+
return (
|
|
741
|
+
<div
|
|
742
|
+
className="ws-example"
|
|
743
|
+
data-testid="ws-example"
|
|
744
|
+
>
|
|
745
|
+
<div className="ws-example-head">
|
|
746
|
+
<strong data-testid="ws-example-name">{name}</strong>
|
|
747
|
+
<CopyButton
|
|
748
|
+
text={() => JSON.stringify(example.payload, null, 2)}
|
|
749
|
+
label="Copy JSON"
|
|
750
|
+
ariaLabel={`Copy example ${name} as JSON`}
|
|
751
|
+
/>
|
|
752
|
+
</div>
|
|
753
|
+
{example.summary && (
|
|
754
|
+
<p
|
|
755
|
+
className="muted"
|
|
756
|
+
data-testid="ws-example-summary"
|
|
757
|
+
>
|
|
758
|
+
<InlineText text={example.summary} />
|
|
759
|
+
</p>
|
|
760
|
+
)}
|
|
761
|
+
{eventPayload !== undefined && (
|
|
762
|
+
<div
|
|
763
|
+
className="ws-example-event ws-highlight"
|
|
764
|
+
data-testid="ws-example-event-payload"
|
|
765
|
+
>
|
|
766
|
+
<p className="muted">
|
|
767
|
+
<code>event.payload</code>
|
|
768
|
+
</p>
|
|
769
|
+
<JsonView
|
|
770
|
+
value={eventPayload}
|
|
771
|
+
label={`event.payload of ${name}`}
|
|
772
|
+
expandDepth={4}
|
|
773
|
+
/>
|
|
774
|
+
</div>
|
|
775
|
+
)}
|
|
776
|
+
<div data-testid="ws-example-frame">
|
|
777
|
+
{eventPayload !== undefined && <p className="muted">The whole frame</p>}
|
|
778
|
+
<JsonView
|
|
779
|
+
value={example.payload}
|
|
780
|
+
label={`Example ${name}`}
|
|
781
|
+
expandDepth={eventPayload !== undefined ? 1 : 2}
|
|
782
|
+
/>
|
|
783
|
+
</div>
|
|
784
|
+
</div>
|
|
785
|
+
);
|
|
786
|
+
}
|