mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 22:45:25 +00:00
Add gRPC support (#5148)
This commit is contained in:
19
packages/bruno-app/src/utils/beta-features.js
Normal file
19
packages/bruno-app/src/utils/beta-features.js
Normal 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;
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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 };
|
||||
|
||||
33
packages/bruno-app/src/utils/filesystem.js
Normal file
33
packages/bruno-app/src/utils/filesystem.js
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
127
packages/bruno-app/src/utils/network/grpc-event-listeners.js
Normal file
127
packages/bruno-app/src/utils/network/grpc-event-listeners.js
Normal 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;
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user