@dxfeed/dxlink-console-rpc 0.0.0-stage → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.mts +56 -0
- package/build/index.d.ts +56 -0
- package/build/index.js +806 -0
- package/build/index.js.map +1 -0
- package/build/index.mjs +775 -0
- package/build/index.mjs.map +1 -0
- package/package.json +84 -3
- package/README.md +0 -3
package/build/index.mjs
ADDED
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
// src/plugin.tsx
|
|
2
|
+
import { defineChannelPlugin } from "@dxfeed/dxlink-console-core";
|
|
3
|
+
import SwapHorizIcon2 from "@mui/icons-material/SwapHoriz";
|
|
4
|
+
|
|
5
|
+
// src/descriptors.ts
|
|
6
|
+
import {
|
|
7
|
+
createFileRegistry,
|
|
8
|
+
fromBinary,
|
|
9
|
+
fromJson,
|
|
10
|
+
ScalarType,
|
|
11
|
+
toJson
|
|
12
|
+
} from "@bufbuild/protobuf";
|
|
13
|
+
import { protoCamelCase } from "@bufbuild/protobuf/reflect";
|
|
14
|
+
import { FileDescriptorSetSchema } from "@bufbuild/protobuf/wkt";
|
|
15
|
+
var METHOD_MODELS = {
|
|
16
|
+
unary: {
|
|
17
|
+
label: "REQUEST_RESPONSE",
|
|
18
|
+
supported: true,
|
|
19
|
+
description: "One request, one response."
|
|
20
|
+
},
|
|
21
|
+
server_streaming: {
|
|
22
|
+
label: "REQUEST_STREAM",
|
|
23
|
+
supported: true,
|
|
24
|
+
description: "One request, a stream of responses."
|
|
25
|
+
},
|
|
26
|
+
bidi_streaming: {
|
|
27
|
+
label: "STREAM_STREAM",
|
|
28
|
+
supported: true,
|
|
29
|
+
description: "A stream of requests, a stream of responses."
|
|
30
|
+
},
|
|
31
|
+
client_streaming: {
|
|
32
|
+
label: "STREAM_RESPONSE",
|
|
33
|
+
supported: false,
|
|
34
|
+
description: "Not supported \u2014 the dxLink v1.0 wire has no graceful request half-close, so the server is never told the requests ended."
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var methodModel = (method) => METHOD_MODELS[method.methodKind];
|
|
38
|
+
var isMethodSupported = (method) => methodModel(method).supported;
|
|
39
|
+
var decodeDescriptorSet = (bytes) => {
|
|
40
|
+
const firstByte = bytes.find((byte) => byte > 32);
|
|
41
|
+
if (firstByte === 123) {
|
|
42
|
+
return fromJson(FileDescriptorSetSchema, JSON.parse(new TextDecoder().decode(bytes)));
|
|
43
|
+
}
|
|
44
|
+
return fromBinary(FileDescriptorSetSchema, bytes);
|
|
45
|
+
};
|
|
46
|
+
var deduceMissingJsonNames = (registry) => {
|
|
47
|
+
for (const desc of registry) {
|
|
48
|
+
if (desc.kind !== "message") continue;
|
|
49
|
+
for (const field of desc.fields) {
|
|
50
|
+
if (field.jsonName === "") {
|
|
51
|
+
;
|
|
52
|
+
field.jsonName = protoCamelCase(field.name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return registry;
|
|
57
|
+
};
|
|
58
|
+
var parseDescriptorSet = (bytes) => deduceMissingJsonNames(createFileRegistry(decodeDescriptorSet(bytes)));
|
|
59
|
+
var BINARY_ACCEPT = "application/protobuf, application/x-protobuf, application/octet-stream";
|
|
60
|
+
var fetchDescriptorSet = async (url) => {
|
|
61
|
+
let response;
|
|
62
|
+
try {
|
|
63
|
+
response = await fetch(url, { headers: { Accept: BINARY_ACCEPT } });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"the endpoint must be reachable, and one on another origin must allow cross-origin requests including the Accept header",
|
|
67
|
+
{ cause: error }
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
72
|
+
}
|
|
73
|
+
return parseDescriptorSet(new Uint8Array(await response.arrayBuffer()));
|
|
74
|
+
};
|
|
75
|
+
var readDescriptorSet = async (file) => parseDescriptorSet(new Uint8Array(await file.arrayBuffer()));
|
|
76
|
+
var listServices = (registry) => {
|
|
77
|
+
const services = [];
|
|
78
|
+
for (const desc of registry) {
|
|
79
|
+
if (desc.kind === "service") {
|
|
80
|
+
services.push(desc);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return services.sort((a, b) => a.typeName.localeCompare(b.typeName));
|
|
84
|
+
};
|
|
85
|
+
var fieldTemplate = (field) => {
|
|
86
|
+
switch (field.fieldKind) {
|
|
87
|
+
case "list":
|
|
88
|
+
return [];
|
|
89
|
+
case "map":
|
|
90
|
+
return {};
|
|
91
|
+
case "message":
|
|
92
|
+
return {};
|
|
93
|
+
case "enum":
|
|
94
|
+
return field.enum.values[0]?.name ?? 0;
|
|
95
|
+
case "scalar":
|
|
96
|
+
switch (field.scalar) {
|
|
97
|
+
case ScalarType.BOOL:
|
|
98
|
+
return false;
|
|
99
|
+
case ScalarType.STRING:
|
|
100
|
+
case ScalarType.BYTES:
|
|
101
|
+
return "";
|
|
102
|
+
case ScalarType.INT64:
|
|
103
|
+
case ScalarType.UINT64:
|
|
104
|
+
case ScalarType.FIXED64:
|
|
105
|
+
case ScalarType.SFIXED64:
|
|
106
|
+
case ScalarType.SINT64:
|
|
107
|
+
return "0";
|
|
108
|
+
default:
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var createRequestTemplate = (message) => Object.fromEntries(message.fields.map((field) => [field.jsonName, fieldTemplate(field)]));
|
|
114
|
+
var parseRequest = (message, json) => {
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = JSON.parse(json);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return { error: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}` };
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return { message: fromJson(message, parsed) };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var formatMessage = (schema, message) => toJson(schema, message);
|
|
128
|
+
|
|
129
|
+
// src/rpc-channel.tsx
|
|
130
|
+
import { useVM } from "@dxfeed/dxlink-console-core";
|
|
131
|
+
import { ChannelWidget } from "@dxfeed/dxlink-console-core";
|
|
132
|
+
import { useConnectionVM } from "@dxfeed/dxlink-console-core";
|
|
133
|
+
import SendIcon from "@mui/icons-material/Send";
|
|
134
|
+
import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
|
|
135
|
+
import Alert from "@mui/material/Alert";
|
|
136
|
+
import Box from "@mui/material/Box";
|
|
137
|
+
import Button from "@mui/material/Button";
|
|
138
|
+
import Chip from "@mui/material/Chip";
|
|
139
|
+
import Paper from "@mui/material/Paper";
|
|
140
|
+
import Stack from "@mui/material/Stack";
|
|
141
|
+
import TextField from "@mui/material/TextField";
|
|
142
|
+
import Typography from "@mui/material/Typography";
|
|
143
|
+
import { useEffect, useState } from "react";
|
|
144
|
+
|
|
145
|
+
// src/rpc-view-model.ts
|
|
146
|
+
import { DXLinkLogLevel } from "@dxfeed/dxlink-api";
|
|
147
|
+
import { prependError } from "@dxfeed/dxlink-console-core";
|
|
148
|
+
import { createDXLinkDynamicService } from "@dxfeed/dxlink-protobuf-es";
|
|
149
|
+
import { ReplaySubject } from "rxjs";
|
|
150
|
+
import { createStore } from "zustand/vanilla";
|
|
151
|
+
var FLUSH_INTERVAL_MS = 100;
|
|
152
|
+
var MAX_RESPONSES = 200;
|
|
153
|
+
var lastEntryId = 0;
|
|
154
|
+
var entry = (json) => {
|
|
155
|
+
lastEntryId += 1;
|
|
156
|
+
return { id: lastEntryId, time: (/* @__PURE__ */ new Date()).toLocaleTimeString(), json };
|
|
157
|
+
};
|
|
158
|
+
var toDXLinkError = (error) => {
|
|
159
|
+
if (typeof error === "object" && error !== null && "type" in error && "message" in error && typeof error.message === "string") {
|
|
160
|
+
return error;
|
|
161
|
+
}
|
|
162
|
+
return { type: "UNKNOWN", message: error instanceof Error ? error.message : String(error) };
|
|
163
|
+
};
|
|
164
|
+
var RpcViewModel = class {
|
|
165
|
+
constructor(client, params) {
|
|
166
|
+
this.store = createStore(() => ({
|
|
167
|
+
callState: "active",
|
|
168
|
+
responses: [],
|
|
169
|
+
requests: [],
|
|
170
|
+
errors: []
|
|
171
|
+
}));
|
|
172
|
+
this.subscription = null;
|
|
173
|
+
/** Request stream for bidirectional methods; null for the other models. */
|
|
174
|
+
this.input = null;
|
|
175
|
+
this.pending = [];
|
|
176
|
+
this.flushHandle = null;
|
|
177
|
+
this.start = () => {
|
|
178
|
+
if (this.subscription !== null) return;
|
|
179
|
+
this.store.setState({ callState: "active", responses: [], requests: [], errors: [] });
|
|
180
|
+
let responses;
|
|
181
|
+
try {
|
|
182
|
+
const service = createDXLinkDynamicService(this.client, this.service, {
|
|
183
|
+
// A descriptor chosen at runtime may declare methods the wire cannot carry; the
|
|
184
|
+
// picker never offers them, and the rest of the service stays callable.
|
|
185
|
+
skipUnsupportedMethods: true,
|
|
186
|
+
// A debug console wants the protocol traffic in the browser log.
|
|
187
|
+
logLevel: DXLinkLogLevel.DEBUG
|
|
188
|
+
});
|
|
189
|
+
const call = service[this.method.localName];
|
|
190
|
+
if (call === void 0) {
|
|
191
|
+
throw new Error(`${this.service.typeName} does not expose ${this.method.name}`);
|
|
192
|
+
}
|
|
193
|
+
if (this.isBidirectional) {
|
|
194
|
+
this.input = new ReplaySubject();
|
|
195
|
+
responses = call(this.input);
|
|
196
|
+
this.emit(this.request);
|
|
197
|
+
} else {
|
|
198
|
+
responses = call(this.request);
|
|
199
|
+
this.record("requests", formatMessage(this.method.input, this.request));
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
this.fail(error);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.subscription = responses.subscribe({
|
|
206
|
+
next: (message) => this.handleResponse(message),
|
|
207
|
+
error: (error) => this.fail(error),
|
|
208
|
+
complete: () => this.store.setState({ callState: "completed" })
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
this.stop = () => {
|
|
212
|
+
if (this.flushHandle !== null) {
|
|
213
|
+
clearTimeout(this.flushHandle);
|
|
214
|
+
this.flushHandle = null;
|
|
215
|
+
}
|
|
216
|
+
this.pending = [];
|
|
217
|
+
this.input?.complete();
|
|
218
|
+
this.input = null;
|
|
219
|
+
this.subscription?.unsubscribe();
|
|
220
|
+
this.subscription = null;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* Send another request on a bidirectional call. Rejected messages are reported as errors
|
|
224
|
+
* on this channel rather than thrown, so a typo in the editor cannot take the card down.
|
|
225
|
+
*/
|
|
226
|
+
this.send = (message) => {
|
|
227
|
+
if (!this.isBidirectional || this.input === null) return;
|
|
228
|
+
this.emit(message);
|
|
229
|
+
};
|
|
230
|
+
this.clearErrors = () => {
|
|
231
|
+
this.store.setState({ errors: [] });
|
|
232
|
+
};
|
|
233
|
+
this.close = () => {
|
|
234
|
+
this.stop();
|
|
235
|
+
};
|
|
236
|
+
this.dispose = () => {
|
|
237
|
+
this.stop();
|
|
238
|
+
};
|
|
239
|
+
this.emit = (message) => {
|
|
240
|
+
try {
|
|
241
|
+
this.input?.next(message);
|
|
242
|
+
this.record("requests", formatMessage(this.method.input, message));
|
|
243
|
+
} catch (error) {
|
|
244
|
+
this.fail(error);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
this.fail = (error) => {
|
|
248
|
+
this.store.setState((state) => ({
|
|
249
|
+
callState: "failed",
|
|
250
|
+
errors: prependError(state.errors, toDXLinkError(error))
|
|
251
|
+
}));
|
|
252
|
+
};
|
|
253
|
+
this.handleResponse = (message) => {
|
|
254
|
+
let json;
|
|
255
|
+
try {
|
|
256
|
+
json = formatMessage(this.method.output, message);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
this.fail(error);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.pending = [entry(json), ...this.pending];
|
|
262
|
+
if (this.flushHandle === null) {
|
|
263
|
+
this.flushHandle = setTimeout(this.flush, FLUSH_INTERVAL_MS);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
this.flush = () => {
|
|
267
|
+
this.flushHandle = null;
|
|
268
|
+
const pending = this.pending;
|
|
269
|
+
if (pending.length === 0) return;
|
|
270
|
+
this.pending = [];
|
|
271
|
+
this.store.setState((state) => ({
|
|
272
|
+
responses: [...pending, ...state.responses].slice(0, MAX_RESPONSES)
|
|
273
|
+
}));
|
|
274
|
+
};
|
|
275
|
+
this.record = (key, json) => {
|
|
276
|
+
this.store.setState((state) => ({
|
|
277
|
+
[key]: [entry(json), ...state[key]].slice(0, MAX_RESPONSES)
|
|
278
|
+
}));
|
|
279
|
+
};
|
|
280
|
+
this.client = client;
|
|
281
|
+
this.service = params.service;
|
|
282
|
+
this.method = params.method;
|
|
283
|
+
this.request = params.request;
|
|
284
|
+
}
|
|
285
|
+
get isBidirectional() {
|
|
286
|
+
return this.method.methodKind === "bidi_streaming";
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// src/rpc-channel.tsx
|
|
291
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
292
|
+
var RpcStatusChip = ({ state, responses }) => {
|
|
293
|
+
if (state === "failed") {
|
|
294
|
+
return /* @__PURE__ */ jsx(Chip, { size: "small", color: "error", variant: "outlined", label: "failed" });
|
|
295
|
+
}
|
|
296
|
+
if (state === "completed") {
|
|
297
|
+
return /* @__PURE__ */ jsx(Chip, { size: "small", variant: "outlined", label: "completed" });
|
|
298
|
+
}
|
|
299
|
+
return responses === 0 ? /* @__PURE__ */ jsx(Chip, { size: "small", color: "warning", variant: "outlined", label: "waiting" }) : /* @__PURE__ */ jsx(Chip, { size: "small", color: "success", variant: "outlined", label: "streaming" });
|
|
300
|
+
};
|
|
301
|
+
var MessageEntry = ({ entry: entry2 }) => /* @__PURE__ */ jsxs(Paper, { variant: "outlined", sx: { p: 1.5 }, children: [
|
|
302
|
+
/* @__PURE__ */ jsx(Typography, { variant: "caption", color: "text.secondary", children: entry2.time }),
|
|
303
|
+
/* @__PURE__ */ jsx(
|
|
304
|
+
Box,
|
|
305
|
+
{
|
|
306
|
+
component: "pre",
|
|
307
|
+
sx: { m: 0, mt: 0.5, fontSize: 13, whiteSpace: "pre-wrap", wordBreak: "break-word" },
|
|
308
|
+
children: JSON.stringify(entry2.json, null, 2)
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
] });
|
|
312
|
+
var MessageList = ({
|
|
313
|
+
title,
|
|
314
|
+
entries,
|
|
315
|
+
empty
|
|
316
|
+
}) => /* @__PURE__ */ jsxs(Stack, { spacing: 1, children: [
|
|
317
|
+
/* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, sx: { alignItems: "baseline" }, children: [
|
|
318
|
+
/* @__PURE__ */ jsx(Typography, { variant: "subtitle2", children: title }),
|
|
319
|
+
/* @__PURE__ */ jsx(Typography, { variant: "caption", color: "text.secondary", children: entries.length })
|
|
320
|
+
] }),
|
|
321
|
+
entries.length === 0 ? /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", children: empty }) : entries.map((entry2) => /* @__PURE__ */ jsx(MessageEntry, { entry: entry2 }, entry2.id))
|
|
322
|
+
] });
|
|
323
|
+
var SendRequest = ({
|
|
324
|
+
vm,
|
|
325
|
+
config,
|
|
326
|
+
active
|
|
327
|
+
}) => {
|
|
328
|
+
const [json, setJson] = useState(
|
|
329
|
+
() => JSON.stringify(createRequestTemplate(config.method.input), null, 2)
|
|
330
|
+
);
|
|
331
|
+
const parsed = parseRequest(config.method.input, json);
|
|
332
|
+
return /* @__PURE__ */ jsxs(Stack, { spacing: 1, children: [
|
|
333
|
+
/* @__PURE__ */ jsx(Typography, { variant: "subtitle2", children: "Send another request" }),
|
|
334
|
+
/* @__PURE__ */ jsx(
|
|
335
|
+
TextField,
|
|
336
|
+
{
|
|
337
|
+
value: json,
|
|
338
|
+
onChange: (e) => setJson(e.target.value),
|
|
339
|
+
multiline: true,
|
|
340
|
+
minRows: 4,
|
|
341
|
+
fullWidth: true,
|
|
342
|
+
size: "small",
|
|
343
|
+
slotProps: { input: { sx: { fontFamily: "monospace", fontSize: 13 } } }
|
|
344
|
+
}
|
|
345
|
+
),
|
|
346
|
+
"error" in parsed && /* @__PURE__ */ jsx(Alert, { severity: "error", children: parsed.error }),
|
|
347
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
|
|
348
|
+
Button,
|
|
349
|
+
{
|
|
350
|
+
variant: "contained",
|
|
351
|
+
startIcon: /* @__PURE__ */ jsx(SendIcon, {}),
|
|
352
|
+
disabled: !active || "error" in parsed,
|
|
353
|
+
onClick: () => {
|
|
354
|
+
if ("message" in parsed) vm.send(parsed.message);
|
|
355
|
+
},
|
|
356
|
+
children: "Send"
|
|
357
|
+
}
|
|
358
|
+
) })
|
|
359
|
+
] });
|
|
360
|
+
};
|
|
361
|
+
var RpcChannel = ({ title, config }) => {
|
|
362
|
+
const connectionVM = useConnectionVM();
|
|
363
|
+
const [vm] = useState(() => {
|
|
364
|
+
const client = connectionVM.getClient();
|
|
365
|
+
if (client === null) {
|
|
366
|
+
throw new Error("RPC channel opened without an active connection");
|
|
367
|
+
}
|
|
368
|
+
return new RpcViewModel(client, {
|
|
369
|
+
service: config.service,
|
|
370
|
+
method: config.method,
|
|
371
|
+
request: config.request
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
useEffect(() => {
|
|
375
|
+
vm.start();
|
|
376
|
+
return () => vm.stop();
|
|
377
|
+
}, [vm]);
|
|
378
|
+
const callState = useVM(vm, (s) => s.callState);
|
|
379
|
+
const responses = useVM(vm, (s) => s.responses);
|
|
380
|
+
const requests = useVM(vm, (s) => s.requests);
|
|
381
|
+
const errors = useVM(vm, (s) => s.errors);
|
|
382
|
+
const model = methodModel(config.method);
|
|
383
|
+
return /* @__PURE__ */ jsx(
|
|
384
|
+
ChannelWidget,
|
|
385
|
+
{
|
|
386
|
+
icon: /* @__PURE__ */ jsx(SwapHorizIcon, {}),
|
|
387
|
+
title,
|
|
388
|
+
subtitle: `${config.service.typeName} \xB7 ${config.method.name}`,
|
|
389
|
+
onClose: vm.close,
|
|
390
|
+
status: /* @__PURE__ */ jsx(RpcStatusChip, { state: callState, responses: responses.length }),
|
|
391
|
+
parameters: { service: config.service.typeName, methodName: config.method.name },
|
|
392
|
+
errors,
|
|
393
|
+
onClearErrors: vm.clearErrors,
|
|
394
|
+
children: /* @__PURE__ */ jsxs(Stack, { spacing: 2, children: [
|
|
395
|
+
/* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 0.5, useFlexGap: true, sx: { flexWrap: "wrap" }, children: [
|
|
396
|
+
/* @__PURE__ */ jsx(Chip, { size: "small", variant: "outlined", label: model.label }),
|
|
397
|
+
/* @__PURE__ */ jsx(Chip, { size: "small", variant: "outlined", label: `in: ${config.method.input.typeName}` }),
|
|
398
|
+
/* @__PURE__ */ jsx(Chip, { size: "small", variant: "outlined", label: `out: ${config.method.output.typeName}` })
|
|
399
|
+
] }),
|
|
400
|
+
vm.isBidirectional && /* @__PURE__ */ jsx(SendRequest, { vm, config, active: callState === "active" }),
|
|
401
|
+
/* @__PURE__ */ jsx(MessageList, { title: "Sent", entries: requests, empty: "Nothing sent yet." }),
|
|
402
|
+
/* @__PURE__ */ jsx(
|
|
403
|
+
MessageList,
|
|
404
|
+
{
|
|
405
|
+
title: "Received",
|
|
406
|
+
entries: responses,
|
|
407
|
+
empty: callState === "completed" ? "The server closed the channel without a response." : "Waiting for the first response\u2026"
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
] })
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// src/rpc-channel-request.tsx
|
|
416
|
+
import { ScalarType as ScalarType2 } from "@bufbuild/protobuf";
|
|
417
|
+
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
418
|
+
import UploadFileIcon from "@mui/icons-material/UploadFile";
|
|
419
|
+
import Accordion from "@mui/material/Accordion";
|
|
420
|
+
import AccordionDetails from "@mui/material/AccordionDetails";
|
|
421
|
+
import AccordionSummary from "@mui/material/AccordionSummary";
|
|
422
|
+
import Alert2 from "@mui/material/Alert";
|
|
423
|
+
import Box2 from "@mui/material/Box";
|
|
424
|
+
import Button2 from "@mui/material/Button";
|
|
425
|
+
import Chip2 from "@mui/material/Chip";
|
|
426
|
+
import FormControlLabel from "@mui/material/FormControlLabel";
|
|
427
|
+
import MenuItem from "@mui/material/MenuItem";
|
|
428
|
+
import Stack2 from "@mui/material/Stack";
|
|
429
|
+
import Switch from "@mui/material/Switch";
|
|
430
|
+
import TextField2 from "@mui/material/TextField";
|
|
431
|
+
import Typography2 from "@mui/material/Typography";
|
|
432
|
+
import { useMemo, useRef, useState as useState2 } from "react";
|
|
433
|
+
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
434
|
+
var stringify = (json) => JSON.stringify(json, null, 2);
|
|
435
|
+
var isEditableField = (field) => (field.fieldKind === "scalar" || field.fieldKind === "enum") && field.oneof === void 0;
|
|
436
|
+
var STRING_SCALARS = /* @__PURE__ */ new Set([
|
|
437
|
+
ScalarType2.STRING,
|
|
438
|
+
ScalarType2.BYTES,
|
|
439
|
+
ScalarType2.INT64,
|
|
440
|
+
ScalarType2.UINT64,
|
|
441
|
+
ScalarType2.FIXED64,
|
|
442
|
+
ScalarType2.SFIXED64,
|
|
443
|
+
ScalarType2.SINT64
|
|
444
|
+
]);
|
|
445
|
+
var scalarHelp = (scalar) => {
|
|
446
|
+
if (scalar === ScalarType2.BYTES) return "bytes \u2014 base64";
|
|
447
|
+
if (STRING_SCALARS.has(scalar) && scalar !== ScalarType2.STRING) return "64-bit \u2014 sent as a string";
|
|
448
|
+
return void 0;
|
|
449
|
+
};
|
|
450
|
+
var RequestFields = ({
|
|
451
|
+
message,
|
|
452
|
+
json,
|
|
453
|
+
onChange
|
|
454
|
+
}) => {
|
|
455
|
+
const parsed = useMemo(() => {
|
|
456
|
+
try {
|
|
457
|
+
const value = JSON.parse(json);
|
|
458
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
459
|
+
} catch {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
}, [json]);
|
|
463
|
+
const fields = message.fields.filter(isEditableField);
|
|
464
|
+
if (fields.length === 0) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
if (parsed === null) {
|
|
468
|
+
return /* @__PURE__ */ jsx2(Typography2, { variant: "body2", color: "text.secondary", children: "Fields are editable again once the request below is a JSON object." });
|
|
469
|
+
}
|
|
470
|
+
const set = (field, fieldValue) => onChange(stringify({ ...parsed, [field.jsonName]: fieldValue }));
|
|
471
|
+
return /* @__PURE__ */ jsx2(Box2, { sx: { display: "grid", gap: 2, gridTemplateColumns: { xs: "1fr", sm: "1fr 1fr" } }, children: fields.map((field) => {
|
|
472
|
+
const current = parsed[field.jsonName];
|
|
473
|
+
if (field.fieldKind === "enum") {
|
|
474
|
+
return /* @__PURE__ */ jsx2(
|
|
475
|
+
TextField2,
|
|
476
|
+
{
|
|
477
|
+
select: true,
|
|
478
|
+
label: field.name,
|
|
479
|
+
value: typeof current === "string" ? current : "",
|
|
480
|
+
onChange: (e) => set(field, e.target.value),
|
|
481
|
+
size: "small",
|
|
482
|
+
helperText: field.enum.typeName,
|
|
483
|
+
children: field.enum.values.map((enumValue) => /* @__PURE__ */ jsx2(MenuItem, { value: enumValue.name, children: enumValue.name }, enumValue.number))
|
|
484
|
+
},
|
|
485
|
+
field.number
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
if (field.fieldKind !== "scalar") {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
if (field.scalar === ScalarType2.BOOL) {
|
|
492
|
+
return /* @__PURE__ */ jsx2(
|
|
493
|
+
FormControlLabel,
|
|
494
|
+
{
|
|
495
|
+
control: /* @__PURE__ */ jsx2(
|
|
496
|
+
Switch,
|
|
497
|
+
{
|
|
498
|
+
checked: current === true,
|
|
499
|
+
onChange: (e) => set(field, e.target.checked),
|
|
500
|
+
size: "small"
|
|
501
|
+
}
|
|
502
|
+
),
|
|
503
|
+
label: field.name
|
|
504
|
+
},
|
|
505
|
+
field.number
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
if (STRING_SCALARS.has(field.scalar)) {
|
|
509
|
+
return /* @__PURE__ */ jsx2(
|
|
510
|
+
TextField2,
|
|
511
|
+
{
|
|
512
|
+
label: field.name,
|
|
513
|
+
value: typeof current === "string" ? current : "",
|
|
514
|
+
onChange: (e) => set(field, e.target.value),
|
|
515
|
+
size: "small",
|
|
516
|
+
helperText: scalarHelp(field.scalar)
|
|
517
|
+
},
|
|
518
|
+
field.number
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return /* @__PURE__ */ jsx2(
|
|
522
|
+
TextField2,
|
|
523
|
+
{
|
|
524
|
+
label: field.name,
|
|
525
|
+
type: "number",
|
|
526
|
+
value: typeof current === "number" ? String(current) : "",
|
|
527
|
+
onChange: (e) => set(field, e.target.value === "" ? 0 : Number(e.target.value)),
|
|
528
|
+
size: "small"
|
|
529
|
+
},
|
|
530
|
+
field.number
|
|
531
|
+
);
|
|
532
|
+
}) });
|
|
533
|
+
};
|
|
534
|
+
var RpcChannelRequest = ({ value, onChange, urlLocked }) => {
|
|
535
|
+
const [loading, setLoading] = useState2(false);
|
|
536
|
+
const [loadError, setLoadError] = useState2(null);
|
|
537
|
+
const fileInput = useRef(null);
|
|
538
|
+
const services = useMemo(
|
|
539
|
+
() => value.registry === null ? [] : listServices(value.registry),
|
|
540
|
+
[value.registry]
|
|
541
|
+
);
|
|
542
|
+
const service = services.find((s) => s.typeName === value.serviceName);
|
|
543
|
+
const method = service?.method[value.methodName];
|
|
544
|
+
const parsedRequest = useMemo(
|
|
545
|
+
() => method === void 0 ? null : parseRequest(method.input, value.json),
|
|
546
|
+
[method, value.json]
|
|
547
|
+
);
|
|
548
|
+
const selectMethod = (next, patch = {}) => onChange({
|
|
549
|
+
...value,
|
|
550
|
+
...patch,
|
|
551
|
+
methodName: next?.localName ?? "",
|
|
552
|
+
json: next === void 0 ? "{}" : stringify(createRequestTemplate(next.input))
|
|
553
|
+
});
|
|
554
|
+
const selectService = (next) => selectMethod(next?.methods.find(isMethodSupported), { serviceName: next?.typeName ?? "" });
|
|
555
|
+
const load = async (source, loader) => {
|
|
556
|
+
setLoading(true);
|
|
557
|
+
setLoadError(null);
|
|
558
|
+
try {
|
|
559
|
+
await loader();
|
|
560
|
+
} catch (error) {
|
|
561
|
+
setLoadError(
|
|
562
|
+
`Could not load definitions from ${source}: ${error instanceof Error ? error.message : String(error)}`
|
|
563
|
+
);
|
|
564
|
+
} finally {
|
|
565
|
+
setLoading(false);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
const applyRegistry = (registry, source) => {
|
|
569
|
+
const firstService = listServices(registry)[0];
|
|
570
|
+
const firstMethod = firstService?.methods.find(isMethodSupported);
|
|
571
|
+
onChange({
|
|
572
|
+
...value,
|
|
573
|
+
registry,
|
|
574
|
+
source,
|
|
575
|
+
serviceName: firstService?.typeName ?? "",
|
|
576
|
+
methodName: firstMethod?.localName ?? "",
|
|
577
|
+
json: firstMethod === void 0 ? "{}" : stringify(createRequestTemplate(firstMethod.input))
|
|
578
|
+
});
|
|
579
|
+
};
|
|
580
|
+
const loadUrl = () => load(value.url, async () => {
|
|
581
|
+
applyRegistry(await fetchDescriptorSet(value.url), value.url);
|
|
582
|
+
});
|
|
583
|
+
const loadFile = (file) => load(file.name, async () => {
|
|
584
|
+
applyRegistry(await readDescriptorSet(file), file.name);
|
|
585
|
+
});
|
|
586
|
+
return /* @__PURE__ */ jsxs2(Stack2, { spacing: 2.5, sx: { pt: 1 }, children: [
|
|
587
|
+
/* @__PURE__ */ jsxs2(Stack2, { spacing: 1.5, children: [
|
|
588
|
+
/* @__PURE__ */ jsx2(Typography2, { variant: "subtitle2", children: "Service definitions" }),
|
|
589
|
+
/* @__PURE__ */ jsxs2(Stack2, { direction: "row", spacing: 1, sx: { alignItems: "flex-start" }, children: [
|
|
590
|
+
/* @__PURE__ */ jsx2(
|
|
591
|
+
TextField2,
|
|
592
|
+
{
|
|
593
|
+
label: "Descriptor set URL",
|
|
594
|
+
value: value.url,
|
|
595
|
+
onChange: (e) => onChange({ ...value, url: e.target.value }),
|
|
596
|
+
disabled: urlLocked,
|
|
597
|
+
size: "small",
|
|
598
|
+
fullWidth: true,
|
|
599
|
+
helperText: urlLocked ? "Fixed by this deployment." : "An endpoint serving a FileDescriptorSet, binary or protobuf-JSON."
|
|
600
|
+
}
|
|
601
|
+
),
|
|
602
|
+
/* @__PURE__ */ jsx2(
|
|
603
|
+
Button2,
|
|
604
|
+
{
|
|
605
|
+
variant: "outlined",
|
|
606
|
+
onClick: () => void loadUrl(),
|
|
607
|
+
disabled: loading || value.url.trim() === "",
|
|
608
|
+
sx: { mt: 0.25 },
|
|
609
|
+
children: "Load"
|
|
610
|
+
}
|
|
611
|
+
)
|
|
612
|
+
] }),
|
|
613
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
614
|
+
/* @__PURE__ */ jsx2(
|
|
615
|
+
Button2,
|
|
616
|
+
{
|
|
617
|
+
size: "small",
|
|
618
|
+
startIcon: /* @__PURE__ */ jsx2(UploadFileIcon, {}),
|
|
619
|
+
onClick: () => fileInput.current?.click(),
|
|
620
|
+
disabled: loading,
|
|
621
|
+
children: "Load from file"
|
|
622
|
+
}
|
|
623
|
+
),
|
|
624
|
+
/* @__PURE__ */ jsx2(
|
|
625
|
+
"input",
|
|
626
|
+
{
|
|
627
|
+
ref: fileInput,
|
|
628
|
+
type: "file",
|
|
629
|
+
hidden: true,
|
|
630
|
+
accept: ".binpb,.bin,.pb,.json",
|
|
631
|
+
onChange: (e) => {
|
|
632
|
+
const file = e.target.files?.[0];
|
|
633
|
+
e.target.value = "";
|
|
634
|
+
if (file !== void 0) void loadFile(file);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
)
|
|
638
|
+
] }),
|
|
639
|
+
loadError !== null && /* @__PURE__ */ jsx2(Alert2, { severity: "error", children: loadError }),
|
|
640
|
+
value.registry !== null && loadError === null && /* @__PURE__ */ jsxs2(Typography2, { variant: "caption", color: "text.secondary", children: [
|
|
641
|
+
services.length,
|
|
642
|
+
" service",
|
|
643
|
+
services.length === 1 ? "" : "s",
|
|
644
|
+
" from ",
|
|
645
|
+
value.source
|
|
646
|
+
] })
|
|
647
|
+
] }),
|
|
648
|
+
value.registry !== null && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
649
|
+
/* @__PURE__ */ jsx2(
|
|
650
|
+
TextField2,
|
|
651
|
+
{
|
|
652
|
+
select: true,
|
|
653
|
+
label: "Service",
|
|
654
|
+
value: service?.typeName ?? "",
|
|
655
|
+
onChange: (e) => selectService(services.find((s) => s.typeName === e.target.value)),
|
|
656
|
+
size: "small",
|
|
657
|
+
fullWidth: true,
|
|
658
|
+
disabled: services.length === 0,
|
|
659
|
+
helperText: services.length === 0 ? "The descriptor set declares no services." : " ",
|
|
660
|
+
children: services.map((option) => /* @__PURE__ */ jsx2(MenuItem, { value: option.typeName, children: option.typeName }, option.typeName))
|
|
661
|
+
}
|
|
662
|
+
),
|
|
663
|
+
service !== void 0 && /* @__PURE__ */ jsx2(
|
|
664
|
+
TextField2,
|
|
665
|
+
{
|
|
666
|
+
select: true,
|
|
667
|
+
label: "Method",
|
|
668
|
+
value: method?.localName ?? "",
|
|
669
|
+
onChange: (e) => selectMethod(service.methods.find((m) => m.localName === e.target.value)),
|
|
670
|
+
size: "small",
|
|
671
|
+
fullWidth: true,
|
|
672
|
+
helperText: method === void 0 ? " " : methodModel(method).description,
|
|
673
|
+
children: service.methods.map((option) => /* @__PURE__ */ jsx2(
|
|
674
|
+
MenuItem,
|
|
675
|
+
{
|
|
676
|
+
value: option.localName,
|
|
677
|
+
disabled: !isMethodSupported(option),
|
|
678
|
+
children: /* @__PURE__ */ jsxs2(Stack2, { direction: "row", spacing: 1, sx: { alignItems: "center" }, children: [
|
|
679
|
+
/* @__PURE__ */ jsx2("span", { children: option.name }),
|
|
680
|
+
/* @__PURE__ */ jsx2(Chip2, { size: "small", variant: "outlined", label: methodModel(option).label })
|
|
681
|
+
] })
|
|
682
|
+
},
|
|
683
|
+
option.name
|
|
684
|
+
))
|
|
685
|
+
}
|
|
686
|
+
),
|
|
687
|
+
method !== void 0 && /* @__PURE__ */ jsxs2(Stack2, { spacing: 1.5, children: [
|
|
688
|
+
/* @__PURE__ */ jsxs2(Typography2, { variant: "subtitle2", children: [
|
|
689
|
+
"Request \xB7 ",
|
|
690
|
+
method.input.typeName
|
|
691
|
+
] }),
|
|
692
|
+
/* @__PURE__ */ jsx2(
|
|
693
|
+
RequestFields,
|
|
694
|
+
{
|
|
695
|
+
message: method.input,
|
|
696
|
+
json: value.json,
|
|
697
|
+
onChange: (json) => onChange({ ...value, json })
|
|
698
|
+
}
|
|
699
|
+
),
|
|
700
|
+
/* @__PURE__ */ jsxs2(
|
|
701
|
+
Accordion,
|
|
702
|
+
{
|
|
703
|
+
disableGutters: true,
|
|
704
|
+
variant: "outlined",
|
|
705
|
+
defaultExpanded: true,
|
|
706
|
+
sx: { "&::before": { display: "none" } },
|
|
707
|
+
children: [
|
|
708
|
+
/* @__PURE__ */ jsx2(AccordionSummary, { expandIcon: /* @__PURE__ */ jsx2(ExpandMoreIcon, {}), children: /* @__PURE__ */ jsx2(Typography2, { sx: { fontWeight: 600 }, children: "Request JSON" }) }),
|
|
709
|
+
/* @__PURE__ */ jsx2(AccordionDetails, { children: /* @__PURE__ */ jsx2(
|
|
710
|
+
TextField2,
|
|
711
|
+
{
|
|
712
|
+
value: value.json,
|
|
713
|
+
onChange: (e) => onChange({ ...value, json: e.target.value }),
|
|
714
|
+
multiline: true,
|
|
715
|
+
minRows: 6,
|
|
716
|
+
fullWidth: true,
|
|
717
|
+
size: "small",
|
|
718
|
+
slotProps: { input: { sx: { fontFamily: "monospace", fontSize: 13 } } },
|
|
719
|
+
helperText: "Canonical protobuf-JSON \u2014 exactly what is sent on the channel."
|
|
720
|
+
}
|
|
721
|
+
) })
|
|
722
|
+
]
|
|
723
|
+
}
|
|
724
|
+
),
|
|
725
|
+
parsedRequest !== null && "error" in parsedRequest && /* @__PURE__ */ jsx2(Alert2, { severity: "error", children: parsedRequest.error })
|
|
726
|
+
] })
|
|
727
|
+
] })
|
|
728
|
+
] });
|
|
729
|
+
};
|
|
730
|
+
var canOpenRpcChannel = (value) => {
|
|
731
|
+
if (value.registry === null) return false;
|
|
732
|
+
const service = value.registry.getService(value.serviceName);
|
|
733
|
+
const method = service?.method[value.methodName];
|
|
734
|
+
if (method === void 0 || !isMethodSupported(method)) return false;
|
|
735
|
+
return !("error" in parseRequest(method.input, value.json));
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
// src/plugin.tsx
|
|
739
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
740
|
+
var rpcChannelPlugin = ({
|
|
741
|
+
descriptorSetUrl = "",
|
|
742
|
+
locked = false
|
|
743
|
+
} = {}) => defineChannelPlugin({
|
|
744
|
+
kind: "rpc",
|
|
745
|
+
label: "RPC",
|
|
746
|
+
icon: /* @__PURE__ */ jsx3(SwapHorizIcon2, {}),
|
|
747
|
+
dialogTitle: "New RPC channel",
|
|
748
|
+
// A service picker, a method picker and a JSON editor; the other three forms are a
|
|
749
|
+
// handful of fields.
|
|
750
|
+
dialogMaxWidth: "md",
|
|
751
|
+
createRequest: () => ({
|
|
752
|
+
url: descriptorSetUrl,
|
|
753
|
+
registry: null,
|
|
754
|
+
source: null,
|
|
755
|
+
serviceName: "",
|
|
756
|
+
methodName: "",
|
|
757
|
+
json: "{}"
|
|
758
|
+
}),
|
|
759
|
+
RequestForm: ({ value, onChange }) => /* @__PURE__ */ jsx3(RpcChannelRequest, { value, onChange, urlLocked: locked }),
|
|
760
|
+
canOpen: canOpenRpcChannel,
|
|
761
|
+
buildConfig: (request) => {
|
|
762
|
+
const service = request.registry?.getService(request.serviceName);
|
|
763
|
+
const method = service?.method[request.methodName];
|
|
764
|
+
if (service === void 0 || method === void 0) {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
const parsed = parseRequest(method.input, request.json);
|
|
768
|
+
return "error" in parsed ? null : { service, method, request: parsed.message };
|
|
769
|
+
},
|
|
770
|
+
Channel: RpcChannel
|
|
771
|
+
});
|
|
772
|
+
export {
|
|
773
|
+
rpcChannelPlugin
|
|
774
|
+
};
|
|
775
|
+
//# sourceMappingURL=index.mjs.map
|