mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 06:39:48 +00:00
feat: gRPC request stubs (#6196)
* feat: grpc request stubs * mark changed lines * add tests * fix infinite recursion * replace empty body * catch Type objects * button to replace stubs * flatten grpc editor * remove grpc-editor * trigger setvalue * rename mocks to example --------- Co-authored-by: jackkav <jackkav@gmail.com> Co-authored-by: James Gatz <jamesgatzos@gmail.com>
This commit is contained in:
parent
96ae43700e
commit
97760348e3
122
packages/insomnia/src/main/ipc/__tests__/automock.test.ts
Normal file
122
packages/insomnia/src/main/ipc/__tests__/automock.test.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { it } from '@jest/globals';
|
||||
import { parse } from 'protobufjs';
|
||||
|
||||
import { mockRequestMethods } from '../automock';
|
||||
|
||||
it('mocks simple requests', () => {
|
||||
const parsed = parse(`
|
||||
syntax = "proto3";
|
||||
|
||||
message FooRequest {
|
||||
string foo = 1;
|
||||
}
|
||||
|
||||
message FooResponse {
|
||||
string foo = 1;
|
||||
}
|
||||
|
||||
service FooService {
|
||||
rpc Foo (FooRequest) returns (FooResponse);
|
||||
}`);
|
||||
|
||||
const service = parsed.root.lookupService('FooService');
|
||||
const mocked = mockRequestMethods(service);
|
||||
|
||||
const plain = mocked['Foo']().plain;
|
||||
expect(plain).toStrictEqual({
|
||||
foo: 'Hello',
|
||||
});
|
||||
});
|
||||
|
||||
it('mocks requests with nested objects', () => {
|
||||
const parsed = parse(`
|
||||
syntax = "proto3";
|
||||
|
||||
message BarBarObject {
|
||||
int32 one = 1;
|
||||
}
|
||||
|
||||
message BarObject {
|
||||
BarBarObject fuzz = 1;
|
||||
}
|
||||
|
||||
message FooRequest {
|
||||
BarObject bar = 2;
|
||||
}
|
||||
|
||||
message FooResponse {
|
||||
string foo = 1;
|
||||
}
|
||||
|
||||
service FooService {
|
||||
rpc Foo (FooRequest) returns (FooResponse);
|
||||
}`);
|
||||
|
||||
const service = parsed.root.lookupService('FooService');
|
||||
const mocked = mockRequestMethods(service);
|
||||
|
||||
const plain = mocked['Foo']().plain;
|
||||
expect(plain).toStrictEqual({
|
||||
bar: {
|
||||
fuzz: {
|
||||
one: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('mocks requests with enums', () => {
|
||||
const parsed = parse(`
|
||||
syntax = "proto3";
|
||||
|
||||
enum MyEnum {
|
||||
MYENUM_UNSPECIFIED = 0;
|
||||
MYENUM_A = 1;
|
||||
MYENUM_B = 2;
|
||||
}
|
||||
|
||||
message FooRequest {
|
||||
MyEnum enum = 1;
|
||||
}
|
||||
|
||||
message FooResponse {
|
||||
string foo = 1;
|
||||
}
|
||||
|
||||
service FooService {
|
||||
rpc Foo (FooRequest) returns (FooResponse);
|
||||
}`);
|
||||
|
||||
const service = parsed.root.lookupService('FooService');
|
||||
const mocked = mockRequestMethods(service);
|
||||
|
||||
const plain = mocked['Foo']().plain;
|
||||
expect(plain).toStrictEqual({
|
||||
enum: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('mocks requests with repeated values', () => {
|
||||
const parsed = parse(`
|
||||
syntax = "proto3";
|
||||
|
||||
message FooRequest {
|
||||
repeated string foo = 1;
|
||||
}
|
||||
|
||||
message FooResponse {
|
||||
string foo = 1;
|
||||
}
|
||||
|
||||
service FooService {
|
||||
rpc Foo (FooRequest) returns (FooResponse);
|
||||
}`);
|
||||
|
||||
const service = parsed.root.lookupService('FooService');
|
||||
const mocked = mockRequestMethods(service);
|
||||
|
||||
const plain = mocked['Foo']().plain;
|
||||
expect(plain).toStrictEqual({
|
||||
foo: ['Hello'],
|
||||
});
|
||||
});
|
@ -41,6 +41,9 @@ describe('loadMethodsFromReflection', () => {
|
||||
expect(methods).toStrictEqual([{
|
||||
type: 'unary',
|
||||
fullPath: '/FooService/Foo',
|
||||
example: {
|
||||
foo: 'Hello',
|
||||
},
|
||||
}]);
|
||||
});
|
||||
});
|
||||
@ -76,6 +79,9 @@ describe('loadMethodsFromReflection', () => {
|
||||
expect(methods).toStrictEqual([{
|
||||
type: 'unary',
|
||||
fullPath: '/FooService/format',
|
||||
example: {
|
||||
foo: 'Hello',
|
||||
},
|
||||
}]);
|
||||
});
|
||||
});
|
||||
@ -123,9 +129,15 @@ describe('loadMethodsFromReflection', () => {
|
||||
expect(methods).toStrictEqual([{
|
||||
type: 'unary',
|
||||
fullPath: '/FooService/Foo',
|
||||
example: {
|
||||
foo: 'Hello',
|
||||
},
|
||||
}, {
|
||||
type: 'unary',
|
||||
fullPath: '/BarService/Bar',
|
||||
example: {
|
||||
bar: 'Hello',
|
||||
},
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
261
packages/insomnia/src/main/ipc/automock.ts
Normal file
261
packages/insomnia/src/main/ipc/automock.ts
Normal file
@ -0,0 +1,261 @@
|
||||
// From https://github.com/bloomrpc/bloomrpc-mock/blob/master/src/automock.ts
|
||||
// TODO simplify this and rename to generate example payload
|
||||
import { Enum, Field, MapField, Message, OneOf, Service, Type } from 'protobufjs';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export interface MethodPayload {
|
||||
plain: {[key: string]: any};
|
||||
message: Message;
|
||||
}
|
||||
|
||||
export interface ServiceMethodsPayload {
|
||||
[name: string]: () => MethodPayload;
|
||||
}
|
||||
|
||||
const enum MethodType {
|
||||
request,
|
||||
response
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock method response
|
||||
*/
|
||||
export function mockResponseMethods(
|
||||
service: Service,
|
||||
mocks?: void | {},
|
||||
) {
|
||||
return mockMethodReturnType(
|
||||
service,
|
||||
MethodType.response,
|
||||
mocks
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock methods request
|
||||
*/
|
||||
export function mockRequestMethods(
|
||||
service: Service,
|
||||
mocks?: void | {},
|
||||
) {
|
||||
return mockMethodReturnType(
|
||||
service,
|
||||
MethodType.request,
|
||||
mocks
|
||||
);
|
||||
}
|
||||
|
||||
function mockMethodReturnType(
|
||||
service: Service,
|
||||
type: MethodType,
|
||||
mocks?: void | {},
|
||||
): ServiceMethodsPayload {
|
||||
const root = service.root;
|
||||
const serviceMethods = service.methods;
|
||||
|
||||
return Object.keys(serviceMethods).reduce((methods: ServiceMethodsPayload, method: string) => {
|
||||
const serviceMethod = serviceMethods[method];
|
||||
|
||||
const methodMessageType = type === MethodType.request
|
||||
? serviceMethod.requestType
|
||||
: serviceMethod.responseType;
|
||||
|
||||
const messageType = root.lookupType(methodMessageType);
|
||||
|
||||
methods[method] = () => {
|
||||
let data = {};
|
||||
if (!mocks) {
|
||||
data = mockTypeFields(messageType, new StackDepth());
|
||||
}
|
||||
return { plain: data, message: messageType.fromObject(data) };
|
||||
};
|
||||
|
||||
return methods;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock a field type
|
||||
*/
|
||||
function mockTypeFields(type: Type, stackDepth: StackDepth): object {
|
||||
if (stackDepth.incrementAndCheckIfOverMax(`$type.${type.name}`)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const fieldsData: { [key: string]: any } = {};
|
||||
if (!type.fieldsArray) {
|
||||
return fieldsData;
|
||||
}
|
||||
return type.fieldsArray.reduce((data, field) => {
|
||||
const resolvedField = field.resolve();
|
||||
|
||||
if (resolvedField.parent !== resolvedField.resolvedType) {
|
||||
if (resolvedField.repeated) {
|
||||
data[resolvedField.name] = [mockField(resolvedField, stackDepth)];
|
||||
} else {
|
||||
data[resolvedField.name] = mockField(resolvedField, stackDepth);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}, fieldsData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock enum
|
||||
*/
|
||||
function mockEnum(enumType: Enum): number {
|
||||
const enumKey = Object.keys(enumType.values)[0];
|
||||
|
||||
return enumType.values[enumKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock a field
|
||||
*/
|
||||
function mockField(field: Field, stackDepth: StackDepth): any {
|
||||
if (stackDepth.incrementAndCheckIfOverMax(`$field.${field.name}`)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (field instanceof MapField) {
|
||||
return mockMapField(field, stackDepth);
|
||||
}
|
||||
|
||||
if (field.resolvedType instanceof Enum) {
|
||||
return mockEnum(field.resolvedType);
|
||||
}
|
||||
|
||||
if (isProtoType(field.resolvedType)) {
|
||||
return mockTypeFields(field.resolvedType, stackDepth);
|
||||
}
|
||||
|
||||
const mockPropertyValue = mockScalar(field.type, field.name);
|
||||
|
||||
if (mockPropertyValue === null) {
|
||||
const resolvedField = field.resolve();
|
||||
|
||||
return mockField(resolvedField, stackDepth);
|
||||
} else {
|
||||
return mockPropertyValue;
|
||||
}
|
||||
}
|
||||
|
||||
function mockMapField(field: MapField, stackDepth: StackDepth): any {
|
||||
let mockPropertyValue = null;
|
||||
if (field.resolvedType === null) {
|
||||
mockPropertyValue = mockScalar(field.type, field.name);
|
||||
}
|
||||
|
||||
if (mockPropertyValue === null) {
|
||||
const resolvedType = field.resolvedType;
|
||||
|
||||
if (resolvedType instanceof Type) {
|
||||
if (resolvedType.oneofs) {
|
||||
mockPropertyValue = pickOneOf(resolvedType.oneofsArray, stackDepth);
|
||||
} else {
|
||||
mockPropertyValue = mockTypeFields(resolvedType, stackDepth);
|
||||
}
|
||||
} else if (resolvedType instanceof Enum) {
|
||||
mockPropertyValue = mockEnum(resolvedType);
|
||||
} else if (resolvedType === null) {
|
||||
mockPropertyValue = {};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
[mockScalar(field.keyType, field.name)]: mockPropertyValue,
|
||||
};
|
||||
}
|
||||
|
||||
function isProtoType(resolvedType: Enum | Type | null): resolvedType is Type {
|
||||
if (!resolvedType) {
|
||||
return false;
|
||||
}
|
||||
const fieldsArray: keyof Type = 'fieldsArray';
|
||||
return resolvedType instanceof Type || (
|
||||
fieldsArray in resolvedType && Array.isArray(resolvedType[fieldsArray])
|
||||
);
|
||||
}
|
||||
|
||||
function pickOneOf(oneofs: OneOf[], stackDepth: StackDepth) {
|
||||
return oneofs.reduce((fields: {[key: string]: any}, oneOf) => {
|
||||
fields[oneOf.name] = mockField(oneOf.fieldsArray[0], stackDepth);
|
||||
return fields;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function mockScalar(type: string, fieldName: string): any {
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return interpretMockViaFieldName(fieldName);
|
||||
case 'number':
|
||||
return 10;
|
||||
case 'bool':
|
||||
return true;
|
||||
case 'int32':
|
||||
return 10;
|
||||
case 'int64':
|
||||
return 20;
|
||||
case 'uint32':
|
||||
return 100;
|
||||
case 'uint64':
|
||||
return 100;
|
||||
case 'sint32':
|
||||
return 100;
|
||||
case 'sint64':
|
||||
return 1200;
|
||||
case 'fixed32':
|
||||
return 1400;
|
||||
case 'fixed64':
|
||||
return 1500;
|
||||
case 'sfixed32':
|
||||
return 1600;
|
||||
case 'sfixed64':
|
||||
return 1700;
|
||||
case 'double':
|
||||
return 1.4;
|
||||
case 'float':
|
||||
return 1.1;
|
||||
case 'bytes':
|
||||
return Buffer.from([0xa1, 0xb2, 0xc3]);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to guess a mock value from the field name.
|
||||
* Default Hello.
|
||||
*/
|
||||
function interpretMockViaFieldName(fieldName: string): string {
|
||||
const fieldNameLower = fieldName.toLowerCase();
|
||||
|
||||
if (fieldNameLower.startsWith('id') || fieldNameLower.endsWith('id')) {
|
||||
return v4();
|
||||
}
|
||||
|
||||
return 'Hello';
|
||||
}
|
||||
|
||||
class StackDepth {
|
||||
private readonly depths: { [type: string]: number };
|
||||
readonly maxStackSize: number;
|
||||
|
||||
constructor(maxStackSize = 3) {
|
||||
this.depths = {};
|
||||
this.maxStackSize = maxStackSize;
|
||||
}
|
||||
|
||||
incrementAndCheckIfOverMax(key: string): boolean {
|
||||
if (this.depths[key] > this.maxStackSize) {
|
||||
return true;
|
||||
}
|
||||
if (!this.depths[key]) {
|
||||
this.depths[key] = 0;
|
||||
}
|
||||
this.depths[key]++;
|
||||
return false;
|
||||
}
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
import { Call, ClientDuplexStream, ClientReadableStream, ServiceError, StatusObject } from '@grpc/grpc-js';
|
||||
import { credentials, makeGenericClientConstructor, Metadata, status } from '@grpc/grpc-js';
|
||||
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 electron, { ipcMain } from 'electron';
|
||||
import { IpcMainEvent } from 'electron';
|
||||
import electron, { ipcMain, IpcMainEvent } from 'electron';
|
||||
import * as grpcReflection from 'grpc-reflection-js';
|
||||
|
||||
import type { RenderedGrpcRequest, RenderedGrpcRequestBody } from '../../common/render';
|
||||
@ -12,6 +10,7 @@ import type { GrpcRequest, GrpcRequestHeader } from '../../models/grpc-request';
|
||||
import { parseGrpcUrl } from '../../network/grpc/parse-grpc-url';
|
||||
import { writeProtoFile } from '../../network/grpc/write-proto-file';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { mockRequestMethods } from './automock';
|
||||
|
||||
const grpcCalls = new Map<string, Call>();
|
||||
export interface GrpcIpcRequestParams {
|
||||
@ -74,6 +73,7 @@ interface MethodDefs {
|
||||
responseStream: boolean;
|
||||
requestSerialize: (value: any) => Buffer;
|
||||
responseDeserialize: (value: Buffer) => any;
|
||||
example?: Record<string, any>;
|
||||
}
|
||||
const getMethodsFromReflection = async (host: string, metadata: GrpcRequestHeader[]): Promise<MethodDefs[]> => {
|
||||
try {
|
||||
@ -83,18 +83,29 @@ const getMethodsFromReflection = async (host: string, metadata: GrpcRequestHeade
|
||||
grpcOptions,
|
||||
filterDisabledMetaData(metadata)
|
||||
);
|
||||
const services = await client.listServices() as string[];
|
||||
const services = await client.listServices();
|
||||
const methodsPromises = services.map(async service => {
|
||||
const fileContainingSymbol = await client.fileContainingSymbol(service);
|
||||
const fullService = fileContainingSymbol.lookupService(service);
|
||||
const mockedRequestMethods = mockRequestMethods(fullService);
|
||||
const descriptorMessage = fileContainingSymbol.toDescriptor('proto3');
|
||||
const packageDefinition = protoLoader.loadFileDescriptorSetFromObject(descriptorMessage, {});
|
||||
const tryToGetMethods = () => {
|
||||
try {
|
||||
console.log('[grpc] loading service from reflection:', service);
|
||||
const packageDefinition = protoLoader.loadFileDescriptorSetFromObject(descriptorMessage, {});
|
||||
const serviceDefinition = asServiceDefinition(packageDefinition[service]);
|
||||
invariant(serviceDefinition, `'${service}' was not a valid ServiceDefinition`);
|
||||
const serviceMethods = Object.values(serviceDefinition);
|
||||
return serviceMethods;
|
||||
return serviceMethods.map(m => {
|
||||
const methodName = Object.keys(mockedRequestMethods).find(name => m.path.endsWith(`/${name}`));
|
||||
if (!methodName) {
|
||||
return m;
|
||||
}
|
||||
return {
|
||||
...m,
|
||||
example: mockedRequestMethods[methodName]().plain,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
@ -114,11 +125,13 @@ export const loadMethodsFromReflection = async (options: { url: string; metadata
|
||||
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 => {
|
||||
if (requestStream && responseStream) {
|
||||
|
@ -1,51 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import * as models from '../../../models';
|
||||
import type { Response } from '../../../models/response';
|
||||
import { useRequestMetaPatcher } from '../../hooks/use-request';
|
||||
import { selectActiveResponse } from '../../redux/selectors';
|
||||
import { CodeEditor } from '../codemirror/code-editor';
|
||||
|
||||
interface Props {
|
||||
content?: string;
|
||||
handleChange?: (arg0: string) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export const GRPCEditor: FunctionComponent<Props> = ({
|
||||
content,
|
||||
handleChange,
|
||||
readOnly,
|
||||
}) => {
|
||||
const response = useSelector(selectActiveResponse) as Response | null;
|
||||
const patchRequestMeta = useRequestMetaPatcher();
|
||||
const handleSetFilter = async (responseFilter: string) => {
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
const requestId = response.parentId;
|
||||
await patchRequestMeta(requestId, { responseFilter });
|
||||
const meta = await models.requestMeta.getByParentId(requestId);
|
||||
if (!meta) {
|
||||
return;
|
||||
}
|
||||
const responseFilterHistory = meta.responseFilterHistory.slice(0, 10);
|
||||
// Already in history or empty?
|
||||
if (!responseFilter || responseFilterHistory.includes(responseFilter)) {
|
||||
return;
|
||||
}
|
||||
responseFilterHistory.unshift(responseFilter);
|
||||
patchRequestMeta(requestId, { responseFilterHistory });
|
||||
};
|
||||
return (<CodeEditor
|
||||
defaultValue={content}
|
||||
onChange={handleChange}
|
||||
mode="application/json"
|
||||
enableNunjucks
|
||||
readOnly={readOnly}
|
||||
autoPrettify={readOnly}
|
||||
showPrettifyButton={!readOnly}
|
||||
updateFilter={handleSetFilter}
|
||||
/>);
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
import React, { FunctionComponent, useRef, useState } from 'react';
|
||||
import { useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
import { useMount } from 'react-use';
|
||||
import styled from 'styled-components';
|
||||
@ -18,6 +18,7 @@ import { RequestLoaderData } from '../../routes/request';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { PanelContainer, TabItem, Tabs } from '../base/tabs';
|
||||
import { GrpcSendButton } from '../buttons/grpc-send-button';
|
||||
import { CodeEditor, CodeEditorHandle } from '../codemirror/code-editor';
|
||||
import { OneLineEditor } from '../codemirror/one-line-editor';
|
||||
import { GrpcMethodDropdown } from '../dropdowns/grpc-method-dropdown/grpc-method-dropdown';
|
||||
import { ErrorBoundary } from '../error-boundary';
|
||||
@ -30,7 +31,6 @@ import { RequestRenderErrorModal } from '../modals/request-render-error-modal';
|
||||
import { SvgIcon } from '../svg-icon';
|
||||
import { Button } from '../themed-button';
|
||||
import { Tooltip } from '../tooltip';
|
||||
import { GrpcTabbedMessages } from '../viewers/grpc-tabbed-messages';
|
||||
import { EmptyStatePane } from './empty-state-pane';
|
||||
import { Pane, PaneBody, PaneHeader } from './pane';
|
||||
interface Props {
|
||||
@ -85,7 +85,7 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
|
||||
const methods = await window.main.grpc.loadMethods(activeRequest.protoFileId);
|
||||
setGrpcState({ ...grpcState, methods });
|
||||
});
|
||||
|
||||
const editorRef = useRef<CodeEditorHandle>(null);
|
||||
const gitVersion = useGitVCSVersion();
|
||||
const activeRequestSyncVersion = useActiveRequestSyncVCSVersion();
|
||||
const { workspaceId, requestId } = useParams() as { workspaceId: string; requestId: string };
|
||||
@ -175,6 +175,20 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="text"
|
||||
data-testid="button-use-request-stubs"
|
||||
disabled={!method?.example}
|
||||
onClick={() => {
|
||||
if (editorRef.current && method?.example) {
|
||||
editorRef.current.setValue(JSON.stringify(method.example, null, 2));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tooltip message="Click to replace body with an example" position="bottom" delay={500}>
|
||||
<i className="fa fa-code" />
|
||||
</Tooltip>
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
data-testid="button-server-reflection"
|
||||
@ -217,35 +231,67 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
|
||||
{methodType && (
|
||||
<Tabs aria-label="Grpc request pane tabs">
|
||||
<TabItem key="method-type" title={GrpcMethodTypeName[methodType]}>
|
||||
<GrpcTabbedMessages
|
||||
uniquenessKey={uniquenessKey}
|
||||
tabNamePrefix="Stream"
|
||||
messages={requestMessages}
|
||||
bodyText={activeRequest.body.text}
|
||||
handleBodyChange={text => patchRequest(requestId, { body: { text } })}
|
||||
showActions={running && canClientStream(methodType)}
|
||||
handleStream={async () => {
|
||||
const requestBody = await getRenderedGrpcRequestMessage({
|
||||
request: activeRequest,
|
||||
environmentId,
|
||||
purpose: RENDER_PURPOSE_SEND,
|
||||
});
|
||||
const preparedMessage = {
|
||||
body: requestBody,
|
||||
requestId,
|
||||
};
|
||||
window.main.grpc.sendMessage(preparedMessage);
|
||||
setGrpcState({
|
||||
...grpcState, requestMessages: [...requestMessages, {
|
||||
id: generateId(),
|
||||
text: preparedMessage.body.text || '',
|
||||
created: Date.now(),
|
||||
}],
|
||||
});
|
||||
|
||||
}}
|
||||
handleCommit={() => window.main.grpc.commit(requestId)}
|
||||
/>
|
||||
<>
|
||||
{running && canClientStream(methodType) && (
|
||||
<ActionButtonsContainer>
|
||||
<button
|
||||
className='btn btn--compact btn--clicky-small margin-left-sm bg-default'
|
||||
onClick={async () => {
|
||||
const requestBody = await getRenderedGrpcRequestMessage({
|
||||
request: activeRequest,
|
||||
environmentId,
|
||||
purpose: RENDER_PURPOSE_SEND,
|
||||
});
|
||||
const preparedMessage = {
|
||||
body: requestBody,
|
||||
requestId,
|
||||
};
|
||||
window.main.grpc.sendMessage(preparedMessage);
|
||||
setGrpcState({
|
||||
...grpcState, requestMessages: [...requestMessages, {
|
||||
id: generateId(),
|
||||
text: preparedMessage.body.text || '',
|
||||
created: Date.now(),
|
||||
}],
|
||||
});
|
||||
}}
|
||||
>
|
||||
Stream <i className='fa fa-plus' />
|
||||
</button>
|
||||
<button
|
||||
className='btn btn--compact btn--clicky-small margin-left-sm bg-surprise'
|
||||
onClick={() => window.main.grpc.commit(requestId)}
|
||||
>
|
||||
Commit <i className='fa fa-arrow-right' />
|
||||
</button>
|
||||
</ActionButtonsContainer>
|
||||
)}
|
||||
<Tabs key={uniquenessKey} aria-label="Grpc tabbed messages tabs" isNested>
|
||||
{[
|
||||
<TabItem key="body" title="Body">
|
||||
<CodeEditor
|
||||
ref={editorRef}
|
||||
defaultValue={activeRequest.body.text}
|
||||
onChange={text => patchRequest(requestId, { body: { text } })}
|
||||
mode="application/json"
|
||||
enableNunjucks
|
||||
showPrettifyButton={true}
|
||||
/>
|
||||
</TabItem>,
|
||||
...requestMessages.sort((a, b) => a.created - b.created).map((m, index) => (
|
||||
<TabItem key={m.id} title={`Stream ${index + 1}`}>
|
||||
<CodeEditor
|
||||
defaultValue={m.text}
|
||||
mode="application/json"
|
||||
enableNunjucks
|
||||
readOnly
|
||||
autoPrettify
|
||||
/>
|
||||
</TabItem>
|
||||
)),
|
||||
]}
|
||||
</Tabs>
|
||||
</>
|
||||
</TabItem>
|
||||
<TabItem key="headers" title="Headers">
|
||||
<PanelContainer className="tall wide">
|
||||
@ -292,3 +338,12 @@ export const GrpcRequestPane: FunctionComponent<Props> = ({
|
||||
</>
|
||||
);
|
||||
};
|
||||
const ActionButtonsContainer = styled.div({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
boxSizing: 'border-box',
|
||||
height: 'var(--line-height-sm)',
|
||||
borderBottom: '1px solid var(--hl-lg)',
|
||||
padding: 3,
|
||||
});
|
||||
|
@ -1,46 +1,40 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { useRouteLoaderData } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
|
||||
import { GrpcRequestState } from '../../routes/debug';
|
||||
import { WorkspaceLoaderData } from '../../routes/workspace';
|
||||
import { TabItem, Tabs } from '../base/tabs';
|
||||
import { CodeEditor } from '../codemirror/code-editor';
|
||||
import { GrpcStatusTag } from '../tags/grpc-status-tag';
|
||||
import { GrpcTabbedMessages } from '../viewers/grpc-tabbed-messages';
|
||||
import { Pane, PaneBody, PaneHeader } from './pane';
|
||||
interface Props {
|
||||
grpcState: GrpcRequestState;
|
||||
}
|
||||
|
||||
export const GrpcResponsePane: FunctionComponent<Props> = ({ grpcState }) => {
|
||||
const gitVersion = useGitVCSVersion();
|
||||
const activeRequestSyncVersion = useActiveRequestSyncVCSVersion();
|
||||
const {
|
||||
activeEnvironment,
|
||||
} = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData;
|
||||
const { requestId } = useParams() as { requestId: string };
|
||||
|
||||
// Force re-render when we switch requests, the environment gets modified, or the (Git|Sync)VCS version changes
|
||||
const uniquenessKey = `${activeEnvironment.modified}::${requestId}::${gitVersion}::${activeRequestSyncVersion}`;
|
||||
|
||||
const { responseMessages, status, error } = grpcState;
|
||||
export const GrpcResponsePane: FunctionComponent<Props> = ({ grpcState: { running, responseMessages, status, error } }) => {
|
||||
return (
|
||||
<Pane type="response">
|
||||
<PaneHeader className="row-spaced">
|
||||
<div className="no-wrap scrollable scrollable--no-bars pad-left">
|
||||
{grpcState.running && <i className='fa fa-refresh fa-spin margin-right-sm' />}
|
||||
{running && <i className='fa fa-refresh fa-spin margin-right-sm' />}
|
||||
{status && <GrpcStatusTag statusCode={status.code} statusMessage={status.details} />}
|
||||
{!status && error && <GrpcStatusTag statusMessage={error.message} />}
|
||||
</div>
|
||||
</PaneHeader>
|
||||
<PaneBody>
|
||||
{!!responseMessages.length && (
|
||||
<GrpcTabbedMessages
|
||||
uniquenessKey={uniquenessKey}
|
||||
tabNamePrefix="Response"
|
||||
messages={responseMessages}
|
||||
/>
|
||||
)}
|
||||
<PaneBody >
|
||||
{responseMessages.length
|
||||
? (<Tabs aria-label="Grpc tabbed messages tabs" isNested>
|
||||
{responseMessages.sort((a, b) => a.created - b.created).map((m, index) => (
|
||||
<TabItem key={m.id} title={`Response ${index + 1}`}>
|
||||
<CodeEditor
|
||||
defaultValue={m.text}
|
||||
mode="application/json"
|
||||
enableNunjucks
|
||||
readOnly
|
||||
autoPrettify
|
||||
/>
|
||||
</TabItem>))}
|
||||
</Tabs>)
|
||||
: null
|
||||
}
|
||||
</PaneBody>
|
||||
</Pane>
|
||||
);
|
||||
|
@ -1,93 +0,0 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { HandleGetRenderContext, HandleRender } from '../../../common/render';
|
||||
import { TabItem, Tabs } from '../base/tabs';
|
||||
import { GRPCEditor } from '../editors/grpc-editor';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
created: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
messages?: Message[];
|
||||
tabNamePrefix: 'Stream' | 'Response';
|
||||
bodyText?: string;
|
||||
uniquenessKey: string;
|
||||
handleBodyChange?: (arg0: string) => void;
|
||||
handleStream?: () => void;
|
||||
handleCommit?: () => void;
|
||||
showActions?: boolean;
|
||||
handleRender?: HandleRender;
|
||||
handleGetRenderContext?: HandleGetRenderContext;
|
||||
}
|
||||
|
||||
const ActionButtonsContainer = styled.div({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
boxSizing: 'border-box',
|
||||
height: 'var(--line-height-sm)',
|
||||
borderBottom: '1px solid var(--hl-lg)',
|
||||
padding: 3,
|
||||
});
|
||||
|
||||
export const GrpcTabbedMessages: FunctionComponent<Props> = ({
|
||||
showActions,
|
||||
bodyText,
|
||||
messages,
|
||||
tabNamePrefix,
|
||||
handleBodyChange,
|
||||
handleCommit,
|
||||
handleStream,
|
||||
uniquenessKey,
|
||||
}) => {
|
||||
const shouldShowBody = !!handleBodyChange;
|
||||
const orderedMessages = messages?.sort((a, b) => a.created - b.created) || [];
|
||||
|
||||
const tabItems = [];
|
||||
|
||||
if (shouldShowBody) {
|
||||
tabItems.push(
|
||||
<TabItem key="body" title="Body">
|
||||
<GRPCEditor content={bodyText} handleChange={handleBodyChange} />
|
||||
</TabItem>
|
||||
);
|
||||
}
|
||||
|
||||
orderedMessages.map((m, index) => tabItems.push(
|
||||
<TabItem key={m.id} title={`${tabNamePrefix} ${index + 1}`}>
|
||||
<GRPCEditor content={m.text} readOnly />
|
||||
</TabItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
{showActions && (
|
||||
<ActionButtonsContainer>
|
||||
{handleStream && (
|
||||
<button
|
||||
className='btn btn--compact btn--clicky-small margin-left-sm bg-default'
|
||||
onClick={handleStream}
|
||||
>
|
||||
Stream <i className='fa fa-plus' />
|
||||
</button>
|
||||
)}
|
||||
{handleCommit && (
|
||||
<button
|
||||
className='btn btn--compact btn--clicky-small margin-left-sm bg-surprise'
|
||||
onClick={handleCommit}
|
||||
>
|
||||
Commit <i className='fa fa-arrow-right' />
|
||||
</button>
|
||||
)}
|
||||
</ActionButtonsContainer>
|
||||
)}
|
||||
<Tabs key={uniquenessKey} aria-label="Grpc tabbed messages tabs" isNested>
|
||||
{tabItems}
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user