Add gRPC support (#5148)

This commit is contained in:
sanish chirayath
2025-08-20 16:24:49 +05:30
committed by GitHub
parent e5a608f962
commit efb2e83ad9
102 changed files with 7756 additions and 841 deletions

View File

@@ -0,0 +1,19 @@
import { useSelector } from 'react-redux';
/**
* Beta features configuration object
* Contains all available beta feature keys
*/
export const BETA_FEATURES = Object.freeze({
GRPC: 'grpc'
});
/**
* Hook to check if a beta feature is enabled
* @param {string} featureName - The name of the beta feature
* @returns {boolean} - Whether the feature is enabled
*/
export const useBetaFeature = (featureName) => {
const preferences = useSelector((state) => state.app.preferences);
return preferences?.beta?.[featureName] || false;
};

View File

@@ -6,7 +6,7 @@ export const deleteUidsInItems = (items) => {
each(items, (item) => {
delete item.uid;
if (['http-request', 'graphql-request'].includes(item.type)) {
if (['http-request', 'graphql-request', 'grpc-request'].includes(item.type)) {
each(get(item, 'request.headers'), (header) => delete header.uid);
each(get(item, 'request.params'), (param) => delete param.uid);
each(get(item, 'request.vars.req'), (v) => delete v.uid);
@@ -29,7 +29,7 @@ export const deleteUidsInItems = (items) => {
*/
export const transformItem = (items = []) => {
each(items, (item) => {
if (['http-request', 'graphql-request'].includes(item.type)) {
if (['http-request', 'graphql-request', 'grpc-request'].includes(item.type)) {
if (item.type === 'graphql-request') {
item.type = 'graphql';
}
@@ -37,6 +37,10 @@ export const transformItem = (items = []) => {
if (item.type === 'http-request') {
item.type = 'http';
}
if (item.type === 'grpc-request') {
item.type = 'grpc';
}
}
if (item.items && item.items.length) {

View File

@@ -246,6 +246,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
di.request = {
url: si.request.url,
method: si.request.method,
methodType: si.request.methodType,
protoPath: si.request.protoPath,
headers: copyHeaders(si.request.headers),
params: copyParams(si.request.params),
body: {
@@ -257,7 +259,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
sparql: si.request.body.sparql,
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
multipartForm: copyMultipartFormParams(si.request.body.multipartForm),
file: copyFileParams(si.request.body.file)
file: copyFileParams(si.request.body.file),
grpc: si.request.body.grpc
},
script: si.request.script,
vars: si.request.vars,
@@ -402,6 +405,13 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
if (di.request.body.mode === 'json') {
di.request.body.json = replaceTabsWithSpaces(di.request.body.json);
}
if (di.request.body.mode === 'grpc') {
di.request.body.grpc = di.request.body.grpc.map(({name, content}, index) => ({
name: name ? name : `message ${index + 1}`,
content: replaceTabsWithSpaces(content)
}))
}
}
if (si.type == 'folder' && si?.root) {
@@ -554,6 +564,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
export const transformRequestToSaveToFilesystem = (item) => {
const _item = item.draft ? item.draft : item;
const itemToSave = {
uid: _item.uid,
type: _item.type,
@@ -576,16 +587,25 @@ export const transformRequestToSaveToFilesystem = (item) => {
}
};
each(_item.request.params, (param) => {
itemToSave.request.params.push({
uid: param.uid,
name: param.name,
value: param.value,
description: param.description,
type: param.type,
enabled: param.enabled
if (_item.type === 'grpc-request') {
itemToSave.request.methodType = _item.request.methodType;
itemToSave.request.protoPath = _item.request.protoPath;
delete itemToSave.request.params
}
// Only process params for non-gRPC requests
if (_item.type !== 'grpc-request') {
each(_item.request.params, (param) => {
itemToSave.request.params.push({
uid: param.uid,
name: param.name,
value: param.value,
description: param.description,
type: param.type,
enabled: param.enabled
});
});
});
}
each(_item.request.headers, (header) => {
itemToSave.request.headers.push({
@@ -604,6 +624,16 @@ export const transformRequestToSaveToFilesystem = (item) => {
};
}
if (itemToSave.request.body.mode === 'grpc') {
itemToSave.request.body = {
...itemToSave.request.body,
grpc: itemToSave.request.body.grpc.map(({name, content}, index) => ({
name: name ? name : `message ${index + 1}`,
content: replaceTabsWithSpaces(content)
}))
};
}
return itemToSave;
};
@@ -631,7 +661,7 @@ export const deleteItemInCollectionByPathname = (pathname, collection) => {
};
export const isItemARequest = (item) => {
return item.hasOwnProperty('request') && ['http-request', 'graphql-request'].includes(item.type) && !item.items;
return item.hasOwnProperty('request') && ['http-request', 'graphql-request', 'grpc-request'].includes(item.type) && !item.items;
};
export const isItemAFolder = (item) => {
@@ -813,6 +843,10 @@ export const getDefaultRequestPaneTab = (item) => {
if (item.type === 'graphql-request') {
return 'query';
}
if (item.type === 'grpc-request') {
return 'body';
}
};
export const getGlobalEnvironmentVariables = ({ globalEnvironments, activeGlobalEnvironmentUid }) => {
@@ -1158,4 +1192,8 @@ export const getRequestItemsForCollectionRun = ({ recursive, items = [], tags })
}
return requestItems;
};
export const getPropertyFromDraftOrRequest = (item, propertyKey, defaultValue = null) => {
return item.draft ? get(item, `draft.${propertyKey}`, defaultValue) : get(item, propertyKey, defaultValue);
};

View File

@@ -9,4 +9,32 @@ const isWindowsOS = () => {
const brunoPath = isWindowsOS() ? path.win32 : path.posix;
const getRelativePath = (absolutePath, collectionPath) => {
try {
const relativePath = brunoPath.relative(collectionPath, absolutePath);
return relativePath || absolutePath;
} catch (error) {
return absolutePath;
}
};
const getBasename = (filePath) => {
if (!filePath) {
return '';
}
const parts = filePath.split(path.sep);
return parts[parts.length - 1];
};
const getDirPath = (filePath) => {
const parts = filePath.split(path.sep);
parts.pop();
return parts.join(path.sep);
};
const getAbsoluteFilePath = (filePath, collectionPath) => {
return brunoPath.resolve(collectionPath, filePath);
};
export default brunoPath;
export { getRelativePath, getBasename, getDirPath, getAbsoluteFilePath };

View File

@@ -0,0 +1,33 @@
/**
* Filesystem utilities for the renderer process
* These functions communicate with the main process via IPC
*/
/**
* Check if a file exists
* @param {string} filePath - The file path to check
* @returns {Promise<boolean>} - True if file exists, false otherwise
*/
export const existsSync = async (filePath) => {
try {
return await window?.ipcRenderer?.invoke('renderer:exists-sync', filePath);
} catch (error) {
console.error('Error checking if file exists:', error);
return false;
}
};
/**
* Resolve a relative path against a base path
* @param {string} relativePath - The relative path to resolve
* @param {string} basePath - The base path to resolve against
* @returns {Promise<string>} - The resolved absolute path
*/
export const resolvePath = async (relativePath, basePath) => {
try {
return await window?.ipcRenderer?.invoke('renderer:resolve-path', relativePath, basePath);
} catch (error) {
console.error('Error resolving path:', error);
return relativePath;
}
};

View File

@@ -62,8 +62,7 @@ export const updateUidsInCollection = (_collection) => {
export const transformItemsInCollection = (collection) => {
const transformItems = (items = []) => {
each(items, (item) => {
if (['http', 'graphql'].includes(item.type)) {
if (['http', 'graphql', 'grpc'].includes(item.type)) {
item.type = `${item.type}-request`;
if (item.request.query) {

View File

@@ -0,0 +1,127 @@
import { useEffect } from 'react';
import { grpcResponseReceived, runGrpcRequestEvent } from 'providers/ReduxStore/slices/collections/index';
import { useDispatch } from 'react-redux';
import { isElectron } from 'utils/common/platform';
import { updateActiveConnectionsInStore } from 'providers/ReduxStore/slices/collections/actions';
const useGrpcEventListeners = () => {
const { ipcRenderer } = window;
const dispatch = useDispatch();
useEffect(() => {
if (!isElectron()) {
return () => {};
}
ipcRenderer.invoke('renderer:ready');
// Handle gRPC requestSent event
const removeGrpcRequestSentListener = ipcRenderer.on('grpc:request', (requestId, collectionUid, eventData) => {
dispatch(runGrpcRequestEvent({
eventType: "request",
itemUid: requestId,
collectionUid: collectionUid,
requestUid: requestId,
eventData
}));
});
const removeGrpcMessageSentListener = ipcRenderer.on('grpc:message', (requestId, collectionUid, eventData) => {
dispatch(runGrpcRequestEvent({
eventType: "message",
itemUid: requestId,
collectionUid: collectionUid,
requestUid: requestId,
eventData
}));
});
// Handle gRPC response event (for unary calls and streaming)
const removeGrpcResponseListener = ipcRenderer.on(`grpc:response`, (requestId, collectionUid, data) => {
dispatch(grpcResponseReceived({
itemUid: requestId,
collectionUid: collectionUid,
eventType: 'response',
eventData: data
}));
});
// Handle gRPC metadata
const removeGrpcMetadataListener = ipcRenderer.on(`grpc:metadata`, (requestId, collectionUid, data) => {
dispatch(grpcResponseReceived({
itemUid: requestId,
collectionUid: collectionUid,
eventType: 'metadata',
eventData: data
}));
});
// Handle gRPC status updates
const removeGrpcStatusListener = ipcRenderer.on(`grpc:status`, (requestId, collectionUid, data) => {
dispatch(grpcResponseReceived({
itemUid: requestId,
collectionUid: collectionUid,
eventType: 'status',
eventData: data
}));
});
// Handle gRPC errors
const removeGrpcErrorListener = ipcRenderer.on(`grpc:error`, (requestId, collectionUid, data) => {
dispatch(grpcResponseReceived({
itemUid: requestId,
collectionUid: collectionUid,
eventType: 'error',
eventData: data
}));
});
// Handle gRPC end event
const removeGrpcEndListener = ipcRenderer.on(`grpc:server-end-stream`, (requestId, collectionUid, data) => {
dispatch(grpcResponseReceived({
itemUid: requestId,
collectionUid: collectionUid,
eventType: 'end',
eventData: data
}));
});
// Handle gRPC cancel event
const removeGrpcCancelListener = ipcRenderer.on(`grpc:server-cancel-stream`, (requestId, collectionUid, data) => {
dispatch(grpcResponseReceived({
itemUid: requestId,
collectionUid: collectionUid,
eventType: 'cancel',
eventData: data
}));
});
const removeGrpcConnectionsChangedListener = ipcRenderer.on(`grpc:connections-changed`, (data) => {
dispatch(updateActiveConnectionsInStore(data));
});
return () => {
removeGrpcRequestSentListener();
removeGrpcMessageSentListener();
removeGrpcResponseListener();
removeGrpcMetadataListener();
removeGrpcStatusListener();
removeGrpcErrorListener();
removeGrpcEndListener();
removeGrpcCancelListener();
removeGrpcConnectionsChangedListener();
};
}, [isElectron]);
};
export default useGrpcEventListeners;

View File

@@ -1,5 +1,3 @@
import { safeStringifyJSON } from 'utils/common';
export const sendNetworkRequest = async (item, collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
if (['http-request', 'graphql-request'].includes(item.type)) {
@@ -27,6 +25,23 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
});
};
export const sendGrpcRequest = async (item, collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
startGrpcRequest(item, collection, environment, runtimeVariables)
.then((initialState) => {
// Return an initial state object to update the UI
// The real response data will be handled by event listeners
resolve({
...initialState,
timeline: []
});
})
.catch((err) => reject(err));
});
}
const sendHttpRequest = async (item, collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
@@ -60,3 +75,141 @@ export const cancelNetworkRequest = async (cancelTokenUid) => {
ipcRenderer.invoke('cancel-http-request', cancelTokenUid).then(resolve).catch(reject);
});
};
export const startGrpcRequest = async (item, collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
const request = item.draft ? item.draft : item;
ipcRenderer.invoke('grpc:start-connection', {
request,
collection,
environment,
runtimeVariables
})
.then(() => {
resolve();
})
.catch(err => {
reject(err);
});
});
};
/**
* Sends a message to an existing gRPC stream
* @param {string} requestId - The request ID to send a message to
* @param {Object} message - The message to send
* @returns {Promise<Object>} - The result of the send operation
*/
export const sendGrpcMessage = async (item, collectionUid, message) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('grpc:send-message', item.uid, collectionUid, message)
.then(resolve)
.catch(reject);
});
};
/**
* Cancels a running gRPC request
* @param {string} requestId - The request ID to cancel
* @returns {Promise<Object>} - The result of the cancel operation
*/
export const cancelGrpcRequest = async (requestId) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('grpc:cancel', requestId)
.then(resolve)
.catch(reject);
});
};
/**
* Ends a gRPC streaming request (client-streaming or bidirectional)
* @param {string} requestId - The request ID to end
* @returns {Promise<Object>} - The result of the end operation
*/
export const endGrpcStream = async (requestId) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('grpc:end', requestId)
.then(resolve)
.catch(reject);
});
};
export const loadGrpcMethodsFromProtoFile = async (filePath, includeDirs = []) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('grpc:load-methods-proto', { filePath, includeDirs }).then(resolve).catch(reject);
});
};
// export const getGrpcMethodsFromReflection = async (request, collection, environment, runtimeVariables) => {
// return new Promise((resolve, reject) => {
// const { ipcRenderer } = window;
// ipcRenderer.invoke('grpc:load-methods-reflection', { request, collection, environment, runtimeVariables }).then(resolve).catch(reject);
// });
// };
export const cancelGrpcConnection = async (connectionId) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('grpc:cancel-request', { requestId: connectionId }).then(resolve).catch(reject);
});
};
export const endGrpcConnection = async (connectionId) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('grpc:end-request', { requestId: connectionId }).then(resolve).catch(reject);
});
};
/**
* Check if a gRPC connection is active
* @param {string} connectionId - The connection ID to check
* @returns {Promise<boolean>} - Whether the connection is active
*/
export const isGrpcConnectionActive = async (connectionId) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('grpc:is-connection-active', connectionId)
.then(response => {
if (response.success) {
resolve(response.isActive);
} else {
// If there was an error, assume the connection is not active
console.error('Error checking connection status:', response.error);
resolve(false);
}
})
.catch(err => {
console.error('Failed to check connection status:', err);
// On error, assume the connection is not active
resolve(false);
});
});
};
/**
* Generates a sample gRPC message for a method
* @param {string} methodPath - The full gRPC method path
* @param {string|null} existingMessage - Optional existing message JSON string to use as a template
* @param {Object} options - Additional options for message generation
* @returns {Promise<Object>} The generated sample message or error
*/
export const generateGrpcSampleMessage = async (methodPath, existingMessage = null, options = {}) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('grpc:generate-sample-message', {
methodPath,
existingMessage,
options
})
.then(resolve)
.catch(reject);
});
};

View File

@@ -1,7 +1,7 @@
import find from 'lodash/find';
export const isItemARequest = (item) => {
return item.hasOwnProperty('request') && ['http-request', 'graphql-request'].includes(item.type);
return item.hasOwnProperty('request') && ['http-request', 'graphql-request', 'grpc-request'].includes(item.type);
};
export const isItemAFolder = (item) => {