Add ability to use Buf Schema Registry as a schema source for gRPC requests (#6975)

* Add support for Buf Reflection Api

* Add test; Change tooltips to links

* style

* Remove label class

* request tests

* Update copy

* Rename prop; Fix alignment, input

* Add user agent header

* use onBlur and simplify

* fix lint

---------

Co-authored-by: jackkav <jackkav@gmail.com>
This commit is contained in:
Sri Krishna
2024-02-05 15:26:23 +05:30
committed by GitHub
parent 9d3ebf944e
commit 302172d4c5
9 changed files with 441 additions and 23 deletions

50
package-lock.json generated
View File

@@ -741,6 +741,11 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"node_modules/@bufbuild/protobuf": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.4.1.tgz",
"integrity": "sha512-4dthhwBGD9nlpY35ic8dMQC5R0dsND2b2xyeVO3qf+hBk8m7Y9dUs+SmMh6rqO2pGLUTKHefGXLDW+z19hBPdQ=="
},
"node_modules/@codemirror/language": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.0.0.tgz",
@@ -775,6 +780,29 @@
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@connectrpc/connect": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.1.3.tgz",
"integrity": "sha512-AXkbsLQe2Nm7VuoN5nqp05GEb9mPa/f5oFzDqTbHME4i8TghTrlY03uefbhuAq4wjsnfDnmuxHZvn6ndlgXmbg==",
"peerDependencies": {
"@bufbuild/protobuf": "^1.3.3"
}
},
"node_modules/@connectrpc/connect-node": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@connectrpc/connect-node/-/connect-node-1.1.3.tgz",
"integrity": "sha512-oq7Uk8XlLzC2+eHaxZTX189dhujD0/tK9plizxofsFHUnLquMSmzQQ2GzvTv4u6U05eZYc/crySmf86Sqpi1bA==",
"dependencies": {
"undici": "^5.26.2"
},
"engines": {
"node": ">=16.0.0"
},
"peerDependencies": {
"@bufbuild/protobuf": "^1.3.3",
"@connectrpc/connect": "1.1.3"
}
},
"node_modules/@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@@ -1774,6 +1802,14 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@fastify/busboy": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz",
"integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==",
"engines": {
"node": ">=14"
}
},
"node_modules/@formatjs/ecma402-abstract": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz",
@@ -23435,6 +23471,17 @@
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A=="
},
"node_modules/undici": {
"version": "5.27.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz",
"integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},
"engines": {
"node": ">=14.0"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
@@ -24829,6 +24876,9 @@
"dependencies": {
"@apideck/better-ajv-errors": "^0.3.6",
"@apidevtools/swagger-parser": "10.1.0",
"@bufbuild/protobuf": "^1.4.1",
"@connectrpc/connect": "^1.1.3",
"@connectrpc/connect-node": "^1.1.3",
"@getinsomnia/node-libcurl": "^2.4.1-9",
"@grpc/grpc-js": "^1.8.17",
"@grpc/proto-loader": "^0.7.7",

View File

@@ -36,6 +36,9 @@
"dependencies": {
"@apideck/better-ajv-errors": "^0.3.6",
"@apidevtools/swagger-parser": "10.1.0",
"@bufbuild/protobuf": "^1.4.1",
"@connectrpc/connect": "^1.1.3",
"@connectrpc/connect-node": "^1.1.3",
"@getinsomnia/node-libcurl": "^2.4.1-9",
"@grpc/grpc-js": "^1.8.17",
"@grpc/proto-loader": "^0.7.7",

View File

@@ -1,3 +1,7 @@
import { TextDecoder, TextEncoder } from 'util';
Object.assign(globalThis, { TextDecoder, TextEncoder });
globalThis.__DEV__ = false;
globalThis.requestAnimationFrame = (callback: FrameRequestCallback) => {

View File

@@ -1,3 +1,11 @@
import {
AnyMessage,
MethodInfo,
PartialMessage,
ServiceType,
} from '@bufbuild/protobuf';
import { UnaryResponse } from '@connectrpc/connect';
import { createConnectTransport } from '@connectrpc/connect-node';
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import * as grpcReflection from 'grpc-reflection-js';
import protobuf from 'protobufjs';
@@ -6,6 +14,7 @@ import { globalBeforeEach } from '../../../__jest__/before-each';
import { loadMethodsFromReflection } from '../grpc';
jest.mock('grpc-reflection-js');
jest.mock('@connectrpc/connect-node');
describe('loadMethodsFromReflection', () => {
beforeEach(globalBeforeEach);
@@ -37,7 +46,11 @@ describe('loadMethodsFromReflection', () => {
});
it('parses methods', async () => {
const methods = await loadMethodsFromReflection({ url: 'foo.com', metadata: [] });
const methods = await loadMethodsFromReflection({
url: 'foo.com',
metadata: [],
reflectionApi: { enabled: false, apiKey: '', url: '', module: '' },
});
expect(methods).toStrictEqual([{
type: 'unary',
fullPath: '/FooService/Foo',
@@ -75,7 +88,11 @@ describe('loadMethodsFromReflection', () => {
});
it('parses methods', async () => {
const methods = await loadMethodsFromReflection({ url: 'foo.com', metadata: [] });
const methods = await loadMethodsFromReflection({
url: 'foo.com',
metadata: [],
reflectionApi: { enabled: false, apiKey: '', url: '', module: '' },
});
expect(methods).toStrictEqual([{
type: 'unary',
fullPath: '/FooService/format',
@@ -125,7 +142,11 @@ describe('loadMethodsFromReflection', () => {
});
it('parses methods', async () => {
const methods = await loadMethodsFromReflection({ url: 'foo-bar.com', metadata: [] });
const methods = await loadMethodsFromReflection({
url: 'foo-bar.com',
metadata: [],
reflectionApi: { enabled: false, apiKey: '', url: '', module: '' },
});
expect(methods).toStrictEqual([{
type: 'unary',
fullPath: '/FooService/Foo',
@@ -142,4 +163,66 @@ describe('loadMethodsFromReflection', () => {
});
});
describe('buf reflection api', () => {
it('loads module', async () => {
(createConnectTransport as unknown as jest.Mock).mockImplementation(
options => {
expect(options.baseUrl).toStrictEqual('https://buf.build');
return {
async unary(
service: ServiceType,
method: MethodInfo,
_: AbortSignal | undefined,
__: number | undefined,
header: HeadersInit | undefined,
input: PartialMessage<AnyMessage>
): Promise<UnaryResponse> {
expect(new Headers(header).get('Authorization')).toStrictEqual('Bearer TEST_KEY');
expect(input).toStrictEqual({ module: 'buf.build/connectrpc/eliza' });
return {
service: service,
method: method,
header: new Headers(),
trailer: new Headers(),
stream: false,
// Output of running `buf curl https://buf.build/buf.reflect.v1beta1.FileDescriptorSetService/GetFileDescriptorSet --data '{"module": "buf.build/connectrpc/eliza"}' --schema buf.build/bufbuild/reflect -H 'Authorization: Bearer buf-token'`
message: method.O.fromJsonString(
'{"fileDescriptorSet":{"file":[{"name":"connectrpc/eliza/v1/eliza.proto","package":"connectrpc.eliza.v1","messageType":[{"name":"SayRequest","field":[{"name":"sentence","number":1,"label":"LABEL_OPTIONAL","type":"TYPE_STRING","jsonName":"sentence"}]},{"name":"SayResponse","field":[{"name":"sentence","number":1,"label":"LABEL_OPTIONAL","type":"TYPE_STRING","jsonName":"sentence"}]},{"name":"ConverseRequest","field":[{"name":"sentence","number":1,"label":"LABEL_OPTIONAL","type":"TYPE_STRING","jsonName":"sentence"}]},{"name":"ConverseResponse","field":[{"name":"sentence","number":1,"label":"LABEL_OPTIONAL","type":"TYPE_STRING","jsonName":"sentence"}]},{"name":"IntroduceRequest","field":[{"name":"name","number":1,"label":"LABEL_OPTIONAL","type":"TYPE_STRING","jsonName":"name"}]},{"name":"IntroduceResponse","field":[{"name":"sentence","number":1,"label":"LABEL_OPTIONAL","type":"TYPE_STRING","jsonName":"sentence"}]}],"service":[{"name":"ElizaService","method":[{"name":"Say","inputType":".connectrpc.eliza.v1.SayRequest","outputType":".connectrpc.eliza.v1.SayResponse","options":{"idempotencyLevel":"NO_SIDE_EFFECTS"}},{"name":"Converse","inputType":".connectrpc.eliza.v1.ConverseRequest","outputType":".connectrpc.eliza.v1.ConverseResponse","options":{},"clientStreaming":true,"serverStreaming":true},{"name":"Introduce","inputType":".connectrpc.eliza.v1.IntroduceRequest","outputType":".connectrpc.eliza.v1.IntroduceResponse","options":{},"serverStreaming":true}]}],"syntax":"proto3"}]},"version":"233fca715f49425581ec0a1b660be886"}'
),
};
},
};
}
);
const methods = await loadMethodsFromReflection({
url: 'foo.com',
metadata: [],
reflectionApi: {
enabled: true,
apiKey: 'TEST_KEY',
url: 'https://buf.build',
module: 'buf.build/connectrpc/eliza',
},
});
expect(methods).toStrictEqual(
[
{
example: undefined,
fullPath: '/connectrpc.eliza.v1.ElizaService/Say',
type: 'unary',
},
{
example: undefined,
fullPath: '/connectrpc.eliza.v1.ElizaService/Converse',
type: 'bidi',
},
{
example: undefined,
fullPath: '/connectrpc.eliza.v1.ElizaService/Introduce',
type: 'server',
},
]
);
});
});
});

View File

@@ -1,9 +1,34 @@
import { Call, ClientDuplexStream, ClientReadableStream, credentials, makeGenericClientConstructor, Metadata, ServiceError, status, StatusObject } from '@grpc/grpc-js';
import {
FileDescriptorSet as ProtobufEsFileDescriptorSet,
MethodIdempotency,
MethodKind,
proto3,
} from '@bufbuild/protobuf';
import { Code, ConnectError, createPromiseClient } from '@connectrpc/connect';
import { createConnectTransport } from '@connectrpc/connect-node';
import {
Call,
ClientDuplexStream,
ClientReadableStream,
credentials,
makeGenericClientConstructor,
Metadata,
ServiceError,
status,
StatusObject,
} from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import { AnyDefinition, EnumTypeDefinition, MessageTypeDefinition, PackageDefinition, ServiceDefinition } from '@grpc/proto-loader';
import {
AnyDefinition,
EnumTypeDefinition,
MessageTypeDefinition,
PackageDefinition,
ServiceDefinition,
} from '@grpc/proto-loader';
import electron, { ipcMain, IpcMainEvent } from 'electron';
import * as grpcReflection from 'grpc-reflection-js';
import { version } from '../../../package.json';
import type { RenderedGrpcRequest, RenderedGrpcRequestBody } from '../../common/render';
import * as models from '../../models';
import type { GrpcRequest, GrpcRequestHeader } from '../../models/grpc-request';
@@ -13,6 +38,7 @@ import { invariant } from '../../utils/invariant';
import { mockRequestMethods } from './automock';
const grpcCalls = new Map<string, Call>();
export interface GrpcIpcRequestParams {
request: RenderedGrpcRequest;
}
@@ -21,6 +47,7 @@ export interface GrpcIpcMessageParams {
requestId: string;
body: RenderedGrpcRequestBody;
}
export interface gRPCBridgeAPI {
start: (options: GrpcIpcRequestParams) => void;
sendMessage: (options: GrpcIpcMessageParams) => void;
@@ -30,6 +57,7 @@ export interface gRPCBridgeAPI {
loadMethodsFromReflection: typeof loadMethodsFromReflection;
closeAll: typeof closeAll;
}
export function registergRPCHandlers() {
ipcMain.on('grpc.start', start);
ipcMain.on('grpc.sendMessage', sendMessage);
@@ -39,6 +67,7 @@ export function registergRPCHandlers() {
ipcMain.handle('grpc.loadMethods', (_, requestId) => loadMethods(requestId));
ipcMain.handle('grpc.loadMethodsFromReflection', (_, requestId) => loadMethodsFromReflection(requestId));
}
const grpcOptions = {
keepCase: true,
longs: String,
@@ -67,6 +96,7 @@ const loadMethods = async (protoFileId: string): Promise<GrpcMethodInfo[]> => {
fullPath: method.path,
}));
};
interface MethodDefs {
path: string;
requestStream: boolean;
@@ -75,10 +105,109 @@ interface MethodDefs {
responseDeserialize: (value: Buffer) => any;
example?: Record<string, any>;
}
const getMethodsFromReflection = async (host: string, metadata: GrpcRequestHeader[]): Promise<MethodDefs[]> => {
const getMethodsFromReflectionServer = async (
reflectionApi: GrpcRequest['reflectionApi']
): Promise<MethodDefs[]> => {
const { url, module, apiKey } = reflectionApi;
const GetFileDescriptorSetRequest = proto3.makeMessageType(
'buf.reflect.v1beta1.GetFileDescriptorSetRequest',
() => [
{ no: 1, name: 'module', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{ no: 2, name: 'version', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{
no: 3,
name: 'symbols',
kind: 'scalar',
T: 9 /* ScalarType.STRING */,
repeated: true,
},
]
);
const GetFileDescriptorSetResponse = proto3.makeMessageType(
'buf.reflect.v1beta1.GetFileDescriptorSetResponse',
() => [
{
no: 1,
name: 'file_descriptor_set',
kind: 'message',
T: ProtobufEsFileDescriptorSet,
},
{ no: 2, name: 'version', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
]
);
const FileDescriptorSetService = {
typeName: 'buf.reflect.v1beta1.FileDescriptorSetService',
methods: {
getFileDescriptorSet: {
name: 'GetFileDescriptorSet',
I: GetFileDescriptorSetRequest,
O: GetFileDescriptorSetResponse,
kind: MethodKind.Unary,
idempotency: MethodIdempotency.NoSideEffects,
},
},
} as const;
const transport = createConnectTransport({
baseUrl: url,
httpVersion: '1.1',
});
const client = createPromiseClient(FileDescriptorSetService, transport);
const headers: HeadersInit = {
'User-Agent': `insomnia/${version}`,
...(apiKey === '' ? {} : { Authorization: `Bearer ${apiKey}` }),
};
try {
const res = await client.getFileDescriptorSet(
{
module,
},
{
headers,
}
);
const methodDefs: MethodDefs[] = [];
if (res.fileDescriptorSet === undefined) {
return [];
}
const packageDefinition = protoLoader.loadFileDescriptorSetFromBuffer(
new Buffer(res.fileDescriptorSet.toBinary())
);
for (const definition of Object.values(packageDefinition)) {
const serviceDefinition = asServiceDefinition(definition);
if (serviceDefinition === null) {
continue;
}
const serviceMethods = Object.values(serviceDefinition);
methodDefs.push(...serviceMethods);
}
return methodDefs;
} catch (error) {
const connectError = ConnectError.from(error);
switch (connectError.code) {
case Code.Unauthenticated:
throw new Error('Invalid reflection server api key');
case Code.NotFound:
throw new Error(
"The reflection server api key doesn't have access to the module or the module does not exists"
);
default:
throw error;
}
}
};
const getMethodsFromReflection = async (
host: string,
metadata: GrpcRequestHeader[],
reflectionApi: GrpcRequest['reflectionApi']
): Promise<MethodDefs[]> => {
if (reflectionApi.enabled) {
return getMethodsFromReflectionServer(reflectionApi);
}
try {
const { url, enableTls } = parseGrpcUrl(host);
const client = new grpcReflection.Client(url,
const client = new grpcReflection.Client(
url,
enableTls ? credentials.createSsl() : credentials.createInsecure(),
grpcOptions,
filterDisabledMetaData(metadata)
@@ -89,15 +218,25 @@ const getMethodsFromReflection = async (host: string, metadata: GrpcRequestHeade
const fullService = fileContainingSymbol.lookupService(service);
const mockedRequestMethods = mockRequestMethods(fullService);
const descriptorMessage = fileContainingSymbol.toDescriptor('proto3');
const packageDefinition = protoLoader.loadFileDescriptorSetFromObject(descriptorMessage, {});
const packageDefinition = protoLoader.loadFileDescriptorSetFromObject(
descriptorMessage,
{}
);
const tryToGetMethods = () => {
try {
console.log('[grpc] loading service from reflection:', service);
const serviceDefinition = asServiceDefinition(packageDefinition[service]);
invariant(serviceDefinition, `'${service}' was not a valid ServiceDefinition`);
const serviceDefinition = asServiceDefinition(
packageDefinition[service]
);
invariant(
serviceDefinition,
`'${service}' was not a valid ServiceDefinition`
);
const serviceMethods = Object.values(serviceDefinition);
return serviceMethods.map(m => {
const methodName = Object.keys(mockedRequestMethods).find(name => m.path.endsWith(`/${name}`));
const methodName = Object.keys(mockedRequestMethods).find(name =>
m.path.endsWith(`/${name}`)
);
if (!methodName) {
return m;
}
@@ -119,21 +258,34 @@ const getMethodsFromReflection = async (host: string, metadata: GrpcRequestHeade
throw error;
}
};
export const loadMethodsFromReflection = async (options: { url: string; metadata: GrpcRequestHeader[] }): Promise<GrpcMethodInfo[]> => {
export const loadMethodsFromReflection = async (options: {
url: string;
metadata: GrpcRequestHeader[];
reflectionApi: GrpcRequest['reflectionApi'];
}): Promise<GrpcMethodInfo[]> => {
invariant(options.url, 'gRPC request url not provided');
const methods = await getMethodsFromReflection(options.url, options.metadata);
const methods = await getMethodsFromReflection(
options.url,
options.metadata,
options.reflectionApi
);
return methods.map(method => ({
type: getMethodType(method),
fullPath: method.path,
example: method.example,
}));
};
export interface GrpcMethodInfo {
type: GrpcMethodType;
fullPath: string;
example?: Record<string, any>;
}
export const getMethodType = ({ requestStream, responseStream }: any): GrpcMethodType => {
export const getMethodType = ({
requestStream,
responseStream,
}: any): GrpcMethodType => {
if (requestStream && responseStream) {
return 'bidi';
}
@@ -146,16 +298,25 @@ export const getMethodType = ({ requestStream, responseStream }: any): GrpcMetho
return 'unary';
};
export const getSelectedMethod = async (request: GrpcRequest): Promise<MethodDefs | undefined> => {
export const getSelectedMethod = async (
request: GrpcRequest
): Promise<MethodDefs | undefined> => {
if (request.protoFileId) {
const protoFile = await models.protoFile.getById(request.protoFileId);
invariant(protoFile?.protoText, `No proto file found for gRPC request ${request._id}`);
invariant(
protoFile?.protoText,
`No proto file found for gRPC request ${request._id}`
);
const { filePath, dirs } = await writeProtoFile(protoFile);
const methods = await loadMethodsFromFilePath(filePath, dirs);
invariant(methods, 'No methods found');
return methods.find(c => c.path === request.protoMethodName);
}
const methods = await getMethodsFromReflection(request.url, request.metadata);
const methods = await getMethodsFromReflection(
request.url,
request.metadata,
request.reflectionApi
);
invariant(methods, 'No reflection methods found');
return methods.find(c => c.path === request.protoMethodName);
};
@@ -336,7 +497,7 @@ const onUnaryResponse = (event: IpcMainEvent, requestId: string) => (err: Servic
grpcCalls.delete(requestId);
};
const filterDisabledMetaData = (metadata: GrpcRequestHeader[],): Metadata => {
const filterDisabledMetaData = (metadata: GrpcRequestHeader[]): Metadata => {
const grpcMetadata = new Metadata();
for (const entry of metadata) {
if (!entry.disabled) {

View File

@@ -18,6 +18,12 @@ describe('init()', () => {
body: {
text: '{}',
},
reflectionApi: {
enabled: false,
apiKey: '',
module: 'buf.build/connectrpc/eliza',
url: 'https://buf.build',
},
metaSortKey: -1478795580200,
isPrivate: false,
});
@@ -47,6 +53,12 @@ describe('create()', () => {
body: {
text: '{}',
},
reflectionApi: {
enabled: false,
apiKey: '',
module: 'buf.build/connectrpc/eliza',
url: 'https://buf.build',
},
metaSortKey: -1478795580200,
isPrivate: false,
type: 'GrpcRequest',

View File

@@ -28,6 +28,12 @@ interface BaseGrpcRequest {
metadata: GrpcRequestHeader[];
metaSortKey: number;
isPrivate: boolean;
reflectionApi: {
enabled: boolean;
url: string;
apiKey: string;
module: string;
};
}
export type GrpcRequest = BaseModel & BaseGrpcRequest;
@@ -53,6 +59,12 @@ export function init(): BaseGrpcRequest {
},
metaSortKey: -1 * Date.now(),
isPrivate: false,
reflectionApi: {
enabled: false,
url: 'https://buf.build',
apiKey: '',
module: 'buf.build/connectrpc/eliza',
},
};
}

View File

@@ -14,6 +14,7 @@ import { ModalBody } from '../base/modal-body';
import { ModalHeader } from '../base/modal-header';
import { CodeEditorHandle } from '../codemirror/code-editor';
import { HelpTooltip } from '../help-tooltip';
import { Icon } from '../icon';
import { MarkdownEditor } from '../markdown-editor';
export interface RequestSettingsModalOptions {
@@ -71,6 +72,15 @@ export const RequestSettingsModal = ({ request, onHide }: ModalProps & RequestSe
const toggleCheckBox = async (event: any) => {
patchRequest(request._id, { [event.currentTarget.name]: event.currentTarget.checked ? true : false });
};
const updateReflectonApi = async (event: React.ChangeEvent<HTMLInputElement>) => {
invariant(isGrpcRequest(request), 'Must be gRPC request');
patchRequest(request._id, {
reflectionApi: {
...request.reflectionApi,
[event.currentTarget.name]: event.currentTarget.value,
},
});
};
const updateDescription = (description: string) => {
patchRequest(request._id, { description });
setState({
@@ -202,10 +212,84 @@ export const RequestSettingsModal = ({ request, onHide }: ModalProps & RequestSe
</div>
</>)}
{request && isGrpcRequest(request) && (
<p className="faint italic">
Are there any gRPC settings you expect to see? Create a{' '}
<a href={'https://github.com/Kong/insomnia/issues/new/choose'}>feature request</a>!
</p>
<>
<div className="form-control form-control--thin pad-top-sm">
<label>
Use the Buf Schema Registry API
<a href="https://buf.build/docs/bsr/reflection/overview" className="pad-left-sm">
<Icon icon="external-link" size="sm" />
</a>
<input
type="checkbox"
name="reflectionApi"
checked={request.reflectionApi.enabled}
onChange={event => patchRequest(request._id, {
reflectionApi: {
...request.reflectionApi,
enabled: event.currentTarget.checked,
},
})}
/>̵
</label>
</div>
<div className="form-row pad-top-sm">
{request.reflectionApi.enabled && (
<>
<div className="form-control form-control--outlined">
<label>
Reflection server URL
<a href="https://buf.build/docs/bsr/api-access" className="pad-left-sm">
<Icon icon="external-link" size="sm" />
</a>
<input
type="text"
name="url"
placeholder="https://buf.build"
defaultValue={request.reflectionApi.url}
onBlur={updateReflectonApi}
disabled={!request.reflectionApi.enabled}
/>
</label>
</div>
<div className="form-control form-control--outlined">
<label>
Reflection server API key
<a href="https://buf.build/docs/bsr/authentication#manage-tokens" className="pad-left-sm">
<Icon icon="external-link" size="sm" />
</a>
<input
type="password"
name="apiKey"
defaultValue={request.reflectionApi.apiKey}
onBlur={updateReflectonApi}
disabled={!request.reflectionApi.enabled}
/>
</label>
</div>
<div className="form-control form-control--outlined">
<label>
Module
<a href="https://buf.build/docs/bsr/module/manage" className="pad-left-sm">
<Icon icon="external-link" size="sm" />
</a>
<input
type="text"
name="module"
placeholder="buf.build/connectrpc/eliza"
defaultValue={request.reflectionApi.module}
onBlur={updateReflectonApi}
disabled={!request.reflectionApi.enabled}
/>
</label>
</div>
</>
)}
</div>
<p className="faint italic pad-top">
Are there any gRPC settings you expect to see? Create a{' '}
<a href={'https://github.com/Kong/insomnia/issues/new/choose'}>feature request</a>!
</p>
</>
)}
{request && isRequest(request) && (
<>

View File

@@ -197,7 +197,16 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
disabled={!activeRequest.url}
onClick={async () => {
try {
const rendered = await tryToInterpolateRequestOrShowRenderErrorModal({ request: activeRequest, environmentId, payload: { url: activeRequest.url, metadata: activeRequest.metadata } });
const rendered =
await tryToInterpolateRequestOrShowRenderErrorModal({
request: activeRequest,
environmentId,
payload: {
url: activeRequest.url,
metadata: activeRequest.metadata,
reflectionApi: activeRequest.reflectionApi,
},
});
const methods = await window.main.grpc.loadMethodsFromReflection(rendered);
setGrpcState({ ...grpcState, methods });
patchRequest(requestId, { protoFileId: '', protoMethodName: '' });