drag(drop(node))}>
{indents && indents.length
@@ -211,6 +214,15 @@ const CollectionItem = ({ item, collection, searchText }) => {
>
New Folder
+
{
+ dropdownTippyRef.current.hide();
+ setRunCollectionModalOpen(true);
+ }}
+ >
+ Run
+
>
)}
{
dispatch(collectionRenamedEvent(val));
};
+ const _runFolderEvent = (val) => {
+ dispatch(runFolderEvent(val));
+ };
+
ipcRenderer.invoke('renderer:ready');
const removeListener1 = ipcRenderer.on('main:collection-opened', _openCollection);
@@ -116,6 +121,7 @@ const useCollectionTreeSync = () => {
const removeListener7 = ipcRenderer.on('main:http-request-queued', _httpRequestQueued);
const removeListener8 = ipcRenderer.on('main:test-results', _testResults);
const removeListener9 = ipcRenderer.on('main:collection-renamed', _collectionRenamed);
+ const removeListener10 = ipcRenderer.on('main:run-folder-event', _runFolderEvent);
return () => {
removeListener1();
@@ -127,6 +133,7 @@ const useCollectionTreeSync = () => {
removeListener7();
removeListener8();
removeListener9();
+ removeListener10();
};
}, [isElectron]);
};
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
index d51fc30d..9b7c1612 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -24,6 +24,7 @@ import { saveCollectionToIdb } from 'utils/idb';
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
import {
+ resetRunResults,
requestCancelled,
responseReceived,
newItem as _newItem,
@@ -138,6 +139,35 @@ export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) =>
.catch((err) => console.log(err));
};
+export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
+ const state = getState();
+ const collection = findCollectionByUid(state.collections.collections, collectionUid);
+
+ return new Promise((resolve, reject) => {
+ if (!collection) {
+ return reject(new Error('Collection not found'));
+ }
+
+ const collectionCopy = cloneDeep(collection);
+ const folder = findItemInCollection(collectionCopy, folderUid);
+
+ if (!folder) {
+ return reject(new Error('Folder not found'));
+ }
+
+ const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
+
+ dispatch(resetRunResults({
+ collectionUid: collection.uid
+ }));
+
+ ipcRenderer
+ .invoke('renderer:run-collection-folder', folder, collectionCopy, environment, recursive)
+ .then(resolve)
+ .catch(reject);
+ });
+};
+
export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -682,7 +712,8 @@ export const openCollectionEvent = (uid, pathname, name) => (dispatch, getState)
uid: uid,
name: name,
pathname: pathname,
- items: []
+ items: [],
+ showRunner: false
};
return new Promise((resolve, reject) => {
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
index a2020bc3..dbd2b080 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -849,6 +849,59 @@ export const collectionsSlice = createSlice({
if (collection) {
collection.name = newName;
}
+ },
+ toggleRunnerView: (state, action) => {
+ const { collectionUid } = action.payload;
+ const collection = findCollectionByUid(state.collections, collectionUid);
+
+ console.log('here');
+ if (collection) {
+ console.log('here2');
+ collection.showRunner = !collection.showRunner;
+ }
+ },
+ resetRunResults: (state, action) => {
+ const { collectionUid } = action.payload;
+ const collection = findCollectionByUid(state.collections, collectionUid);
+
+ if (collection) {
+ collection.runnerResult = null;
+ }
+ },
+ runFolderEvent: (state, action) => {
+ const { collectionUid, folderUid, itemUid, type } = action.payload;
+ const collection = findCollectionByUid(state.collections, collectionUid);
+
+ if (collection) {
+ const folder = findItemInCollection(collection, folderUid);
+ const request = findItemInCollection(collection, itemUid);
+
+ collection.runnerResult = collection.runnerResult || {items: []};
+
+ if(type === 'request-queued') {
+ collection.runnerResult.items.push({
+ uid: request.uid,
+ status: 'queued'
+ });
+ }
+
+ if(type === 'request-sent') {
+ const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
+ item.status = 'running';
+ item.requestSent = action.payload.requestSent;
+ }
+
+ if(type === 'response-received') {
+ const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
+ item.status = 'completed';
+ item.responseReceived = action.payload.responseReceived;
+ }
+
+ if(type === 'test-results') {
+ const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
+ item.testResults = action.payload.testResults;
+ }
+ }
}
}
});
@@ -901,7 +954,10 @@ export const {
collectionUnlinkDirectoryEvent,
collectionAddEnvFileEvent,
testResultsEvent,
- collectionRenamedEvent
+ collectionRenamedEvent,
+ toggleRunnerView,
+ resetRunResults,
+ runFolderEvent
} = collectionsSlice.actions;
export default collectionsSlice.reducer;
diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js
index 20afbc5f..191fefc2 100644
--- a/packages/bruno-app/src/utils/common/index.js
+++ b/packages/bruno-app/src/utils/common/index.js
@@ -36,11 +36,14 @@ export const safeParseJSON = (str) => {
}
};
-export const safeStringifyJSON = (obj) => {
+export const safeStringifyJSON = (obj, indent=false) => {
if(!obj) {
return obj;
}
try {
+ if(indent) {
+ return JSON.stringify(obj, null, 2);
+ }
return JSON.stringify(obj);
} catch (e) {
return obj;
diff --git a/packages/bruno-electron/src/ipc/network/helper.js b/packages/bruno-electron/src/ipc/network/helper.js
new file mode 100644
index 00000000..6be61ea9
--- /dev/null
+++ b/packages/bruno-electron/src/ipc/network/helper.js
@@ -0,0 +1,80 @@
+const {
+ each,
+ filter
+} = require('lodash');
+
+
+const sortCollection = (collection) => {
+ const items = collection.items || [];
+ let folderItems = filter(items, (item) => item.type === 'folder');
+ let requestItems = filter(items, (item) => item.type !== 'folder');
+
+ folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name));
+ requestItems = requestItems.sort((a, b) => a.seq - b.seq);
+
+ collection.items = folderItems.concat(requestItems);
+
+ each(folderItems, (item) => {
+ sortCollection(item);
+ });
+};
+
+const sortFolder = (folder = {}) => {
+ const items = folder.items || [];
+ let folderItems = filter(items, (item) => item.type === 'folder');
+ let requestItems = filter(items, (item) => item.type !== 'folder');
+
+ folderItems = folderItems.sort((a, b) => a.name.localeCompare(b.name));
+ requestItems = requestItems.sort((a, b) => a.seq - b.seq);
+
+ folder.items = folderItems.concat(requestItems);
+
+ each(folderItems, (item) => {
+ sortFolder(item);
+ });
+
+ return folder;
+};
+
+const findItemInCollection = (collection, itemId) => {
+ let item = null;
+
+ if (collection.uid === itemId) {
+ return collection;
+ }
+
+ if (collection.items && collection.items.length) {
+ collection.items.forEach((item) => {
+ if (item.uid === itemId) {
+ item = item;
+ } else if (item.type === 'folder') {
+ item = findItemInCollection(item, itemId);
+ }
+ });
+ }
+
+ return item;
+};
+
+const getAllRequestsInFolderRecursively = (folder = {}) => {
+ let requests = [];
+
+ if (folder.items && folder.items.length) {
+ folder.items.forEach((item) => {
+ if (item.type !== 'folder') {
+ requests.push(item);
+ } else {
+ requests = requests.concat(getAllRequestsInFolderRecursively(item));
+ }
+ });
+ }
+
+ return requests;
+};
+
+module.exports = {
+ sortCollection,
+ sortFolder,
+ findItemInCollection,
+ getAllRequestsInFolderRecursively
+};
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index 4312c55b..92f07554 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -9,6 +9,10 @@ const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-requ
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
const { uuid } = require('../../utils/common');
const interpolateVars = require('./interpolate-vars');
+const {
+ sortFolder,
+ getAllRequestsInFolderRecursively
+} = require('./helper');
// override the default escape function to prevent escaping
Mustache.escape = function (value) {
@@ -47,6 +51,22 @@ const getEnvVars = (environment = {}) => {
return envVars;
};
+const getSize = (data) => {
+ if(!data) {
+ return 0;
+ }
+
+ if(typeof data === 'string') {
+ return Buffer.byteLength(data, 'utf8');
+ }
+
+ if(typeof data === 'object') {
+ return Buffer.byteLength(JSON.stringify(data), 'utf8');
+ }
+
+ return 0;
+};
+
const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
// handler for sending http request
ipcMain.handle('send-http-request', async (event, item, collectionUid, collectionPath, environment) => {
@@ -197,6 +217,141 @@ const registerNetworkIpc = (mainWindow, watcher, lastOpenedCollections) => {
return Promise.reject(error);
}
});
+
+ ipcMain.handle('renderer:run-collection-folder', async (event, folder, collection, environment, recursive) => {
+ const collectionUid = collection.uid;
+ const collectionPath = collection.pathname;
+ const folderUid = folder.uid;
+
+ try {
+ const envVars = getEnvVars(environment);
+ let folderRequests = [];
+
+ if(recursive) {
+ let sortedFolder = sortFolder(folder);
+ folderRequests = getAllRequestsInFolderRecursively(sortedFolder);
+ console.log('-----sortedFolder------');
+ console.log(sortedFolder);
+ console.log('-----folderRequests------');
+ console.log(folderRequests);
+ } else {
+ each(folder.items, (item) => {
+ if(item.request) {
+ folderRequests.push(item);
+ }
+ });
+
+ // sort requests by seq property
+ folderRequests.sort((a, b) => {
+ return a.seq - b.seq;
+ });
+ }
+
+ for(let item of folderRequests) {
+ const itemUid = item.uid;
+ const eventData = {
+ collectionUid,
+ folderUid,
+ itemUid
+ };
+
+ try {
+ mainWindow.webContents.send('main:run-folder-event', {
+ type: 'request-queued',
+ ...eventData
+ });
+
+ const _request = item.draft ? item.draft.request : item.request;
+ const request = prepareRequest(_request);
+
+ // make axios work in node using form data
+ // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
+ if(request.headers && request.headers['content-type'] === 'multipart/form-data') {
+ const form = new FormData();
+ forOwn(request.data, (value, key) => {
+ form.append(key, value);
+ });
+ extend(request.headers, form.getHeaders());
+ request.data = form;
+ }
+
+ interpolateVars(request, envVars);
+
+ // todo:
+ // i have no clue why electron can't send the request object
+ // without safeParseJSON(safeStringifyJSON(request.data))
+ mainWindow.webContents.send('main:run-folder-event', {
+ type: 'request-sent',
+ requestSent: {
+ url: request.url,
+ method: request.method,
+ headers: request.headers,
+ data: safeParseJSON(safeStringifyJSON(request.data))
+ },
+ ...eventData
+ });
+
+ const timeStart = Date.now();
+ const response = await axios(request);
+ const timeEnd = Date.now();
+
+ if(request.script && request.script.length) {
+ let script = request.script + '\n if (typeof onResponse === "function") {onResponse(__brunoResponse);}';
+ const scriptRuntime = new ScriptRuntime();
+ const result = scriptRuntime.runResponseScript(script, response, envVars, collectionPath);
+
+ mainWindow.webContents.send('main:script-environment-update', {
+ environment: result.environment,
+ collectionUid
+ });
+ }
+
+ const testFile = get(item, 'request.tests');
+ if(testFile && testFile.length) {
+ const testRuntime = new TestRuntime();
+ const result = testRuntime.runTests(testFile, request, response, envVars, collectionPath);
+
+ mainWindow.webContents.send('main:run-folder-event', {
+ type: 'test-results',
+ testResults: result.results,
+ ...eventData
+ });
+ }
+
+ mainWindow.webContents.send('main:run-folder-event', {
+ type: 'response-received',
+ ...eventData,
+ responseReceived: {
+ status: response.status,
+ statusText: response.statusText,
+ headers: Object.entries(response.headers),
+ duration: timeEnd - timeStart,
+ size: response.headers['content-length'] || getSize(response.data),
+ data: response.data,
+ }
+ });
+ } catch (error) {
+ console.log(error);
+ mainWindow.webContents.send('main:run-folder-event', {
+ type: 'error',
+ error,
+ ...eventData
+ });
+ }
+ }
+ } catch (error) {
+ mainWindow.webContents.send('main:run-folder-event', {
+ type: 'error',
+ data: {
+ error : {
+ message: error.message
+ },
+ collectionUid,
+ folderUid
+ }
+ });
+ }
+ });
};
module.exports = registerNetworkIpc;
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index 948de409..5563d278 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -85,7 +85,11 @@ const collectionSchema = Yup.object({
.matches(/^[a-zA-Z0-9]*$/, 'uid must be alphanumeric')
.nullable(),
environments: environmentsSchema,
- pathname: Yup.string().nullable()
+ pathname: Yup.string().nullable(),
+ showRunner: Yup.boolean(),
+ runnerResult: Yup.object({
+ items: Yup.array()
+ })
}).noUnknown(true).strict();