diff --git a/package-lock.json b/package-lock.json index b9f5cbf360..c84f2dee4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index a43872fe0a..90f111090b 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -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", diff --git a/packages/insomnia/src/__jest__/setup.ts b/packages/insomnia/src/__jest__/setup.ts index 698e7a5231..8f3ba0c49a 100644 --- a/packages/insomnia/src/__jest__/setup.ts +++ b/packages/insomnia/src/__jest__/setup.ts @@ -1,3 +1,7 @@ +import { TextDecoder, TextEncoder } from 'util'; + +Object.assign(globalThis, { TextDecoder, TextEncoder }); + globalThis.__DEV__ = false; globalThis.requestAnimationFrame = (callback: FrameRequestCallback) => { diff --git a/packages/insomnia/src/main/ipc/__tests__/grpc.test.ts b/packages/insomnia/src/main/ipc/__tests__/grpc.test.ts index db5fd052b5..a7845a5c0e 100644 --- a/packages/insomnia/src/main/ipc/__tests__/grpc.test.ts +++ b/packages/insomnia/src/main/ipc/__tests__/grpc.test.ts @@ -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 + ): Promise { + 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', + }, + ] + ); + }); + }); }); diff --git a/packages/insomnia/src/main/ipc/grpc.ts b/packages/insomnia/src/main/ipc/grpc.ts index 5496c787e4..6403b4a21d 100644 --- a/packages/insomnia/src/main/ipc/grpc.ts +++ b/packages/insomnia/src/main/ipc/grpc.ts @@ -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(); + 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 => { fullPath: method.path, })); }; + interface MethodDefs { path: string; requestStream: boolean; @@ -75,10 +105,109 @@ interface MethodDefs { responseDeserialize: (value: Buffer) => any; example?: Record; } -const getMethodsFromReflection = async (host: string, metadata: GrpcRequestHeader[]): Promise => { + +const getMethodsFromReflectionServer = async ( + reflectionApi: GrpcRequest['reflectionApi'] +): Promise => { + 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 => { + 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 => { +export const loadMethodsFromReflection = async (options: { + url: string; + metadata: GrpcRequestHeader[]; + reflectionApi: GrpcRequest['reflectionApi']; +}): Promise => { 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; } -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 => { +export const getSelectedMethod = async ( + request: GrpcRequest +): Promise => { 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) { diff --git a/packages/insomnia/src/models/__tests__/grpc-request.test.ts b/packages/insomnia/src/models/__tests__/grpc-request.test.ts index f2fcff95ad..c68434df44 100644 --- a/packages/insomnia/src/models/__tests__/grpc-request.test.ts +++ b/packages/insomnia/src/models/__tests__/grpc-request.test.ts @@ -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', diff --git a/packages/insomnia/src/models/grpc-request.ts b/packages/insomnia/src/models/grpc-request.ts index a90ee28359..2dd8c03179 100644 --- a/packages/insomnia/src/models/grpc-request.ts +++ b/packages/insomnia/src/models/grpc-request.ts @@ -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', + }, }; } diff --git a/packages/insomnia/src/ui/components/modals/request-settings-modal.tsx b/packages/insomnia/src/ui/components/modals/request-settings-modal.tsx index 7ac371284d..c7b6337f32 100644 --- a/packages/insomnia/src/ui/components/modals/request-settings-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/request-settings-modal.tsx @@ -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) => { + 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 )} {request && isGrpcRequest(request) && ( -

- Are there any gRPC settings you expect to see? Create a{' '} - feature request! -

+ <> +
+ +
+
+ {request.reflectionApi.enabled && ( + <> +
+ +
+
+ +
+
+ +
+ + )} +
+

+ Are there any gRPC settings you expect to see? Create a{' '} + feature request! +

+ )} {request && isRequest(request) && ( <> diff --git a/packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx b/packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx index 0d32366f18..bc66e4dcf9 100644 --- a/packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/grpc-request-pane.tsx @@ -197,7 +197,16 @@ export const GrpcRequestPane: FunctionComponent = ({ 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: '' });