From 05be59f00c9ee2adf1559dcd315e82a64136295b Mon Sep 17 00:00:00 2001 From: sanish chirayath Date: Mon, 27 Jan 2025 18:49:05 +0530 Subject: [PATCH 001/114] style: update StyledWrapper to use flex layout and adjust dialog positioning (#3888) --- .../bruno-app/src/components/CodeEditor/StyledWrapper.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js index edcee4cd9..9573022a4 100644 --- a/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js +++ b/packages/bruno-app/src/components/CodeEditor/StyledWrapper.js @@ -8,6 +8,8 @@ const StyledWrapper = styled.div` font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')}; line-break: anywhere; flex: 1 1 0; + display: flex; + flex-direction: column-reverse; } /* Removes the glow outline around the folded json */ @@ -26,6 +28,10 @@ const StyledWrapper = styled.div` .CodeMirror-dialog { overflow: visible; + position: relative; + top: unset; + left: unset; + input { background: transparent; border: 1px solid #d3d6db; From bfc110137129e7d4319694297af10a3baeb5cde6 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <161328623+sanjaikumar-bruno@users.noreply.github.com> Date: Tue, 28 Jan 2025 14:13:14 +0530 Subject: [PATCH 002/114] refactor: update GitHub Actions workflow to add permissions for checks and pull requests for the `cli-tests` job (#3844) --- .github/workflows/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aec3d68a0..c029b0224 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,6 +52,9 @@ jobs: cli-test: name: CLI Tests runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 From 956d5a38e9d3df82fcd5f065c10fde7e0320e511 Mon Sep 17 00:00:00 2001 From: Alex <59348559+AlexCQY@users.noreply.github.com> Date: Tue, 28 Jan 2025 17:06:29 +0800 Subject: [PATCH 003/114] fix: bump axios version to handle ivp6 (#3878) Co-authored-by: alex.chua --- package-lock.json | 13 ++++++++++++- packages/bruno-electron/package.json | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 98b220863..d8ffef63f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24210,7 +24210,7 @@ "@usebruno/vm2": "^3.9.13", "about-window": "^1.15.2", "aws4-axios": "^3.3.0", - "axios": "1.7.5", + "axios": "1.7.7", "axios-ntlm": "^1.4.2", "chai": "^4.3.7", "chokidar": "^3.5.3", @@ -24247,6 +24247,17 @@ "dmg-license": "^1.0.11" } }, + "packages/bruno-electron/node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "packages/bruno-electron/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index 16a032b3a..f554e54d9 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -33,7 +33,7 @@ "@usebruno/vm2": "^3.9.13", "about-window": "^1.15.2", "aws4-axios": "^3.3.0", - "axios": "1.7.5", + "axios": "1.7.7", "axios-ntlm": "^1.4.2", "chai": "^4.3.7", "chokidar": "^3.5.3", From 16e27d2ca4337d01a53e8b6716a659e98badff0a Mon Sep 17 00:00:00 2001 From: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com> Date: Tue, 28 Jan 2025 14:57:00 +0530 Subject: [PATCH 004/114] feat: make `BrunoResponse` callable to access body data using expressions fixes (#481) (#3710) * feat: make `BrunoResponse` callable to access body data using expressions --- packages/bruno-js/src/bruno-response.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/bruno-js/src/bruno-response.js b/packages/bruno-js/src/bruno-response.js index 9e68045d9..faa315235 100644 --- a/packages/bruno-js/src/bruno-response.js +++ b/packages/bruno-js/src/bruno-response.js @@ -1,3 +1,5 @@ +const { get } = require('@usebruno/query'); + class BrunoResponse { constructor(res) { this.res = res; @@ -6,6 +8,13 @@ class BrunoResponse { this.headers = res ? res.headers : null; this.body = res ? res.data : null; this.responseTime = res ? res.responseTime : null; + + // Make the instance callable + const callable = (...args) => get(this.body, ...args); + Object.setPrototypeOf(callable, this.constructor.prototype); + Object.assign(callable, this); + + return callable; } getStatus() { From 0633d45a10e79c5f617e03dbe2e434800a52ed37 Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 28 Jan 2025 18:55:59 +0530 Subject: [PATCH 005/114] upgraded axios libarary (#3899) --- package-lock.json | 37 +++++++++++++++++++++++++++---- packages/bruno-app/package.json | 1 - packages/bruno-cli/package.json | 3 +-- packages/bruno-js/package.json | 2 +- packages/bruno-tests/package.json | 2 +- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index d8ffef63f..f53c786a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24030,7 +24030,6 @@ "@usebruno/common": "0.1.0", "@usebruno/graphql-docs": "0.1.0", "@usebruno/schema": "0.7.0", - "axios": "1.7.5", "classnames": "^2.3.1", "codemirror": "5.65.2", "codemirror-graphql": "2.1.1", @@ -24147,7 +24146,7 @@ "@usebruno/lang": "0.12.0", "@usebruno/vm2": "^3.9.13", "aws4-axios": "^3.3.0", - "axios": "1.7.5", + "axios": "1.7.7", "axios-ntlm": "^1.4.2", "chai": "^4.3.7", "chalk": "^3.0.0", @@ -24168,6 +24167,16 @@ "bru": "bin/bru.js" } }, + "packages/bruno-cli/node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "packages/bruno-cli/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -24352,7 +24361,7 @@ "ajv": "^8.12.0", "ajv-formats": "^2.1.1", "atob": "^2.1.2", - "axios": "1.7.5", + "axios": "1.7.7", "btoa": "^1.2.1", "chai": "^4.3.7", "chai-string": "^1.5.0", @@ -24379,6 +24388,16 @@ "@usebruno/vm2": "^3.9.13" } }, + "packages/bruno-js/node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "packages/bruno-js/node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -24456,7 +24475,7 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "axios": "1.7.5", + "axios": "1.7.7", "body-parser": "1.20.3", "cookie-parser": "^1.4.6", "cors": "^2.8.5", @@ -24470,6 +24489,16 @@ "multer": "^1.4.5-lts.1" } }, + "packages/bruno-tests/node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "packages/bruno-tests/node_modules/fast-xml-parser": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.1.tgz", diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index d4fb47bcf..c824f13a8 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -20,7 +20,6 @@ "@usebruno/common": "0.1.0", "@usebruno/graphql-docs": "0.1.0", "@usebruno/schema": "0.7.0", - "axios": "1.7.5", "classnames": "^2.3.1", "codemirror": "5.65.2", "codemirror-graphql": "2.1.1", diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json index 8353e8ba8..cfeff6f63 100644 --- a/packages/bruno-cli/package.json +++ b/packages/bruno-cli/package.json @@ -52,7 +52,7 @@ "@usebruno/lang": "0.12.0", "@usebruno/vm2": "^3.9.13", "aws4-axios": "^3.3.0", - "axios": "1.7.5", + "axios": "1.7.7", "axios-ntlm": "^1.4.2", "chai": "^4.3.7", "chalk": "^3.0.0", @@ -66,7 +66,6 @@ "qs": "^6.11.0", "socks-proxy-agent": "^8.0.2", "tough-cookie": "^4.1.3", - "@usebruno/vm2": "^3.9.13", "xmlbuilder": "^15.1.1", "yargs": "^17.6.2" } diff --git a/packages/bruno-js/package.json b/packages/bruno-js/package.json index 8ad1d8e46..ad400ab58 100644 --- a/packages/bruno-js/package.json +++ b/packages/bruno-js/package.json @@ -21,7 +21,7 @@ "ajv": "^8.12.0", "ajv-formats": "^2.1.1", "atob": "^2.1.2", - "axios": "1.7.5", + "axios": "1.7.7", "btoa": "^1.2.1", "chai": "^4.3.7", "chai-string": "^1.5.0", diff --git a/packages/bruno-tests/package.json b/packages/bruno-tests/package.json index ad819bf1d..129b12a51 100644 --- a/packages/bruno-tests/package.json +++ b/packages/bruno-tests/package.json @@ -18,7 +18,7 @@ }, "homepage": "https://github.com/usebruno/bruno-testbench#readme", "dependencies": { - "axios": "1.7.5", + "axios": "1.7.7", "body-parser": "1.20.3", "cookie-parser": "^1.4.6", "cors": "^2.8.5", From b5bd259a1b36d1b5cdbd965bad9717775208eced Mon Sep 17 00:00:00 2001 From: Pragadesh-45 <54320162+Pragadesh-45@users.noreply.github.com> Date: Tue, 28 Jan 2025 21:32:51 +0530 Subject: [PATCH 006/114] fix: handle Windows paths in cloneItem and getDirectoryName functions (fixes: #3401) (#3646) * fix: handle Windows paths in cloneItem and getDirectoryName functions * chore: removed commented lines --------- Co-authored-by: Anoop M D --- .../ReduxStore/slices/collections/actions.js | 4 ++-- .../bruno-app/src/utils/common/platform.js | 22 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) 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 75c6f2cb9..7973e57ad 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -21,7 +21,7 @@ import { transformRequestToSaveToFilesystem } from 'utils/collections'; import { uuid, waitForNextTick } from 'utils/common'; -import { PATH_SEPARATOR, getDirectoryName } from 'utils/common/platform'; +import { PATH_SEPARATOR, getDirectoryName, isWindowsPath } from 'utils/common/platform'; import { cancelNetworkRequest, sendNetworkRequest } from 'utils/network'; import { @@ -494,7 +494,7 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat ); if (!reqWithSameNameExists) { const dirname = getDirectoryName(item.pathname); - const fullName = path.join(dirname, filename); + const fullName = isWindowsPath(item.pathname) ? path.win32.join(dirname, filename) : path.join(dirname, filename); const { ipcRenderer } = window; const requestItems = filter(parentItem.items, (i) => i.type !== 'folder'); itemToSave.seq = requestItems ? requestItems.length + 1 : 1; diff --git a/packages/bruno-app/src/utils/common/platform.js b/packages/bruno-app/src/utils/common/platform.js index ddfdb3a1f..c50ded79a 100644 --- a/packages/bruno-app/src/utils/common/platform.js +++ b/packages/bruno-app/src/utils/common/platform.js @@ -24,11 +24,25 @@ export const getSubdirectoriesFromRoot = (rootPath, pathname) => { return relativePath ? relativePath.split(path.sep) : []; }; -export const getDirectoryName = (pathname) => { - // convert to unix style path - pathname = slash(pathname); - return path.dirname(pathname); +export const isWindowsPath = (pathname) => { + + if (!isWindowsOS()) { + return false; + } + + // Check for Windows drive letter format (e.g., "C:\") + const hasDriveLetter = /^[a-zA-Z]:\\/.test(pathname); + + // Check for UNC path format (e.g., "\\server\share") a.k.a. network path || WSL path + const isUNCPath = pathname.startsWith('\\\\'); + + return hasDriveLetter || isUNCPath; +}; + + +export const getDirectoryName = (pathname) => { + return isWindowsPath(pathname) ? path.win32.dirname(pathname) : path.dirname(pathname); }; export const isWindowsOS = () => { From 9f5f975f705201484703f0b2e3f55e1ffc3a6939 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Wed, 29 Jan 2025 02:53:53 +0530 Subject: [PATCH 007/114] feat: async parser workers (#3834) (#3887) feat: async parser workers (#3834) Co-authored-by: lohit --- .../CollectionSettings/Auth/StyledWrapper.js | 4 +- .../CollectionSettings/Docs/StyledWrapper.js | 1 - .../CollectionSettings/Docs/index.js | 103 ++++++++-- .../Headers/StyledWrapper.js | 2 + .../CollectionSettings/Info/StyledWrapper.js | 13 -- .../CollectionSettings/Info/index.js | 39 ---- .../CollectionSettings/Overview/Info/index.js | 56 +++++ .../RequestsNotLoaded/StyledWrapper.js | 25 +++ .../Overview/RequestsNotLoaded/index.js | 50 +++++ .../Overview/StyledWrapper.js | 25 +++ .../CollectionSettings/Overview/index.js | 27 +++ .../Presets/StyledWrapper.js | 2 + .../Script/StyledWrapper.js | 2 + .../CollectionSettings/StyledWrapper.js | 2 - .../CollectionSettings/Tests/StyledWrapper.js | 4 +- .../CollectionSettings/Vars/StyledWrapper.js | 2 + .../components/CollectionSettings/index.js | 22 +- .../components/Documentation/StyledWrapper.js | 1 - .../src/components/MarkDown/StyledWrapper.js | 7 - .../RequestIsLoading/StyledWrapper.js | 19 ++ .../RequestTabPanel/RequestIsLoading/index.js | 47 +++++ .../RequestNotLoaded/StyledWrapper.js | 19 ++ .../RequestTabPanel/RequestNotLoaded/index.js | 89 ++++++++ .../src/components/RequestTabPanel/index.js | 16 ++ .../RequestTabs/RequestTab/SpecialTab.js | 8 + .../RequestTabs/RequestTab/index.js | 2 +- .../src/components/RunnerResults/index.jsx | 5 +- .../CollectionItemIcon/StyledWrapper.js | 12 ++ .../CollectionItemIcon/index.js | 21 ++ .../RunCollectionItem/StyledWrapper.js | 3 + .../CollectionItem/RunCollectionItem/index.js | 10 +- .../Collection/CollectionItem/index.js | 21 +- .../Sidebar/Collections/Collection/index.js | 61 +++--- .../bruno-app/src/components/Sidebar/index.js | 2 +- .../ReduxStore/slices/collections/actions.js | 34 +++- .../ReduxStore/slices/collections/index.js | 33 ++- .../src/providers/ReduxStore/slices/tabs.js | 2 +- packages/bruno-app/src/themes/dark.js | 20 +- packages/bruno-app/src/themes/light.js | 17 +- .../bruno-app/src/utils/collections/index.js | 16 +- packages/bruno-app/src/utils/common/ipc.js | 14 ++ .../bruno-electron/src/app/collections.js | 8 +- packages/bruno-electron/src/app/watcher.js | 116 +++++++---- packages/bruno-electron/src/bru/index.js | 80 +++++++- .../bruno-electron/src/bru/workers/index.js | 57 ++++++ .../src/bru/workers/scripts/bru-to-json.js | 14 ++ .../src/bru/workers/scripts/json-to-bru.js | 13 ++ packages/bruno-electron/src/ipc/collection.js | 192 +++++++++++++++--- .../bruno-electron/src/utils/collection.js | 60 +++++- .../bruno-electron/src/utils/filesystem.js | 48 ++++- packages/bruno-electron/src/workers/index.js | 60 ++++++ .../tests/utils/collection.spec.js | 121 +++++++++++ .../bruno-tests/collection_oauth2/bruno.json | 8 +- 53 files changed, 1377 insertions(+), 258 deletions(-) delete mode 100644 packages/bruno-app/src/components/CollectionSettings/Info/StyledWrapper.js delete mode 100644 packages/bruno-app/src/components/CollectionSettings/Info/index.js create mode 100644 packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js create mode 100644 packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js create mode 100644 packages/bruno-app/src/components/CollectionSettings/Overview/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/CollectionSettings/Overview/index.js create mode 100644 packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/index.js create mode 100644 packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js create mode 100644 packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemIcon/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemIcon/index.js create mode 100644 packages/bruno-app/src/utils/common/ipc.js create mode 100644 packages/bruno-electron/src/bru/workers/index.js create mode 100644 packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js create mode 100644 packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js create mode 100644 packages/bruno-electron/src/workers/index.js create mode 100644 packages/bruno-electron/tests/utils/collection.spec.js diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Auth/StyledWrapper.js index e49220854..b7e4b56c7 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/StyledWrapper.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/StyledWrapper.js @@ -1,5 +1,7 @@ import styled from 'styled-components'; -const Wrapper = styled.div``; +const Wrapper = styled.div` + max-width: 800px; +`; export default Wrapper; diff --git a/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js index 262f068e7..afe08bcba 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js +++ b/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js @@ -8,7 +8,6 @@ const StyledWrapper = styled.div` } .editing-mode { cursor: pointer; - color: ${(props) => props.theme.colors.text.yellow}; } `; diff --git a/packages/bruno-app/src/components/CollectionSettings/Docs/index.js b/packages/bruno-app/src/components/CollectionSettings/Docs/index.js index 23dbe9e70..2d869de65 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Docs/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Docs/index.js @@ -8,6 +8,7 @@ import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/acti import Markdown from 'components/MarkDown'; import CodeEditor from 'components/CodeEditor'; import StyledWrapper from './StyledWrapper'; +import { IconEdit, IconX, IconFileText } from '@tabler/icons'; const Docs = ({ collection }) => { const dispatch = useDispatch(); @@ -29,35 +30,95 @@ const Docs = ({ collection }) => { ); }; - const onSave = () => dispatch(saveCollectionRoot(collection.uid)); + const handleDiscardChanges = () => { + dispatch( + updateCollectionDocs({ + collectionUid: collection.uid, + docs: docs + }) + ); + toggleViewMode(); + } + + const onSave = () => { + dispatch(saveCollectionRoot(collection.uid)); + toggleViewMode(); + } return ( -
- {isEditing ? 'Preview' : 'Edit'} -
- - {isEditing ? ( -
- - +
+
+ + Documentation
+
+ {isEditing ? ( + <> +
+ +
+ + + ) : ( +
+ +
+ )} +
+
+ {isEditing ? ( + ) : ( - +
+
+ { + docs?.length > 0 ? + + : + + } +
+
)} ); }; export default Docs; + + +const documentationPlaceholder = ` +Welcome to your collection documentation! This space is designed to help you document your API collection effectively. + +## Overview +Use this section to provide a high-level overview of your collection. You can describe: +- The purpose of these API endpoints +- Key features and functionalities +- Target audience or users + +## Best Practices +- Keep documentation up to date +- Include request/response examples +- Document error scenarios +- Add relevant links and references + +## Markdown Support +This documentation supports Markdown formatting! You can use: +- **Bold** and *italic* text +- \`code blocks\` and syntax highlighting +- Tables and lists +- [Links](https://example.com) +- And more! +`; diff --git a/packages/bruno-app/src/components/CollectionSettings/Headers/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Headers/StyledWrapper.js index 9f723cb81..c4d03c5ed 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Headers/StyledWrapper.js +++ b/packages/bruno-app/src/components/CollectionSettings/Headers/StyledWrapper.js @@ -1,6 +1,8 @@ import styled from 'styled-components'; const Wrapper = styled.div` + max-width: 800px; + table { width: 100%; border-collapse: collapse; diff --git a/packages/bruno-app/src/components/CollectionSettings/Info/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Info/StyledWrapper.js deleted file mode 100644 index 7fd98347c..000000000 --- a/packages/bruno-app/src/components/CollectionSettings/Info/StyledWrapper.js +++ /dev/null @@ -1,13 +0,0 @@ -import styled from 'styled-components'; - -const StyledWrapper = styled.div` - table { - td { - &:first-child { - width: 120px; - } - } - } -`; - -export default StyledWrapper; diff --git a/packages/bruno-app/src/components/CollectionSettings/Info/index.js b/packages/bruno-app/src/components/CollectionSettings/Info/index.js deleted file mode 100644 index 3b0a1297b..000000000 --- a/packages/bruno-app/src/components/CollectionSettings/Info/index.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import StyledWrapper from './StyledWrapper'; -import { getTotalRequestCountInCollection } from 'utils/collections/'; - -const Info = ({ collection }) => { - const totalRequestsInCollection = getTotalRequestCountInCollection(collection); - - return ( - -
General information about the collection.
- - - - - - - - - - - - - - - - - - - - - - - -
Name :{collection.name}
Location :{collection.pathname}
Ignored files :{collection.brunoConfig?.ignore?.map((x) => `'${x}'`).join(', ')}
Environments :{collection.environments?.length || 0}
Requests :{totalRequestsInCollection}
-
- ); -}; - -export default Info; diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js new file mode 100644 index 000000000..86bf2308f --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js @@ -0,0 +1,56 @@ +import React from 'react'; +import { getTotalRequestCountInCollection } from 'utils/collections/'; +import { IconFolder, IconFileOff, IconWorld, IconApi } from '@tabler/icons'; + +const Info = ({ collection }) => { + const totalRequestsInCollection = getTotalRequestCountInCollection(collection); + + return ( +
+
+
+ {/* Location Row */} +
+
+ +
+
+
Location
+
+ {collection.pathname} +
+
+
+ + {/* Environments Row */} +
+
+ +
+
+
Environments
+
+ {collection.environments?.length || 0} environment{collection.environments?.length !== 1 ? 's' : ''} configured +
+
+
+ + {/* Requests Row */} +
+
+ +
+
+
Requests
+
+ {totalRequestsInCollection} request{totalRequestsInCollection !== 1 ? 's' : ''} in collection +
+
+
+
+
+
+ ); +}; + +export default Info; diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/StyledWrapper.js new file mode 100644 index 000000000..e9a9cd06f --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/StyledWrapper.js @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + &.card { + background-color: ${(props) => props.theme.requestTabPanel.card.bg}; + + .title { + border-top: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border}; + border-left: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border}; + border-right: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border}; + + border-top-left-radius: 3px; + border-top-right-radius: 3px; + } + + .table { + thead { + background-color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.bg}; + color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.color}; + } + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js new file mode 100644 index 000000000..c15b36cd8 --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { flattenItems } from "utils/collections"; +import { IconAlertTriangle } from '@tabler/icons'; +import StyledWrapper from "./StyledWrapper"; + +const RequestsNotLoaded = ({ collection }) => { + const flattenedItems = flattenItems(collection.items); + const itemsFailedLoading = flattenedItems?.filter(item => item?.partial && !item?.loading); + + if (!itemsFailedLoading?.length) { + return null; + } + + return ( + +
+ + Following requests were not loaded +
+ + + + + + + + + {flattenedItems?.map((item, index) => ( + item?.partial && !item?.loading ? ( + + + + + ) : null + ))} + +
+ Pathname + + Size +
+ {item?.pathname?.split(`${collection?.pathname}/`)?.[1]} + + {item?.size?.toFixed?.(2)} MB +
+
+ ); +}; + +export default RequestsNotLoaded; diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Overview/StyledWrapper.js new file mode 100644 index 000000000..4d77f2600 --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Overview/StyledWrapper.js @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + .partial { + color: ${(props) => props.theme.colors.text.yellow}; + opacity: 0.8; + } + + .loading { + color: ${(props) => props.theme.colors.text.muted}; + opacity: 0.8; + } + + .completed { + color: ${(props) => props.theme.colors.text.green}; + opacity: 0.8; + } + + .failed { + color: ${(props) => props.theme.colors.text.danger}; + opacity: 0.8; + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/index.js new file mode 100644 index 000000000..87b461e9c --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Overview/index.js @@ -0,0 +1,27 @@ +import StyledWrapper from "./StyledWrapper"; +import Docs from "../Docs"; +import Info from "./Info"; +import { IconBox } from '@tabler/icons'; +import RequestsNotLoaded from "./RequestsNotLoaded"; + +const Overview = ({ collection }) => { + return ( +
+
+
+
+ + {collection?.name} +
+ + +
+
+ +
+
+
+ ); +} + +export default Overview; \ No newline at end of file diff --git a/packages/bruno-app/src/components/CollectionSettings/Presets/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Presets/StyledWrapper.js index 602851baa..db26e863b 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Presets/StyledWrapper.js +++ b/packages/bruno-app/src/components/CollectionSettings/Presets/StyledWrapper.js @@ -1,6 +1,8 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` + max-width: 800px; + .settings-label { width: 110px; } diff --git a/packages/bruno-app/src/components/CollectionSettings/Script/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Script/StyledWrapper.js index 66ba1ed3d..03aed74aa 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Script/StyledWrapper.js +++ b/packages/bruno-app/src/components/CollectionSettings/Script/StyledWrapper.js @@ -1,6 +1,8 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` + max-width: 800px; + div.CodeMirror { height: inherit; } diff --git a/packages/bruno-app/src/components/CollectionSettings/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/StyledWrapper.js index b88a31e0d..90ab7fee5 100644 --- a/packages/bruno-app/src/components/CollectionSettings/StyledWrapper.js +++ b/packages/bruno-app/src/components/CollectionSettings/StyledWrapper.js @@ -1,8 +1,6 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` - max-width: 800px; - div.tabs { div.tab { padding: 6px 0px; diff --git a/packages/bruno-app/src/components/CollectionSettings/Tests/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Tests/StyledWrapper.js index ec278887d..b9014ebd5 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Tests/StyledWrapper.js +++ b/packages/bruno-app/src/components/CollectionSettings/Tests/StyledWrapper.js @@ -1,5 +1,7 @@ import styled from 'styled-components'; -const StyledWrapper = styled.div``; +const StyledWrapper = styled.div` + max-width: 800px; +`; export default StyledWrapper; diff --git a/packages/bruno-app/src/components/CollectionSettings/Vars/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Vars/StyledWrapper.js index 44b01b464..26459a3c6 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Vars/StyledWrapper.js +++ b/packages/bruno-app/src/components/CollectionSettings/Vars/StyledWrapper.js @@ -1,6 +1,8 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` + max-width: 800px; + div.title { color: var(--color-tab-inactive); } diff --git a/packages/bruno-app/src/components/CollectionSettings/index.js b/packages/bruno-app/src/components/CollectionSettings/index.js index b849d6b18..7d5d60574 100644 --- a/packages/bruno-app/src/components/CollectionSettings/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/index.js @@ -12,12 +12,11 @@ import Headers from './Headers'; import Auth from './Auth'; import Script from './Script'; import Test from './Tests'; -import Docs from './Docs'; import Presets from './Presets'; -import Info from './Info'; import StyledWrapper from './StyledWrapper'; import Vars from './Vars/index'; import DotIcon from 'components/Icons/Dot'; +import Overview from './Overview/index'; const ContentIndicator = () => { return ( @@ -97,6 +96,9 @@ const CollectionSettings = ({ collection }) => { const getTabPanel = (tab) => { switch (tab) { + case 'overview': { + return ; + } case 'headers': { return ; } @@ -128,12 +130,6 @@ const CollectionSettings = ({ collection }) => { /> ); } - case 'docs': { - return ; - } - case 'info': { - return ; - } } }; @@ -146,6 +142,9 @@ const CollectionSettings = ({ collection }) => { return (
+
setTab('overview')}> + Overview +
setTab('headers')}> Headers {activeHeadersCount > 0 && {activeHeadersCount}} @@ -177,13 +176,6 @@ const CollectionSettings = ({ collection }) => { Client Certificates {clientCertConfig.length > 0 && }
-
setTab('docs')}> - Docs - {hasDocs && } -
-
setTab('info')}> - Info -
{getTabPanel(tab)}
diff --git a/packages/bruno-app/src/components/Documentation/StyledWrapper.js b/packages/bruno-app/src/components/Documentation/StyledWrapper.js index f159d94dc..af80d4c08 100644 --- a/packages/bruno-app/src/components/Documentation/StyledWrapper.js +++ b/packages/bruno-app/src/components/Documentation/StyledWrapper.js @@ -3,7 +3,6 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` .editing-mode { cursor: pointer; - color: ${(props) => props.theme.colors.text.yellow}; } `; diff --git a/packages/bruno-app/src/components/MarkDown/StyledWrapper.js b/packages/bruno-app/src/components/MarkDown/StyledWrapper.js index fa1269e14..0ac61b4e5 100644 --- a/packages/bruno-app/src/components/MarkDown/StyledWrapper.js +++ b/packages/bruno-app/src/components/MarkDown/StyledWrapper.js @@ -9,7 +9,6 @@ const StyledMarkdownBodyWrapper = styled.div` box-sizing: border-box; height: 100%; margin: 0 auto; - padding-top: 0.5rem; font-size: 0.875rem; h1 { @@ -80,12 +79,6 @@ const StyledMarkdownBodyWrapper = styled.div` } } } - - @media (max-width: 767px) { - .markdown-body { - padding: 15px; - } - } `; export default StyledMarkdownBodyWrapper; diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/StyledWrapper.js new file mode 100644 index 000000000..ff6c48575 --- /dev/null +++ b/packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/StyledWrapper.js @@ -0,0 +1,19 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + div.card { + background: ${(props) => props.theme.requestTabPanel.card.bg}; + border: 1px solid ${(props) => props.theme.requestTabPanel.card.border}; + + div.hr { + border-bottom: 1px solid ${(props) => props.theme.requestTabPanel.card.hr}; + height: 1px; + } + + div.border-top { + border-top: 1px solid ${(props) => props.theme.requestTabPanel.card.border}; + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/index.js b/packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/index.js new file mode 100644 index 000000000..9d2ff1346 --- /dev/null +++ b/packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/index.js @@ -0,0 +1,47 @@ +import { IconLoader2, IconFile } from '@tabler/icons'; +import StyledWrapper from './StyledWrapper'; + +const RequestIsLoading = ({ item }) => { + return +
+
+
+
+ + File Info +
+
+ +
+ Name: +
+ {item?.name} +
+
+ +
+ Path: +
+ {item?.pathname} +
+
+ +
+ Size: +
+ {item?.size?.toFixed?.(2)} MB +
+
+ +
+
+ + Loading... +
+
+
+
+ +} + +export default RequestIsLoading; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/StyledWrapper.js b/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/StyledWrapper.js new file mode 100644 index 000000000..ff6c48575 --- /dev/null +++ b/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/StyledWrapper.js @@ -0,0 +1,19 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + div.card { + background: ${(props) => props.theme.requestTabPanel.card.bg}; + border: 1px solid ${(props) => props.theme.requestTabPanel.card.border}; + + div.hr { + border-bottom: 1px solid ${(props) => props.theme.requestTabPanel.card.hr}; + height: 1px; + } + + div.border-top { + border-top: 1px solid ${(props) => props.theme.requestTabPanel.card.border}; + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js b/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js new file mode 100644 index 000000000..1a951b624 --- /dev/null +++ b/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js @@ -0,0 +1,89 @@ +import { IconLoader2, IconFile } from '@tabler/icons'; +import { loadRequest, loadRequestViaWorker } from 'providers/ReduxStore/slices/collections/actions'; +import { useDispatch } from 'react-redux'; +import StyledWrapper from './StyledWrapper'; + +const RequestNotLoaded = ({ collection, item }) => { + const dispatch = useDispatch(); + const handleLoadRequestViaWorker = () => { + !item?.loading && dispatch(loadRequestViaWorker({ collectionUid: collection?.uid, pathname: item?.pathname })); + } + + const handleLoadRequest = () => { + !item?.loading && dispatch(loadRequest({ collectionUid: collection?.uid, pathname: item?.pathname })); + } + + return +
+
+
+
+ + File Info +
+
+ +
+ Name: +
{item?.name}
+
+ +
+ Path: +
{item?.pathname}
+
+ +
+ Size: +
{item?.size?.toFixed?.(2)} MB
+
+ + {!item?.error && ( + <> +
+
+ Due to its large size, this request wasn't loaded automatically. +
+
+
+ + + May cause the app to freeze temporarily while it runs. + +
+
+ + + Runs in background. + +
+
+ + )} + + {item?.loading && ( + <> +
+
+ + Loading... +
+ + )} +
+
+
+ +} + +export default RequestNotLoaded; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js index 4bcfff1c3..d7690e08a 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/index.js @@ -22,6 +22,9 @@ import SecuritySettings from 'components/SecuritySettings'; import FolderSettings from 'components/FolderSettings'; import { getGlobalEnvironmentVariables, getGlobalEnvironmentVariablesMasked } from 'utils/collections/index'; import { produce } from 'immer'; +import CollectionOverview from 'components/CollectionSettings/Overview'; +import RequestNotLoaded from './RequestNotLoaded'; +import RequestIsLoading from './RequestIsLoading'; const MIN_LEFT_PANE_WIDTH = 300; const MIN_RIGHT_PANE_WIDTH = 350; @@ -153,6 +156,11 @@ const RequestTabPanel = () => { if (focusedTab.type === 'collection-settings') { return ; } + + if (focusedTab.type === 'collection-overview') { + return ; + } + if (focusedTab.type === 'folder-settings') { const folder = findItemInCollection(collection, focusedTab.folderUid); return ; @@ -167,6 +175,14 @@ const RequestTabPanel = () => { return ; } + if (item?.partial) { + return + } + + if (item?.loading) { + return + } + const handleRun = async () => { dispatch(sendRequest(item, collection.uid)).catch((err) => toast.custom((t) => toast.dismiss(t.id)} />, { diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js index c5d09faa8..1cbb0aa05 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js @@ -13,6 +13,14 @@ const SpecialTab = ({ handleCloseClick, type, tabName }) => { ); } + case 'collection-overview': { + return ( + <> + + Collection + + ); + } case 'security-settings': { return ( <> diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index e73313c13..2d74a4290 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -70,7 +70,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi }; const folder = folderUid ? findItemInCollection(collection, folderUid) : null; - if (['collection-settings', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) { + if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) { return ( { // convert to unix style path @@ -106,6 +107,8 @@ export default function RunnerResults({ collection }) { return (item.status !== 'error' && item.testStatus === 'fail') || item.assertionStatus === 'fail'; }); + let isCollectionLoading = areItemsLoading(collection); + if (!items || !items.length) { return ( @@ -116,7 +119,7 @@ export default function RunnerResults({ collection }) {
You have {totalRequestsInCollection} requests in this collection.
- + {isCollectionLoading ?
Requests in this collection are still loading.
: null}
props.theme.colors.text.yellow}; + } + .error { + color: ${(props) => props.theme.colors.text.danger}; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemIcon/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemIcon/index.js new file mode 100644 index 000000000..82d87aa7d --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemIcon/index.js @@ -0,0 +1,21 @@ +import RequestMethod from "../RequestMethod"; +import { IconLoader2, IconAlertTriangle, IconAlertCircle } from '@tabler/icons'; +import StyledWrapper from "./StyledWrapper"; + +const CollectionItemIcon = ({ item }) => { + if (item?.error) { + return ; + } + + if (item?.loading) { + return ; + } + + if (item?.partial) { + return ; + } + + return ; +}; + +export default CollectionItemIcon; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js index 3b6e08f42..e7dd94d2f 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js @@ -4,6 +4,9 @@ const Wrapper = styled.div` .bruno-modal-content { padding-bottom: 1rem; } + .warning { + color: ${(props) => props.theme.colors.text.danger}; + } `; export default Wrapper; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js index 4a81f59af..cfd236f8c 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js @@ -7,6 +7,7 @@ import { addTab } from 'providers/ReduxStore/slices/tabs'; import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions'; import { flattenItems } from 'utils/collections'; import StyledWrapper from './StyledWrapper'; +import { areItemsLoading } from 'utils/collections'; const RunCollectionItem = ({ collection, item, onClose }) => { const dispatch = useDispatch(); @@ -32,6 +33,10 @@ const RunCollectionItem = ({ collection, item, onClose }) => { const flattenedItems = flattenItems(item ? item.items : collection.items); const recursiveRunLength = getRequestsCount(flattenedItems); + const isFolderLoading = areItemsLoading(item); + console.log(item); + console.log(isFolderLoading); + return ( @@ -44,13 +49,12 @@ const RunCollectionItem = ({ collection, item, onClose }) => { ({runLength} requests)
This will only run the requests in this folder.
-
Recursive Run ({recursiveRunLength} requests)
-
This will run all the requests in this folder and all its subfolders.
- +
This will run all the requests in this folder and all its subfolders.
+ {isFolderLoading ?
Requests in this folder are still loading.
: null}
- + {item.name} @@ -421,4 +414,4 @@ const CollectionItem = ({ item, collection, searchText }) => { ); }; -export default CollectionItem; +export default CollectionItem; \ No newline at end of file diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index 3b814a7e5..1b16f4eea 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -3,10 +3,10 @@ import classnames from 'classnames'; import { uuid } from 'utils/common'; import filter from 'lodash/filter'; import { useDrop } from 'react-dnd'; -import { IconChevronRight, IconDots } from '@tabler/icons'; +import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons'; import Dropdown from 'components/Dropdown'; -import { collectionClicked } from 'providers/ReduxStore/slices/collections'; -import { moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions'; +import { collapseCollection } from 'providers/ReduxStore/slices/collections'; +import { mountCollection, moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions'; import { useDispatch } from 'react-redux'; import { addTab } from 'providers/ReduxStore/slices/tabs'; import NewRequest from 'components/Sidebar/NewRequest'; @@ -15,12 +15,12 @@ import CollectionItem from './CollectionItem'; import RemoveCollection from './RemoveCollection'; import ExportCollection from './ExportCollection'; import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search'; -import { isItemAFolder, isItemARequest, transformCollectionToSaveToExportAsFile } from 'utils/collections'; -import exportCollection from 'utils/collections/export'; +import { isItemAFolder, isItemARequest } from 'utils/collections'; import RenameCollection from './RenameCollection'; import StyledWrapper from './StyledWrapper'; -import CloneCollection from './CloneCollection/index'; +import CloneCollection from './CloneCollection'; +import { areItemsLoading } from 'utils/collections'; const Collection = ({ collection, searchText }) => { const [showNewFolderModal, setShowNewFolderModal] = useState(false); @@ -29,8 +29,8 @@ const Collection = ({ collection, searchText }) => { const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false); const [showExportCollectionModal, setShowExportCollectionModal] = useState(false); const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false); - const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed); const dispatch = useDispatch(); + const isLoading = areItemsLoading(collection); const menuDropdownTippyRef = useRef(); const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref); @@ -52,32 +52,37 @@ const Collection = ({ collection, searchText }) => { ); }; - useEffect(() => { - if (searchText && searchText.length) { - setCollectionIsCollapsed(false); - } else { - setCollectionIsCollapsed(collection.collapsed); - } - }, [searchText, collection]); + const hasSearchText = searchText && searchText?.trim()?.length; + const collectionIsCollapsed = hasSearchText ? false : collection.collapsed; const iconClassName = classnames({ 'rotate-90': !collectionIsCollapsed }); const handleClick = (event) => { - dispatch(collectionClicked(collection.uid)); - }; + // Check if the click came from the chevron icon + const isChevronClick = event.target.closest('svg')?.classList.contains('chevron-icon'); - const handleCollapseCollection = () => { - dispatch(collectionClicked(collection.uid)); - dispatch( - addTab({ - uid: uuid(), + if (collection.mountStatus === 'unmounted') { + dispatch(mountCollection({ collectionUid: collection.uid, - type: 'collection-settings' - }) - ); - } + collectionPathname: collection.pathname, + brunoConfig: collection.brunoConfig + })); + } + dispatch(collapseCollection(collection.uid)); + + // Only open collection settings if not clicking the chevron + if(!isChevronClick) { + dispatch( + addTab({ + uid: uuid(), + collectionUid: collection.uid, + type: 'collection-settings' + }) + ); + } + }; const handleRightClick = (event) => { const _menuDropdown = menuDropdownTippyRef.current; @@ -152,19 +157,19 @@ const Collection = ({ collection, searchText }) => {
+ {isLoading ? : null}
} placement="bottom-start"> diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js index 4163ffc37..50e19c22e 100644 --- a/packages/bruno-app/src/components/Sidebar/index.js +++ b/packages/bruno-app/src/components/Sidebar/index.js @@ -184,7 +184,7 @@ const Sidebar = () => { Star */}
-
v1.36.0
+
v1.36.1
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 7973e57ad..de9ad78e9 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -23,6 +23,7 @@ import { import { uuid, waitForNextTick } from 'utils/common'; import { PATH_SEPARATOR, getDirectoryName, isWindowsPath } from 'utils/common/platform'; import { cancelNetworkRequest, sendNetworkRequest } from 'utils/network'; +import { callIpc } from 'utils/common/ipc'; import { collectionAddEnvFileEvent as _collectionAddEnvFileEvent, @@ -30,6 +31,7 @@ import { removeCollection as _removeCollection, selectEnvironment as _selectEnvironment, sortCollections as _sortCollections, + updateCollectionMountStatus, requestCancelled, resetRunResults, responseReceived, @@ -42,7 +44,6 @@ import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs'; import { resolveRequestFilename } from 'utils/common/platform'; import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index'; import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index'; -import { name } from 'file-loader'; import slash from 'utils/common/slash'; import { getGlobalEnvironmentVariables } from 'utils/collections/index'; import { findCollectionByPathname, findEnvironmentInCollectionByName } from 'utils/collections/index'; @@ -161,7 +162,6 @@ export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState) if (!folder) { return reject(new Error('Folder not found')); } - console.log(collection); const { ipcRenderer } = window; @@ -170,7 +170,6 @@ export const saveFolderRoot = (collectionUid, folderUid) => (dispatch, getState) pathname: folder.pathname, root: folder.root }; - console.log(folderData); ipcRenderer .invoke('renderer:save-folder-root', folderData) @@ -1192,4 +1191,31 @@ export const hydrateCollectionWithUiStateSnapshot = (payload) => (dispatch, getS reject(error); } }); - }; \ No newline at end of file + }; + +export const loadRequestViaWorker = ({ collectionUid, pathname }) => (dispatch, getState) => { + return new Promise(async (resolve, reject) => { + const { ipcRenderer } = window; + ipcRenderer.invoke('renderer:load-request-via-worker', { collectionUid, pathname }).then(resolve).catch(reject); + }); +}; + +export const loadRequest = ({ collectionUid, pathname }) => (dispatch, getState) => { + return new Promise(async (resolve, reject) => { + const { ipcRenderer } = window; + ipcRenderer.invoke('renderer:load-request', { collectionUid, pathname }).then(resolve).catch(reject); + }); +}; + +export const mountCollection = ({ collectionUid, collectionPathname, brunoConfig }) => (dispatch, getState) => { + dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'mounting' })); + return new Promise(async (resolve, reject) => { + callIpc('renderer:mount-collection', { collectionUid, collectionPathname, brunoConfig }) + .then(() => dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'mounted' }))) + .then(resolve) + .catch(() => { + dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'unmounted' })); + 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 11f12026f..6a795171f 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -4,7 +4,7 @@ import { createSlice } from '@reduxjs/toolkit'; import { addDepth, areItemsTheSameExceptSeqUpdate, - collapseCollection, + collapseAllItemsInCollection, deleteItemInCollection, deleteItemInCollectionByPathname, findCollectionByPathname, @@ -32,9 +32,13 @@ export const collectionsSlice = createSlice({ const collectionUids = map(state.collections, (c) => c.uid); const collection = action.payload; - collection.settingsSelectedTab = 'headers'; + collection.settingsSelectedTab = 'overview'; collection.folderLevelSettingsSelectedTab = {}; + // Collection mount status is used to track the mount status of the collection + // values can be 'unmounted', 'mounting', 'mounted' + collection.mountStatus = 'unmounted'; + // TODO: move this to use the nextAction approach // last action is used to track the last action performed on the collection // this is optional @@ -44,12 +48,18 @@ export const collectionsSlice = createSlice({ collection.importedAt = new Date().getTime(); collection.lastAction = null; - collapseCollection(collection); + collapseAllItemsInCollection(collection); addDepth(collection.items); if (!collectionUids.includes(collection.uid)) { state.collections.push(collection); } }, + updateCollectionMountStatus: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + if (collection) { + collection.mountStatus = action.payload.mountStatus; + } + }, setCollectionSecurityConfig: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); if (collection) { @@ -358,7 +368,7 @@ export const collectionsSlice = createSlice({ collection.items.push(item); } }, - collectionClicked: (state, action) => { + collapseCollection: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload); if (collection) { @@ -1582,7 +1592,7 @@ export const collectionsSlice = createSlice({ name: directoryName, collapsed: true, type: 'folder', - items: [] + items: [], }; currentSubItems.push(childItem); } @@ -1604,6 +1614,10 @@ export const collectionsSlice = createSlice({ currentItem.filename = file.meta.name; currentItem.pathname = file.meta.pathname; currentItem.draft = null; + currentItem.partial = file.partial; + currentItem.loading = file.loading; + currentItem.size = file.size; + currentItem.error = file.error; } else { currentSubItems.push({ uid: file.data.uid, @@ -1613,7 +1627,11 @@ export const collectionsSlice = createSlice({ request: file.data.request, filename: file.meta.name, pathname: file.meta.pathname, - draft: null + draft: null, + partial: file.partial, + loading: file.loading, + size: file.size, + error: file.error }); } } @@ -1890,6 +1908,7 @@ export const collectionsSlice = createSlice({ export const { createCollection, + updateCollectionMountStatus, setCollectionSecurityConfig, brunoConfigUpdateEvent, renameCollection, @@ -1913,7 +1932,7 @@ export const { saveRequest, deleteRequestDraft, newEphemeralHttpRequest, - collectionClicked, + collapseCollection, collectionFolderClicked, requestUrlChanged, updateAuth, diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js index 935be6075..2dfa3d94a 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/tabs.js @@ -25,7 +25,7 @@ export const tabsSlice = createSlice({ } if ( - ['variables', 'collection-settings', 'collection-runner', 'security-settings'].includes(action.payload.type) + ['variables', 'collection-settings', 'collection-overview', 'collection-runner', 'security-settings'].includes(action.payload.type) ) { const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type); if (tab) { diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js index 9e8e923aa..a47abb8d2 100644 --- a/packages/bruno-app/src/themes/dark.js +++ b/packages/bruno-app/src/themes/dark.js @@ -114,7 +114,25 @@ const darkTheme = { responseStatus: '#ccc', responseOk: '#8cd656', responseError: '#f06f57', - responseOverlayBg: 'rgba(30, 30, 30, 0.6)' + responseOverlayBg: 'rgba(30, 30, 30, 0.6)', + + card: { + bg: '#252526', + border: 'transparent', + borderDark: '#8cd656', + hr: '#424242' + }, + + cardTable: { + border: '#333', + bg: '#252526', + table: { + thead: { + bg: '#3D3D3D', + color: '#ccc' + } + } + } }, collection: { diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js index a25583136..9d3439895 100644 --- a/packages/bruno-app/src/themes/light.js +++ b/packages/bruno-app/src/themes/light.js @@ -114,7 +114,22 @@ const lightTheme = { responseStatus: 'rgb(117 117 117)', responseOk: '#047857', responseError: 'rgb(185, 28, 28)', - responseOverlayBg: 'rgba(255, 255, 255, 0.6)' + responseOverlayBg: 'rgba(255, 255, 255, 0.6)', + card: { + bg: '#fff', + border: '#f4f4f4', + hr: '#f4f4f4' + }, + cardTable: { + border: '#efefef', + bg: '#fff', + table: { + thead: { + bg: 'rgb(249, 250, 251)', + color: 'rgb(75 85 99)' + } + } + } }, collection: { diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index bc6c731f4..956616710 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -34,7 +34,7 @@ export const addDepth = (items = []) => { depth(items, 1); }; -export const collapseCollection = (collection) => { +export const collapseAllItemsInCollection = (collection) => { collection.collapsed = true; const collapseItem = (items) => { @@ -47,7 +47,7 @@ export const collapseCollection = (collection) => { }); }; - collapseItem(collection.items, 1); + collapseItem(collection.items); }; export const sortItems = (collection) => { @@ -136,6 +136,16 @@ export const findEnvironmentInCollectionByName = (collection, name) => { return find(collection.environments, (e) => e.name === name); }; +export const areItemsLoading = (folder) => { + let flattenedItems = flattenItems(folder.items); + return flattenedItems?.reduce((isLoading, i) => { + if (i?.loading) { + isLoading = true; + } + return isLoading; + }, false); +} + export const moveCollectionItem = (collection, draggedItem, targetItem) => { let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid); @@ -991,4 +1001,4 @@ const mergeVars = (collection, requestTreePath = []) => { folderVariables, requestVariables }; -}; +}; \ No newline at end of file diff --git a/packages/bruno-app/src/utils/common/ipc.js b/packages/bruno-app/src/utils/common/ipc.js new file mode 100644 index 000000000..3559737f2 --- /dev/null +++ b/packages/bruno-app/src/utils/common/ipc.js @@ -0,0 +1,14 @@ +/** + * Wrapper for ipcRenderer.invoke that handles error cases + * @param {string} channel - The IPC channel name + * @param {...any} args - Arguments to pass to the channel + * @returns {Promise} - Resolves with the result or rejects with error + */ +export const callIpc = (channel, ...args) => { + const { ipcRenderer } = window; + if (!ipcRenderer) { + return Promise.reject(new Error('IPC Renderer not available')); + } + + return ipcRenderer.invoke(channel, ...args); +}; \ No newline at end of file diff --git a/packages/bruno-electron/src/app/collections.js b/packages/bruno-electron/src/app/collections.js index 5c9889e13..7bd74c43b 100644 --- a/packages/bruno-electron/src/app/collections.js +++ b/packages/bruno-electron/src/app/collections.js @@ -2,7 +2,7 @@ const fs = require('fs'); const path = require('path'); const { dialog, ipcMain } = require('electron'); const Yup = require('yup'); -const { isDirectory, normalizeAndResolvePath } = require('../utils/filesystem'); +const { isDirectory, normalizeAndResolvePath, getCollectionStats } = require('../utils/filesystem'); const { generateUidBasedOnHash } = require('../utils/common'); // todo: bruno.json config schema validation errors must be propagated to the UI @@ -59,7 +59,7 @@ const openCollectionDialog = async (win, watcher) => { const openCollection = async (win, watcher, collectionPath, options = {}) => { if (!watcher.hasWatcher(collectionPath)) { try { - const brunoConfig = await getCollectionConfigFile(collectionPath); + let brunoConfig = await getCollectionConfigFile(collectionPath); const uid = generateUidBasedOnHash(collectionPath); if (!brunoConfig.ignore || brunoConfig.ignore.length === 0) { @@ -70,6 +70,10 @@ const openCollection = async (win, watcher, collectionPath, options = {}) => { brunoConfig.ignore = ['node_modules', '.git']; } + const { size, filesCount } = await getCollectionStats(collectionPath); + brunoConfig.size = size; + brunoConfig.filesCount = filesCount; + win.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig); ipcMain.emit('main:collection-opened', win, collectionPath, uid, brunoConfig); } catch (err) { diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js index 43d01153d..b2b60fd55 100644 --- a/packages/bruno-electron/src/app/watcher.js +++ b/packages/bruno-electron/src/app/watcher.js @@ -2,8 +2,8 @@ const _ = require('lodash'); const fs = require('fs'); const path = require('path'); const chokidar = require('chokidar'); -const { hasBruExtension, isWSLPath, normalizeAndResolvePath, normalizeWslPath } = require('../utils/filesystem'); -const { bruToEnvJson, bruToJson, collectionBruToJson } = require('../bru'); +const { hasBruExtension, isWSLPath, normalizeAndResolvePath, normalizeWslPath, sizeInMB } = require('../utils/filesystem'); +const { bruToEnvJson, bruToJson, bruToJsonViaWorker ,collectionBruToJson } = require('../bru'); const { dotenvToJson } = require('@usebruno/lang'); const { uuid } = require('../utils/common'); @@ -13,6 +13,9 @@ const { setDotEnvVars } = require('../store/process-env'); const { setBrunoConfig } = require('../store/bruno-config'); const EnvironmentSecretsStore = require('../store/env-secrets'); const UiStateSnapshot = require('../store/ui-state-snapshot'); +const { parseBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection'); + +const MAX_FILE_SIZE = 2.5 * 1024 * 1024; const environmentSecretsStore = new EnvironmentSecretsStore(); @@ -44,28 +47,6 @@ const isCollectionRootBruFile = (pathname, collectionPath) => { return dirname === collectionPath && basename === 'collection.bru'; }; -const hydrateRequestWithUuid = (request, pathname) => { - request.uid = getRequestUid(pathname); - - const params = _.get(request, 'request.params', []); - const headers = _.get(request, 'request.headers', []); - const requestVars = _.get(request, 'request.vars.req', []); - const responseVars = _.get(request, 'request.vars.res', []); - const assertions = _.get(request, 'request.assertions', []); - const bodyFormUrlEncoded = _.get(request, 'request.body.formUrlEncoded', []); - const bodyMultipartForm = _.get(request, 'request.body.multipartForm', []); - - params.forEach((param) => (param.uid = uuid())); - headers.forEach((header) => (header.uid = uuid())); - requestVars.forEach((variable) => (variable.uid = uuid())); - responseVars.forEach((variable) => (variable.uid = uuid())); - assertions.forEach((assertion) => (assertion.uid = uuid())); - bodyFormUrlEncoded.forEach((param) => (param.uid = uuid())); - bodyMultipartForm.forEach((param) => (param.uid = uuid())); - - return request; -}; - const hydrateBruCollectionFileWithUuid = (collectionRoot) => { const params = _.get(collectionRoot, 'request.params', []); const headers = _.get(collectionRoot, 'request.headers', []); @@ -99,7 +80,7 @@ const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath) let bruContent = fs.readFileSync(pathname, 'utf8'); - file.data = bruToEnvJson(bruContent); + file.data = await bruToEnvJson(bruContent); file.data.name = basename.substring(0, basename.length - 4); file.data.uid = getRequestUid(pathname); @@ -134,7 +115,7 @@ const changeEnvironmentFile = async (win, pathname, collectionUid, collectionPat }; const bruContent = fs.readFileSync(pathname, 'utf8'); - file.data = bruToEnvJson(bruContent); + file.data = await bruToEnvJson(bruContent); file.data.name = basename.substring(0, basename.length - 4); file.data.uid = getRequestUid(pathname); _.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid())); @@ -179,7 +160,7 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => { } }; -const add = async (win, pathname, collectionUid, collectionPath) => { +const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread) => { console.log(`watcher add: ${pathname}`); if (isBrunoConfigFile(pathname, collectionPath)) { @@ -228,7 +209,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => { try { let bruContent = fs.readFileSync(pathname, 'utf8'); - file.data = collectionBruToJson(bruContent); + file.data = await collectionBruToJson(bruContent); hydrateBruCollectionFileWithUuid(file.data); win.webContents.send('main:collection-tree-updated', 'addFile', file); @@ -241,7 +222,6 @@ const add = async (win, pathname, collectionUid, collectionPath) => { // Is this a folder.bru file? if (path.basename(pathname) === 'folder.bru') { - console.log('folder.bru file detected'); const file = { meta: { collectionUid, @@ -254,7 +234,7 @@ const add = async (win, pathname, collectionUid, collectionPath) => { try { let bruContent = fs.readFileSync(pathname, 'utf8'); - file.data = collectionBruToJson(bruContent); + file.data = await collectionBruToJson(bruContent); hydrateBruCollectionFileWithUuid(file.data); win.webContents.send('main:collection-tree-updated', 'addFile', file); @@ -274,15 +254,67 @@ const add = async (win, pathname, collectionUid, collectionPath) => { } }; + const fileStats = fs.statSync(pathname); + let bruContent = fs.readFileSync(pathname, 'utf8'); + // If worker thread is not used, we can directly parse the file + if (!useWorkerThread) { + try { + file.data = await bruToJson(bruContent); + file.partial = false; + file.loading = false; + file.size = sizeInMB(fileStats?.size); + hydrateRequestWithUuid(file.data, pathname); + win.webContents.send('main:collection-tree-updated', 'addFile', file); + } catch (error) { + console.error(error); + } + return; + } + try { - let bruContent = fs.readFileSync(pathname, 'utf8'); - - file.data = bruToJson(bruContent); + // we need to send a partial file info to the UI + // so that the UI can display the file in the collection tree + file.data = { + name: path.basename(pathname), + type: 'http-request' + }; + const metaJson = await bruToJson(parseBruFileMeta(bruContent), true); + file.data = metaJson; + file.partial = true; + file.loading = false; + file.size = sizeInMB(fileStats?.size); + hydrateRequestWithUuid(file.data, pathname); + win.webContents.send('main:collection-tree-updated', 'addFile', file); + + if (fileStats.size < MAX_FILE_SIZE) { + // This is to update the loading indicator in the UI + file.data = metaJson; + file.partial = false; + file.loading = true; + hydrateRequestWithUuid(file.data, pathname); + win.webContents.send('main:collection-tree-updated', 'addFile', file); + + // This is to update the file info in the UI + file.data = await bruToJsonViaWorker(bruContent); + file.partial = false; + file.loading = false; + hydrateRequestWithUuid(file.data, pathname); + win.webContents.send('main:collection-tree-updated', 'addFile', file); + } + } catch(error) { + file.data = { + name: path.basename(pathname), + type: 'http-request' + }; + file.error = { + message: error?.message + }; + file.partial = true; + file.loading = false; + file.size = sizeInMB(fileStats?.size); hydrateRequestWithUuid(file.data, pathname); win.webContents.send('main:collection-tree-updated', 'addFile', file); - } catch (err) { - console.error(err); } } }; @@ -357,7 +389,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => { try { let bruContent = fs.readFileSync(pathname, 'utf8'); - file.data = collectionBruToJson(bruContent); + file.data = await collectionBruToJson(bruContent); hydrateBruCollectionFileWithUuid(file.data); win.webContents.send('main:collection-tree-updated', 'change', file); return; @@ -378,7 +410,7 @@ const change = async (win, pathname, collectionUid, collectionPath) => { }; const bru = fs.readFileSync(pathname, 'utf8'); - file.data = bruToJson(bru); + file.data = await bruToJson(bru); hydrateRequestWithUuid(file.data, pathname); win.webContents.send('main:collection-tree-updated', 'change', file); @@ -424,10 +456,10 @@ const unlinkDir = (win, pathname, collectionUid, collectionPath) => { win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory); }; -const onWatcherSetupComplete = (win, collectionPath) => { +const onWatcherSetupComplete = (win, watchPath) => { const UiStateSnapshotStore = new UiStateSnapshot(); const collectionsSnapshotState = UiStateSnapshotStore.getCollections(); - const collectionSnapshotState = collectionsSnapshotState?.find(c => c?.pathname == collectionPath); + const collectionSnapshotState = collectionsSnapshotState?.find(c => c?.pathname == watchPath); win.webContents.send('main:hydrate-app-with-ui-state-snapshot', collectionSnapshotState); }; @@ -436,7 +468,7 @@ class Watcher { this.watchers = {}; } - addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false) { + addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false, useWorkerThread) { if (this.watchers[watchPath]) { this.watchers[watchPath].close(); } @@ -467,7 +499,7 @@ class Watcher { let startedNewWatcher = false; watcher .on('ready', () => onWatcherSetupComplete(win, watchPath)) - .on('add', (pathname) => add(win, pathname, collectionUid, watchPath)) + .on('add', (pathname) => add(win, pathname, collectionUid, watchPath, useWorkerThread)) .on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath)) .on('change', (pathname) => change(win, pathname, collectionUid, watchPath)) .on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath)) @@ -488,7 +520,7 @@ class Watcher { 'Update you system config to allow more concurrently watched files with:', '"echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p"' ); - this.addWatcher(win, watchPath, collectionUid, brunoConfig, true); + this.addWatcher(win, watchPath, collectionUid, brunoConfig, true, useWorkerThread); } else { console.error(`An error occurred in the watcher for: ${watchPath}`, error); } diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js index 7fe43218a..a641a95a7 100644 --- a/packages/bruno-electron/src/bru/index.js +++ b/packages/bruno-electron/src/bru/index.js @@ -7,10 +7,13 @@ const { collectionBruToJson: _collectionBruToJson, jsonToCollectionBru: _jsonToCollectionBru } = require('@usebruno/lang'); +const BruParserWorker = require('./workers'); -const collectionBruToJson = (bru) => { +const bruParserWorker = new BruParserWorker(); + +const collectionBruToJson = async (data, parsed = false) => { try { - const json = _collectionBruToJson(bru); + const json = parsed ? data : _collectionBruToJson(data); const transformedJson = { request: { @@ -38,7 +41,7 @@ const collectionBruToJson = (bru) => { } }; -const jsonToCollectionBru = (json, isFolder) => { +const jsonToCollectionBru = async (json, isFolder) => { try { const collectionBruJson = { headers: _.get(json, 'request.headers', []), @@ -73,7 +76,7 @@ const jsonToCollectionBru = (json, isFolder) => { } }; -const bruToEnvJson = (bru) => { +const bruToEnvJson = async (bru) => { try { const json = bruToEnvJsonV2(bru); @@ -90,7 +93,7 @@ const bruToEnvJson = (bru) => { } }; -const envJsonToBru = (json) => { +const envJsonToBru = async (json) => { try { const bru = envJsonToBruV2(json); return bru; @@ -105,12 +108,12 @@ const envJsonToBru = (json) => { * We map the json response from the bru lang and transform it into the DSL * format that the app uses * - * @param {string} bru The BRU file content. + * @param {string} data The BRU file content. * @returns {object} The JSON representation of the BRU file. */ -const bruToJson = (bru) => { +const bruToJson = (data, parsed = false) => { try { - const json = bruToJsonV2(bru); + const json = parsed ? data : bruToJsonV2(data); let requestType = _.get(json, 'meta.type'); if (requestType === 'http') { @@ -149,6 +152,16 @@ const bruToJson = (bru) => { return Promise.reject(e); } }; + +const bruToJsonViaWorker = async (data) => { + try { + const json = await bruParserWorker?.bruToJson(data); + return bruToJson(json, true); + } catch (e) { + return Promise.reject(e); + } +}; + /** * The transformer function for converting a JSON to BRU file. * @@ -158,7 +171,7 @@ const bruToJson = (bru) => { * @param {object} json The JSON representation of the BRU file. * @returns {string} The BRU file content. */ -const jsonToBru = (json) => { +const jsonToBru = async (json) => { let type = _.get(json, 'type'); if (type === 'http-request') { type = 'http'; @@ -195,14 +208,59 @@ const jsonToBru = (json) => { docs: _.get(json, 'request.docs', '') }; - return jsonToBruV2(bruJson); + const bru = jsonToBruV2(bruJson); + return bru; }; +const jsonToBruViaWorker = async (json) => { + let type = _.get(json, 'type'); + if (type === 'http-request') { + type = 'http'; + } else if (type === 'graphql-request') { + type = 'graphql'; + } else { + type = 'http'; + } + + const sequence = _.get(json, 'seq'); + const bruJson = { + meta: { + name: _.get(json, 'name'), + type: type, + seq: !isNaN(sequence) ? Number(sequence) : 1 + }, + http: { + method: _.lowerCase(_.get(json, 'request.method')), + url: _.get(json, 'request.url'), + auth: _.get(json, 'request.auth.mode', 'none'), + body: _.get(json, 'request.body.mode', 'none') + }, + params: _.get(json, 'request.params', []), + headers: _.get(json, 'request.headers', []), + auth: _.get(json, 'request.auth', {}), + body: _.get(json, 'request.body', {}), + script: _.get(json, 'request.script', {}), + vars: { + req: _.get(json, 'request.vars.req', []), + res: _.get(json, 'request.vars.res', []) + }, + assertions: _.get(json, 'request.assertions', []), + tests: _.get(json, 'request.tests', ''), + docs: _.get(json, 'request.docs', '') + }; + + const bru = await bruParserWorker?.jsonToBru(bruJson) + return bru; +}; + + module.exports = { bruToJson, + bruToJsonViaWorker, jsonToBru, bruToEnvJson, envJsonToBru, collectionBruToJson, - jsonToCollectionBru + jsonToCollectionBru, + jsonToBruViaWorker }; diff --git a/packages/bruno-electron/src/bru/workers/index.js b/packages/bruno-electron/src/bru/workers/index.js new file mode 100644 index 000000000..62c19f99d --- /dev/null +++ b/packages/bruno-electron/src/bru/workers/index.js @@ -0,0 +1,57 @@ +const WorkerQueue = require("../../workers"); +const path = require("path"); + +const getSize = (data) => { + return typeof data === 'string' ? Buffer.byteLength(data, 'utf8') : Buffer.byteLength(JSON.stringify(data), 'utf8'); +} + +/** + * Lanes are used to determine which worker queue to use based on the size of the data. + * + * The first lane is for smaller files (<0.1MB), the second lane is for larger files (>=0.1MB). + * This helps with parsing performance. + */ +const LANES = [{ + maxSize: 0.1 +},{ + maxSize: 100 +}]; + +class BruParserWorker { + constructor() { + this.workerQueues = LANES?.map(lane => ({ + maxSize: lane?.maxSize, + workerQueue: new WorkerQueue() + })); + } + + getWorkerQueue(size) { + // Find the first queue that can handle the given size + // or fallback to the last queue for largest files + const queueForSize = this.workerQueues.find((queue) => + queue.maxSize >= size + ); + + return queueForSize?.workerQueue ?? this.workerQueues.at(-1).workerQueue; + } + + async enqueueTask({data, scriptFile }) { + const size = getSize(data); + const workerQueue = this.getWorkerQueue(size); + return workerQueue.enqueue({ + data, + priority: size, + scriptPath: path.join(__dirname, `./scripts/${scriptFile}.js`) + }); + } + + async bruToJson(data) { + return this.enqueueTask({ data, scriptFile: `bru-to-json` }); + } + + async jsonToBru(data) { + return this.enqueueTask({ data, scriptFile: `json-to-bru` }); + } +} + +module.exports = BruParserWorker; \ No newline at end of file diff --git a/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js b/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js new file mode 100644 index 000000000..c1bbb44e7 --- /dev/null +++ b/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js @@ -0,0 +1,14 @@ +const { workerData, parentPort } = require('worker_threads'); +const { + bruToJsonV2, +} = require('@usebruno/lang'); + +try { + const bru = workerData; + const json = bruToJsonV2(bru); + parentPort.postMessage(json); +} +catch(error) { + console.error(error); + parentPort.postMessage({ error: error?.message }); +} \ No newline at end of file diff --git a/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js b/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js new file mode 100644 index 000000000..e08be60b9 --- /dev/null +++ b/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js @@ -0,0 +1,13 @@ +const { workerData, parentPort } = require('worker_threads'); +const { + jsonToBruV2, +} = require('@usebruno/lang'); +try { + const json = workerData; + const bru = jsonToBruV2(json); + parentPort.postMessage(bru); +} +catch(error) { + console.error(error); + parentPort.postMessage({ error: error?.message }); +} \ No newline at end of file diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 898324892..b9061a227 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -4,7 +4,7 @@ const fsExtra = require('fs-extra'); const os = require('os'); const path = require('path'); const { ipcMain, shell, dialog, app } = require('electron'); -const { envJsonToBru, bruToJson, jsonToBru, jsonToCollectionBru } = require('../bru'); +const { envJsonToBru, bruToJson, jsonToBruViaWorker, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru'); const { isValidPathname, @@ -24,6 +24,8 @@ const { isWindowsOS, isValidFilename, hasSubDirectories, + getCollectionStats, + sizeInMB } = require('../utils/filesystem'); const { openCollectionDialog } = require('../app/collections'); const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common'); @@ -32,11 +34,17 @@ const { deleteCookiesForDomain, getDomainsWithCookies } = require('../utils/cook const EnvironmentSecretsStore = require('../store/env-secrets'); const CollectionSecurityStore = require('../store/collection-security'); const UiStateSnapshotStore = require('../store/ui-state-snapshot'); +const { parseBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection'); const environmentSecretsStore = new EnvironmentSecretsStore(); const collectionSecurityStore = new CollectionSecurityStore(); const uiStateSnapshotStore = new UiStateSnapshotStore(); +// size and file count limits to determine whether the bru files in the collection should be loaded asynchronously or not. +const MAX_COLLECTION_SIZE_IN_MB = 5; +const MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB = 2; +const MAX_COLLECTION_FILES_COUNT = 100; + const envHasSecrets = (environment = {}) => { const secrets = _.filter(environment.variables, (v) => v.secret); @@ -97,6 +105,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection const content = await stringifyJson(brunoConfig); await writeFile(path.join(dirPath, 'bruno.json'), content); + const { size, filesCount } = await getCollectionStats(dirPath); + brunoConfig.size = size; + brunoConfig.filesCount = filesCount; + mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig); ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid, brunoConfig); } catch (error) { @@ -126,9 +138,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection const brunoJsonFilePath = path.join(previousPath, 'bruno.json'); const content = fs.readFileSync(brunoJsonFilePath, 'utf8'); - //Change new name of collection - let json = JSON.parse(content); - json.name = collectionName; + // Change new name of collection + let brunoConfig = JSON.parse(content); + brunoConfig.name = collectionName; const cont = await stringifyJson(json); // write the bruno.json to new dir @@ -147,7 +159,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection fs.copyFileSync(sourceFilePath, newFilePath); } - mainWindow.webContents.send('main:collection-opened', dirPath, uid, json); + const { size, filesCount } = await getCollectionStats(dirPath); + brunoConfig.size = size; + brunoConfig.filesCount = filesCount; + + mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig); ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid); } ); @@ -184,7 +200,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection name: folderName }; - const content = jsonToCollectionBru( + const content = await jsonToCollectionBru( folderRoot, true // isFolder ); @@ -197,7 +213,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection try { const collectionBruFilePath = path.join(collectionPathname, 'collection.bru'); - const content = jsonToCollectionBru(collectionRoot); + const content = await jsonToCollectionBru(collectionRoot); await writeFile(collectionBruFilePath, content); } catch (error) { return Promise.reject(error); @@ -213,7 +229,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection if (!isValidFilename(request.name)) { throw new Error(`path: ${request.name}.bru is not a valid filename`); } - const content = jsonToBru(request); + const content = await jsonToBruViaWorker(request); await writeFile(pathname, content); } catch (error) { return Promise.reject(error); @@ -227,7 +243,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection throw new Error(`path: ${pathname} does not exist`); } - const content = jsonToBru(request); + const content = await jsonToBruViaWorker(request); await writeFile(pathname, content); } catch (error) { return Promise.reject(error); @@ -245,7 +261,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection throw new Error(`path: ${pathname} does not exist`); } - const content = jsonToBru(request); + const content = await jsonToBruViaWorker(request); await writeFile(pathname, content); } } catch (error) { @@ -275,7 +291,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection environmentSecretsStore.storeEnvSecrets(collectionPathname, environment); } - const content = envJsonToBru(environment); + const content = await envJsonToBru(environment); await writeFile(envFilePath, content); } catch (error) { @@ -300,7 +316,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection environmentSecretsStore.storeEnvSecrets(collectionPathname, environment); } - const content = envJsonToBru(environment); + const content = await envJsonToBru(environment); await writeFile(envFilePath, content); } catch (error) { return Promise.reject(error); @@ -412,11 +428,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // update name in file and save new copy, then delete old copy const data = await fs.promises.readFile(oldPath, 'utf8'); // Use async read - const jsonData = bruToJson(data); + const jsonData = await bruToJsonViaWorker(data); jsonData.name = newName; moveRequestUid(oldPath, newPath); - const content = jsonToBru(jsonData); + const content = await jsonToBruViaWorker(jsonData); await fs.promises.unlink(oldPath); await writeFile(newPath, content); @@ -516,9 +532,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // Recursive function to parse the collection items and create files/folders const parseCollectionItems = (items = [], currentPath) => { - items.forEach((item) => { + items.forEach(async (item) => { if (['http-request', 'graphql-request'].includes(item.type)) { - const content = jsonToBru(item); + const content = await jsonToBruViaWorker(item); const filePath = path.join(currentPath, `${item.name}.bru`); fs.writeFileSync(filePath, content); } @@ -529,7 +545,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection if (item?.root?.meta?.name) { const folderBruFilePath = path.join(folderPath, 'folder.bru'); - const folderContent = jsonToCollectionBru( + const folderContent = await jsonToCollectionBru( item.root, true // isFolder ); @@ -554,8 +570,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection fs.mkdirSync(envDirPath); } - environments.forEach((env) => { - const content = envJsonToBru(env); + environments.forEach(async (env) => { + const content = await envJsonToBru(env); const filePath = path.join(envDirPath, `${env.name}.bru`); fs.writeFileSync(filePath, content); }); @@ -579,15 +595,19 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection await createDirectory(collectionPath); const uid = generateUidBasedOnHash(collectionPath); - const brunoConfig = getBrunoJsonConfig(collection); + let brunoConfig = getBrunoJsonConfig(collection); const stringifiedBrunoConfig = await stringifyJson(brunoConfig); // Write the Bruno configuration to a file await writeFile(path.join(collectionPath, 'bruno.json'), stringifiedBrunoConfig); - const collectionContent = jsonToCollectionBru(collection.root); + const collectionContent = await jsonToCollectionBru(collection.root); await writeFile(path.join(collectionPath, 'collection.bru'), collectionContent); + const { size, filesCount } = await getCollectionStats(collectionPath); + brunoConfig.size = size; + brunoConfig.filesCount = filesCount; + mainWindow.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig); ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid, brunoConfig); @@ -609,9 +629,9 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // Recursive function to parse the folder and create files/folders const parseCollectionItems = (items = [], currentPath) => { - items.forEach((item) => { + items.forEach(async (item) => { if (['http-request', 'graphql-request'].includes(item.type)) { - const content = jsonToBru(item); + const content = await jsonToBruViaWorker(item); const filePath = path.join(currentPath, `${item.name}.bru`); fs.writeFileSync(filePath, content); } @@ -621,7 +641,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // If folder has a root element, then I should write its folder.bru file if (item.root) { - const folderContent = jsonToCollectionBru(item.root, true); + const folderContent = await jsonToCollectionBru(item.root, true); if (folderContent) { const bruFolderPath = path.join(folderPath, `folder.bru`); fs.writeFileSync(bruFolderPath, folderContent); @@ -639,7 +659,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // If initial folder has a root element, then I should write its folder.bru file if (itemFolder.root) { - const folderContent = jsonToCollectionBru(itemFolder.root, true); + const folderContent = await jsonToCollectionBru(itemFolder.root, true); if (folderContent) { const bruFolderPath = path.join(collectionPath, `folder.bru`); fs.writeFileSync(bruFolderPath, folderContent); @@ -655,13 +675,13 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection ipcMain.handle('renderer:resequence-items', async (event, itemsToResequence) => { try { - for (let item of itemsToResequence) { + for await (let item of itemsToResequence) { const bru = fs.readFileSync(item.pathname, 'utf8'); - const jsonData = bruToJson(bru); + const jsonData = await bruToJsonViaWorker(bru); if (jsonData.seq !== item.seq) { jsonData.seq = item.seq; - const content = jsonToBru(jsonData); + const content = await jsonToBruViaWorker(jsonData); await writeFile(item.pathname, content); } } @@ -776,6 +796,119 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection throw new Error(error.message); } }); + + ipcMain.handle('renderer:load-request-via-worker', async (event, { collectionUid, pathname }) => { + let fileStats; + try { + fileStats = fs.statSync(pathname); + if (hasBruExtension(pathname)) { + const file = { + meta: { + collectionUid, + pathname, + name: path.basename(pathname) + } + }; + let bruContent = fs.readFileSync(pathname, 'utf8'); + const metaJson = await bruToJson(parseBruFileMeta(bruContent), true); + file.data = metaJson; + file.loading = true; + file.partial = true; + file.size = sizeInMB(fileStats?.size); + hydrateRequestWithUuid(file.data, pathname); + mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); + file.data = await bruToJsonViaWorker(bruContent); + file.partial = false; + file.loading = true; + file.size = sizeInMB(fileStats?.size); + hydrateRequestWithUuid(file.data, pathname); + mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); + } + } catch (error) { + if (hasBruExtension(pathname)) { + const file = { + meta: { + collectionUid, + pathname, + name: path.basename(pathname) + } + }; + let bruContent = fs.readFileSync(pathname, 'utf8'); + const metaJson = await bruToJson(parseBruFileMeta(bruContent), true); + file.data = metaJson; + file.partial = true; + file.loading = false; + file.size = sizeInMB(fileStats?.size); + hydrateRequestWithUuid(file.data, pathname); + mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); + } + return Promise.reject(error); + } + }); + + ipcMain.handle('renderer:load-request', async (event, { collectionUid, pathname }) => { + let fileStats; + try { + fileStats = fs.statSync(pathname); + if (hasBruExtension(pathname)) { + const file = { + meta: { + collectionUid, + pathname, + name: path.basename(pathname) + } + }; + let bruContent = fs.readFileSync(pathname, 'utf8'); + const metaJson = await bruToJson(parseBruFileMeta(bruContent), true); + file.data = metaJson; + file.loading = true; + file.partial = true; + file.size = sizeInMB(fileStats?.size); + hydrateRequestWithUuid(file.data, pathname); + mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); + file.data = bruToJson(bruContent); + file.partial = false; + file.loading = true; + file.size = sizeInMB(fileStats?.size); + hydrateRequestWithUuid(file.data, pathname); + mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); + } + } catch (error) { + if (hasBruExtension(pathname)) { + const file = { + meta: { + collectionUid, + pathname, + name: path.basename(pathname) + } + }; + let bruContent = fs.readFileSync(pathname, 'utf8'); + const metaJson = await bruToJson(parseBruFileMeta(bruContent), true); + file.data = metaJson; + file.partial = true; + file.loading = false; + file.size = sizeInMB(fileStats?.size); + hydrateRequestWithUuid(file.data, pathname); + mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file); + } + return Promise.reject(error); + } + }); + + ipcMain.handle('renderer:mount-collection', async (event, { collectionUid, collectionPathname, brunoConfig }) => { + const { + size, + filesCount, + maxFileSize + } = await getCollectionStats(collectionPathname); + + const shouldLoadCollectionAsync = + (size > MAX_COLLECTION_SIZE_IN_MB) || + (filesCount > MAX_COLLECTION_FILES_COUNT) || + (maxFileSize > MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB); + + watcher.addWatcher(mainWindow, collectionPathname, collectionUid, brunoConfig, false, shouldLoadCollectionAsync); + }); }; const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => { @@ -790,8 +923,7 @@ const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) = shell.openExternal(docsURL); }); - ipcMain.on('main:collection-opened', (win, pathname, uid, brunoConfig) => { - watcher.addWatcher(win, pathname, uid, brunoConfig); + ipcMain.on('main:collection-opened', async (win, pathname, uid, brunoConfig) => { lastOpenedCollections.add(pathname); app.addRecentDocument(pathname); }); diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index 15d5574e2..d0ec68ab1 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -1,3 +1,7 @@ +const fs = require('fs'); +const { getRequestUid } = require('../cache/requestUids'); +const { uuid } = require('./common'); + const { get, each, find, compact } = require('lodash'); const os = require('os'); @@ -203,6 +207,51 @@ const getTreePathFromCollectionToItem = (collection, _item) => { return path; }; +const parseBruFileMeta = (data) => { + try { + const metaRegex = /meta\s*{\s*([\s\S]*?)\s*}/; + const match = data?.match?.(metaRegex); + if (match) { + const metaContent = match[1].trim(); + const lines = metaContent.replace(/\r\n/g, '\n').split('\n'); + const metaJson = {}; + lines.forEach(line => { + const [key, value] = line.split(':').map(str => str.trim()); + if (key && value) { + metaJson[key] = isNaN(value) ? value : Number(value); + } + }); + return { meta: metaJson }; + } else { + console.log('No "meta" block found in the file.'); + } + } catch (err) { + console.error('Error reading file:', err); + } +} + +const hydrateRequestWithUuid = (request, pathname) => { + request.uid = getRequestUid(pathname); + + const params = get(request, 'request.params', []); + const headers = get(request, 'request.headers', []); + const requestVars = get(request, 'request.vars.req', []); + const responseVars = get(request, 'request.vars.res', []); + const assertions = get(request, 'request.assertions', []); + const bodyFormUrlEncoded = get(request, 'request.body.formUrlEncoded', []); + const bodyMultipartForm = get(request, 'request.body.multipartForm', []); + + params.forEach((param) => (param.uid = uuid())); + headers.forEach((header) => (header.uid = uuid())); + requestVars.forEach((variable) => (variable.uid = uuid())); + responseVars.forEach((variable) => (variable.uid = uuid())); + assertions.forEach((assertion) => (assertion.uid = uuid())); + bodyFormUrlEncoded.forEach((param) => (param.uid = uuid())); + bodyMultipartForm.forEach((param) => (param.uid = uuid())); + + return request; +}; + const slash = (path) => { const isExtendedLengthPath = /^\\\\\?\\/.test(path); if (isExtendedLengthPath) { @@ -221,13 +270,18 @@ const findItemInCollectionByPathname = (collection, pathname) => { return findItemByPathname(flattenedItems, pathname); }; - module.exports = { mergeHeaders, mergeVars, mergeScripts, getTreePathFromCollectionToItem, + flattenItems, + findItem, + findItemInCollection, slash, findItemByPathname, - findItemInCollectionByPathname -} \ No newline at end of file + findItemInCollectionByPathname, + findParentItemInCollection, + parseBruFileMeta, + hydrateRequestWithUuid +}; \ No newline at end of file diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index d2f74d10e..0ab6bbf0a 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -211,6 +211,50 @@ const safeToRename = (oldPath, newPath) => { } }; +const getCollectionStats = async (directoryPath) => { + let size = 0; + let filesCount = 0; + let maxFileSize = 0; + + async function calculateStats(directory) { + const entries = await fsPromises.readdir(directory, { withFileTypes: true }); + + const tasks = entries.map(async (entry) => { + const fullPath = path.join(directory, entry.name); + + if (entry.isDirectory()) { + if (['node_modules', '.git'].includes(entry.name)) { + return; + } + + await calculateStats(fullPath); + } + + if (path.extname(fullPath) === '.bru') { + const stats = await fsPromises.stat(fullPath); + size += stats?.size; + if (maxFileSize < stats?.size) { + maxFileSize = stats?.size; + } + filesCount += 1; + } + }); + + await Promise.all(tasks); + } + + await calculateStats(directoryPath); + + size = sizeInMB(size); + maxFileSize = sizeInMB(maxFileSize); + + return { size, filesCount, maxFileSize }; +} + +const sizeInMB = (size) => { + return size / (1024 * 1024); +} + module.exports = { isValidPathname, exists, @@ -235,5 +279,7 @@ module.exports = { isWindowsOS, safeToRename, isValidFilename, - hasSubDirectories + hasSubDirectories, + getCollectionStats, + sizeInMB }; diff --git a/packages/bruno-electron/src/workers/index.js b/packages/bruno-electron/src/workers/index.js new file mode 100644 index 000000000..04836e9fc --- /dev/null +++ b/packages/bruno-electron/src/workers/index.js @@ -0,0 +1,60 @@ +const { Worker } = require('worker_threads'); + +class WorkerQueue { + constructor() { + this.queue = []; + this.isProcessing = false; + } + + async enqueue(task) { + const { priority, scriptPath, data } = task; + + return new Promise((resolve, reject) => { + this.queue.push({ priority, scriptPath, data, resolve, reject }); + this.queue?.sort((taskX, taskY) => taskX?.priority - taskY?.priority); + this.processQueue(); + }); + } + + async processQueue() { + if (this.isProcessing || this.queue.length === 0){ + return; + } + + this.isProcessing = true; + const { scriptPath, data, resolve, reject } = this.queue.shift(); + + try { + const result = await this.runWorker({ scriptPath, data }); + resolve(result); + } catch (error) { + reject(error); + } finally { + this.isProcessing = false; + this.processQueue(); + } + } + + async runWorker({ scriptPath, data }) { + return new Promise((resolve, reject) => { + const worker = new Worker(scriptPath, { workerData: data }); + worker.on('message', (data) => { + if (data?.error) { + reject(new Error(data?.error)); + } + resolve(data); + worker.terminate(); + }); + worker.on('error', (error) => { + reject(error); + worker.terminate(); + }); + worker.on('exit', (code) => { + reject(new Error(`stopped with ${code} exit code`)); + worker.terminate(); + }); + }); + } +} + +module.exports = WorkerQueue; diff --git a/packages/bruno-electron/tests/utils/collection.spec.js b/packages/bruno-electron/tests/utils/collection.spec.js new file mode 100644 index 000000000..4efc9c002 --- /dev/null +++ b/packages/bruno-electron/tests/utils/collection.spec.js @@ -0,0 +1,121 @@ +const { parseBruFileMeta } = require("../../src/utils/collection"); + +describe('parseBruFileMeta', () => { + test('parses valid meta block correctly', () => { + const data = `meta { + name: 0.2_mb + type: http + seq: 1 + }`; + + const result = parseBruFileMeta(data); + + expect(result).toEqual({ + meta: { + name: '0.2_mb', + type: 'http', + seq: 1, + }, + }); + }); + + test('returns undefined for missing meta block', () => { + const data = `someOtherBlock { + key: value + }`; + + const result = parseBruFileMeta(data); + + expect(result).toBeUndefined(); + }); + + test('handles empty meta block gracefully', () => { + const data = `meta {}`; + + const result = parseBruFileMeta(data); + + expect(result).toEqual({ meta: {} }); + }); + + test('ignores invalid lines in meta block', () => { + const data = `meta { + name: 0.2_mb + invalidLine + seq: 1 + }`; + + const result = parseBruFileMeta(data); + + expect(result).toEqual({ + meta: { + name: '0.2_mb', + seq: 1, + }, + }); + }); + + test('handles unexpected input gracefully', () => { + const data = null; + + const result = parseBruFileMeta(data); + + expect(result).toBeUndefined(); + }); + + test('handles missing colon gracefully', () => { + const data = `meta { + name 0.2_mb + seq: 1 + }`; + + const result = parseBruFileMeta(data); + + expect(result).toEqual({ + meta: { + seq: 1, + }, + }); + }); + + test('parses numeric values correctly', () => { + const data = `meta { + numValue: 1234 + floatValue: 12.34 + strValue: some_text + }`; + + const result = parseBruFileMeta(data); + + expect(result).toEqual({ + meta: { + numValue: 1234, + floatValue: 12.34, + strValue: 'some_text', + }, + }); + }); + + test('handles syntax error in meta block 1', () => { + const data = `meta + name: 0.2_mb + type: http + seq: 1 + }`; + + const result = parseBruFileMeta(data); + + expect(result).toBeUndefined(); + }); + + test('handles syntax error in meta block 2', () => { + const data = `meta { + name: 0.2_mb + type: http + seq: 1 + `; + + const result = parseBruFileMeta(data); + + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/bruno-tests/collection_oauth2/bruno.json b/packages/bruno-tests/collection_oauth2/bruno.json index 66949e685..82816b2b5 100644 --- a/packages/bruno-tests/collection_oauth2/bruno.json +++ b/packages/bruno-tests/collection_oauth2/bruno.json @@ -1,9 +1,11 @@ { "version": "1", - "name": "collection_oauth2", + "name": "OAuth2 Demo", "type": "collection", "scripts": { - "moduleWhitelist": ["crypto"], + "moduleWhitelist": [ + "crypto" + ], "filesystemAccess": { "allow": true } @@ -15,4 +17,4 @@ "presets": { "requestType": "http" } -} +} \ No newline at end of file From 10e0fde2a80707a6386150051c95f88c5953eb50 Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Wed, 15 Jan 2025 11:09:37 +0530 Subject: [PATCH 008/114] use lowercase header keys while making requests --- packages/bruno-electron/src/utils/collection.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index d0ec68ab1..96e75acae 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -11,7 +11,7 @@ const mergeHeaders = (collection, request, requestTreePath) => { let collectionHeaders = get(collection, 'root.request.headers', []); collectionHeaders.forEach((header) => { if (header.enabled) { - headers.set(header.name, header.value); + headers.set(header.name?.toLowerCase?.(), header.value); if (header?.name?.toLowerCase() === 'content-type') { contentTypeDefined = true; } @@ -23,14 +23,14 @@ const mergeHeaders = (collection, request, requestTreePath) => { let _headers = get(i, 'root.request.headers', []); _headers.forEach((header) => { if (header.enabled) { - headers.set(header.name, header.value); + headers.set(header.name?.toLowerCase?.(), header.value); } }); } else { const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []); _headers.forEach((header) => { if (header.enabled) { - headers.set(header.name, header.value); + headers.set(header.name?.toLowerCase?.(), header.value); } }); } From b7fda331dcbcf9e11c62f8b6458235592d1cd4cc Mon Sep 17 00:00:00 2001 From: Sanjai Kumar Date: Thu, 26 Dec 2024 18:23:29 +0530 Subject: [PATCH 009/114] Fix: Comment toggling for JSON modes in CodeEditor --- .../bruno-app/src/components/CodeEditor/index.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index 398007a4a..d589e6436 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -194,8 +194,20 @@ export default class CodeEditor extends React.Component { 'Cmd-Y': 'foldAll', 'Ctrl-I': 'unfoldAll', 'Cmd-I': 'unfoldAll', - 'Ctrl-/': 'toggleComment', - 'Cmd-/': 'toggleComment' + 'Ctrl-/': () => { + if (['application/ld+json', 'application/json'].includes(this.props.mode)) { + this.editor.toggleComment({ lineComment: '//', blockComment: '/*' }); + } else { + this.editor.toggleComment(); + } + }, + 'Cmd-/': () => { + if (['application/ld+json', 'application/json'].includes(this.props.mode)) { + this.editor.toggleComment({ lineComment: '//', blockComment: '/*' }); + } else { + this.editor.toggleComment(); + } + } }, foldOptions: { widget: (from, to) => { From 8f241a32ae1df45ad25f75cf78fa3803affafae1 Mon Sep 17 00:00:00 2001 From: Naman <72590190+NV404@users.noreply.github.com> Date: Tue, 14 Jan 2025 02:17:28 +0530 Subject: [PATCH 010/114] Feature: Reveal in Finder (#3698) * feat: Reveal in Finder * added support for linux --- .../Collection/CollectionItem/index.js | 19 +++++++++- .../ReduxStore/slices/collections/actions.js | 9 +++++ packages/bruno-electron/src/ipc/collection.js | 36 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 3c9f27115..b840885de 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -6,7 +6,7 @@ import { useDrag, useDrop } from 'react-dnd'; import { IconChevronRight, IconDots } from '@tabler/icons'; import { useSelector, useDispatch } from 'react-redux'; import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs'; -import { moveItem, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; +import { moveItem, revealInFinder, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections'; import Dropdown from 'components/Dropdown'; import NewRequest from 'components/Sidebar/NewRequest'; @@ -220,6 +220,12 @@ const CollectionItem = ({ item, collection, searchText }) => { } }; + const handleReveal = () => { + dispatch(revealInFinder(item.pathname)).catch((error) => { + toast.error('Error revealing file:', error); + }); + }; + const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i))); const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i))); @@ -371,6 +377,17 @@ const CollectionItem = ({ item, collection, searchText }) => { Generate Code
)} + {!isFolder && ( +
{ + dropdownTippyRef.current.hide(); + handleReveal(); + }} + > + Reveal in Finder +
+ )}
{ 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 de9ad78e9..513c2f798 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -1219,3 +1219,12 @@ export const mountCollection = ({ collectionUid, collectionPathname, brunoConfig }); }); }; + + export const revealInFinder = (collectionPath) => () => { + return new Promise((resolve, reject) => { + const { ipcRenderer } = window; + + ipcRenderer.invoke('renderer:reveal-in-finder', collectionPath).then(resolve).catch(reject); + }); + }; + diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index b9061a227..f04b49619 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -3,6 +3,7 @@ const fs = require('fs'); const fsExtra = require('fs-extra'); const os = require('os'); const path = require('path'); +const { exec } = require('child_process'); const { ipcMain, shell, dialog, app } = require('electron'); const { envJsonToBru, bruToJson, jsonToBruViaWorker, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru'); @@ -909,6 +910,41 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection watcher.addWatcher(mainWindow, collectionPathname, collectionUid, brunoConfig, false, shouldLoadCollectionAsync); }); + + ipcMain.handle('renderer:reveal-in-finder', async (event, filePath) => { + try { + if (!filePath) { + throw new Error('File path is required.'); + } + + const resolvedPath = path.resolve(filePath); + + if (!fs.existsSync(resolvedPath)) { + throw new Error('The specified file does not exist.'); + } + + console.log(process.platform, "process.platform") + + switch (process.platform) { + case 'darwin': // macOS + shell.showItemInFolder(resolvedPath); + break; + case 'win32': // Windows + shell.showItemInFolder(resolvedPath); + break; + case 'linux': // Linux + exec(`xdg-open "${resolvedPath}"`); + break; + default: + throw new Error('Unsupported platform.'); + } + + return { success: true }; + } catch (error) { + console.error('Error in reveal-in-finder:', error); + return { success: false, message: error.message }; + } + }); }; const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => { From b51f8109a208b9c3b5a7e81bcc9abd52eb6be3ae Mon Sep 17 00:00:00 2001 From: ramki-bruno Date: Mon, 13 Jan 2025 19:30:55 +0530 Subject: [PATCH 011/114] Review changes and minor-refactoring in `Reveal in Finder` feature --- .../Collection/CollectionItem/index.js | 29 ++++++++------- .../ReduxStore/slices/collections/actions.js | 6 ++-- packages/bruno-electron/src/ipc/collection.js | 36 ++++--------------- 3 files changed, 22 insertions(+), 49 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index b840885de..2bfece171 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -6,7 +6,7 @@ import { useDrag, useDrop } from 'react-dnd'; import { IconChevronRight, IconDots } from '@tabler/icons'; import { useSelector, useDispatch } from 'react-redux'; import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs'; -import { moveItem, revealInFinder, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; +import { moveItem, showInFolder, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections'; import Dropdown from 'components/Dropdown'; import NewRequest from 'components/Sidebar/NewRequest'; @@ -220,9 +220,10 @@ const CollectionItem = ({ item, collection, searchText }) => { } }; - const handleReveal = () => { - dispatch(revealInFinder(item.pathname)).catch((error) => { - toast.error('Error revealing file:', error); + const handleShowInFolder = () => { + dispatch(showInFolder(item.pathname)).catch((error) => { + console.error('Error opening the folder', error); + toast.error('Error opening the folder'); }); }; @@ -377,17 +378,15 @@ const CollectionItem = ({ item, collection, searchText }) => { Generate Code
)} - {!isFolder && ( -
{ - dropdownTippyRef.current.hide(); - handleReveal(); - }} - > - Reveal in Finder -
- )} +
{ + dropdownTippyRef.current.hide(); + handleShowInFolder(); + }} + > + Show in Folder +
{ 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 513c2f798..cc53b3339 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -1220,11 +1220,9 @@ export const mountCollection = ({ collectionUid, collectionPathname, brunoConfig }); }; - export const revealInFinder = (collectionPath) => () => { + export const showInFolder = (collectionPath) => () => { return new Promise((resolve, reject) => { const { ipcRenderer } = window; - - ipcRenderer.invoke('renderer:reveal-in-finder', collectionPath).then(resolve).catch(reject); + ipcRenderer.invoke('renderer:show-in-folder', collectionPath).then(resolve).catch(reject); }); }; - diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index f04b49619..c7454c113 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -3,7 +3,6 @@ const fs = require('fs'); const fsExtra = require('fs-extra'); const os = require('os'); const path = require('path'); -const { exec } = require('child_process'); const { ipcMain, shell, dialog, app } = require('electron'); const { envJsonToBru, bruToJson, jsonToBruViaWorker, jsonToCollectionBru, bruToJsonViaWorker } = require('../bru'); @@ -911,40 +910,17 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection watcher.addWatcher(mainWindow, collectionPathname, collectionUid, brunoConfig, false, shouldLoadCollectionAsync); }); - ipcMain.handle('renderer:reveal-in-finder', async (event, filePath) => { + ipcMain.handle('renderer:show-in-folder', async (event, filePath) => { try { if (!filePath) { - throw new Error('File path is required.'); + throw new Error('File path is required'); } - - const resolvedPath = path.resolve(filePath); - - if (!fs.existsSync(resolvedPath)) { - throw new Error('The specified file does not exist.'); - } - - console.log(process.platform, "process.platform") - - switch (process.platform) { - case 'darwin': // macOS - shell.showItemInFolder(resolvedPath); - break; - case 'win32': // Windows - shell.showItemInFolder(resolvedPath); - break; - case 'linux': // Linux - exec(`xdg-open "${resolvedPath}"`); - break; - default: - throw new Error('Unsupported platform.'); - } - - return { success: true }; + shell.showItemInFolder(filePath); } catch (error) { - console.error('Error in reveal-in-finder:', error); - return { success: false, message: error.message }; + console.error('Error in show-in-folder: ', error); + throw error; } - }); + }); }; const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => { From 2d08567d8d82b54471d0c03daad53bb56b43cab5 Mon Sep 17 00:00:00 2001 From: Jarod Gowgiel Date: Mon, 13 Jan 2025 16:32:44 -0700 Subject: [PATCH 012/114] Add an npm script to run the Electron app for debugging (#3616) This allows for developers to attach Dev Tools, e.g. the Chrome "dedicated DevTools for node", to the main Electron process for debugging operations that occur on the main process. --- package.json | 1 + packages/bruno-electron/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/package.json b/package.json index 2c46fdd2c..b105c1e0b 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "build:web": "npm run build --workspace=packages/bruno-app", "prettier:web": "npm run prettier --workspace=packages/bruno-app", "dev:electron": "npm run dev --workspace=packages/bruno-electron", + "dev:electron:debug": "npm run debug --workspace=packages/bruno-electron", "build:bruno-common": "npm run build --workspace=packages/bruno-common", "build:bruno-query": "npm run build --workspace=packages/bruno-query", "build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs", diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index f554e54d9..c3d2cc2e5 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -9,6 +9,7 @@ "scripts": { "clean": "rimraf dist", "dev": "electron .", + "debug": "electron . --inspect=9229", "dist:mac": "electron-builder --mac --config electron-builder-config.js", "dist:win": "electron-builder --win --config electron-builder-config.js", "dist:linux": "electron-builder --linux AppImage --config electron-builder-config.js", From d13e4b3b54cdee0f707f935d1c3713b4c999c7ee Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Tue, 21 Jan 2025 23:15:07 +0530 Subject: [PATCH 013/114] ensure variables set in scripts/tests during `bru.runRequest` reflect in original request scripts/tests --- .../bruno-electron/src/ipc/network/index.js | 50 ++++++++-------- .../scripting/api/bru/runRequest-1.bru | 57 +++++++++++++++++++ .../scripting/api/bru/runRequest-2.bru | 21 +++++++ 3 files changed, 105 insertions(+), 23 deletions(-) create mode 100644 packages/bruno-tests/collection/scripting/api/bru/runRequest-1.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/runRequest-2.bru diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 218fadf38..4a2b29c7d 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -274,7 +274,6 @@ const configureRequest = async ( }); } - let axiosInstance = makeAxiosInstance(); if (request.ntlmConfig) { @@ -403,7 +402,7 @@ const registerNetworkIpc = (mainWindow) => { requestUid, envVars, collectionPath, - collectionRoot, + collection, collectionUid, runtimeVariables, processEnvVars, @@ -437,6 +436,8 @@ const registerNetworkIpc = (mainWindow) => { mainWindow.webContents.send('main:global-environment-variables-update', { globalEnvironmentVariables: scriptResult.globalEnvironmentVariables }); + + collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables; } // interpolate variables inside request @@ -470,7 +471,7 @@ const registerNetworkIpc = (mainWindow) => { requestUid, envVars, collectionPath, - collectionRoot, + collection, collectionUid, runtimeVariables, processEnvVars, @@ -507,6 +508,8 @@ const registerNetworkIpc = (mainWindow) => { if (result?.error) { mainWindow.webContents.send('main:display-error', result.error); } + + collection.globalEnvironmentVariables = result.globalEnvironmentVariables; } // run post-response script @@ -537,11 +540,13 @@ const registerNetworkIpc = (mainWindow) => { mainWindow.webContents.send('main:global-environment-variables-update', { globalEnvironmentVariables: scriptResult.globalEnvironmentVariables }); + + collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables; } return scriptResult; }; - const runRequest = async ({ item, collection, environment, runtimeVariables, runInBackground = false }) => { + const runRequest = async ({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground = false }) => { const collectionUid = collection.uid; const collectionPath = collection.pathname; const cancelTokenUid = uuid(); @@ -553,9 +558,9 @@ const registerNetworkIpc = (mainWindow) => { if (itemPathname && !itemPathname?.endsWith('.bru')) { itemPathname = `${itemPathname}.bru`; } - const _item = findItemInCollectionByPathname(collection, itemPathname); + const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname)); if(_item) { - const res = await runRequest({ item: _item, collection, environment, runtimeVariables, runInBackground: true }); + const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true }); resolve(res); } reject(`bru.runRequest: invalid request path - ${itemPathname}`); @@ -570,11 +575,8 @@ const registerNetworkIpc = (mainWindow) => { cancelTokenUid }); - const collectionRoot = get(collection, 'root', {}); const request = prepareRequest(item, collection); request.__bruno__executionMode = 'standalone'; - const envVars = getEnvVars(environment); - const processEnvVars = getProcessEnvVars(collectionUid); const brunoConfig = getBrunoConfig(collectionUid); const scriptingConfig = get(brunoConfig, 'scripts', {}); scriptingConfig.runtime = getJsSandboxRuntime(collection); @@ -589,7 +591,7 @@ const registerNetworkIpc = (mainWindow) => { requestUid, envVars, collectionPath, - collectionRoot, + collection, collectionUid, runtimeVariables, processEnvVars, @@ -674,7 +676,7 @@ const registerNetworkIpc = (mainWindow) => { requestUid, envVars, collectionPath, - collectionRoot, + collection, collectionUid, runtimeVariables, processEnvVars, @@ -758,7 +760,10 @@ const registerNetworkIpc = (mainWindow) => { // handler for sending http request ipcMain.handle('send-http-request', async (event, item, collection, environment, runtimeVariables) => { - return await runRequest({ item, collection, environment, runtimeVariables }); + const collectionUid = collection.uid; + const envVars = getEnvVars(environment); + const processEnvVars = getProcessEnvVars(collectionUid); + return await runRequest({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: false }); }); ipcMain.handle('send-collection-oauth2-request', async (event, collection, environment, runtimeVariables) => { @@ -782,7 +787,7 @@ const registerNetworkIpc = (mainWindow) => { requestUid, envVars, collectionPath, - collectionRoot, + collection, collectionUid, runtimeVariables, processEnvVars, @@ -818,7 +823,7 @@ const registerNetworkIpc = (mainWindow) => { requestUid, envVars, collectionPath, - collectionRoot, + collection, collectionUid, runtimeVariables, processEnvVars, @@ -888,7 +893,7 @@ const registerNetworkIpc = (mainWindow) => { requestUid, envVars, collectionPath, - collectionRoot, + collection, collectionUid, runtimeVariables, processEnvVars, @@ -912,7 +917,7 @@ const registerNetworkIpc = (mainWindow) => { requestUid, envVars, collectionPath, - collectionRoot, + collection, collectionUid, runtimeVariables, processEnvVars, @@ -949,7 +954,8 @@ const registerNetworkIpc = (mainWindow) => { const brunoConfig = getBrunoConfig(collectionUid); const scriptingConfig = get(brunoConfig, 'scripts', {}); scriptingConfig.runtime = getJsSandboxRuntime(collection); - const collectionRoot = get(collection, 'root', {}); + const envVars = getEnvVars(environment); + const processEnvVars = getProcessEnvVars(collectionUid); let stopRunnerExecution = false; const abortController = new AbortController(); @@ -961,9 +967,9 @@ const registerNetworkIpc = (mainWindow) => { if (itemPathname && !itemPathname?.endsWith('.bru')) { itemPathname = `${itemPathname}.bru`; } - const _item = findItemInCollectionByPathname(collection, itemPathname); + const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname)); if(_item) { - const res = await runRequest({ item: _item, collection, environment, runtimeVariables, runInBackground: true }); + const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true }); resolve(res); } reject(`bru.runRequest: invalid request path - ${itemPathname}`); @@ -983,7 +989,6 @@ const registerNetworkIpc = (mainWindow) => { }); try { - const envVars = getEnvVars(environment); let folderRequests = []; if (recursive) { @@ -1035,7 +1040,6 @@ const registerNetworkIpc = (mainWindow) => { request.__bruno__executionMode = 'runner'; const requestUid = uuid(); - const processEnvVars = getProcessEnvVars(collectionUid); try { const preRequestScriptResult = await runPreRequest( @@ -1043,7 +1047,7 @@ const registerNetworkIpc = (mainWindow) => { requestUid, envVars, collectionPath, - collectionRoot, + collection, collectionUid, runtimeVariables, processEnvVars, @@ -1181,7 +1185,7 @@ const registerNetworkIpc = (mainWindow) => { requestUid, envVars, collectionPath, - collectionRoot, + collection, collectionUid, runtimeVariables, processEnvVars, diff --git a/packages/bruno-tests/collection/scripting/api/bru/runRequest-1.bru b/packages/bruno-tests/collection/scripting/api/bru/runRequest-1.bru new file mode 100644 index 000000000..599f80c4c --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/runRequest-1.bru @@ -0,0 +1,57 @@ +meta { + name: runRequest-1 + type: http + seq: 10 +} + +post { + url: {{echo-host}} + body: text + auth: none +} + +body:text { + bruno +} + +script:pre-request { + // reset values + bru.setVar('run-request-runtime-var', null); + bru.setEnvVar('run-request-env-var', null); + bru.setGlobalEnvVar('run-request-global-env-var', null); + + // the above vars will be set in the below request + const resp = await bru.runRequest('scripting/api/bru/runRequest-2'); + + bru.setVar('run-request-resp', { + data: resp?.data, + statusText: resp?.statusText, + status: resp?.status + }); +} + +tests { + test("should get runtime var set in runRequest-2", function() { + const val = bru.getVar("run-request-runtime-var"); + expect(val).to.equal("run-request-runtime-var-value"); + }); + + test("should get env var set in runRequest-2", function() { + const val = bru.getEnvVar("run-request-env-var"); + expect(val).to.equal("run-request-env-var-value"); + }); + + test("should get global env var set in runRequest-2", function() { + const val = bru.getGlobalEnvVar("run-request-global-env-var"); + expect(val).to.equal("run-request-global-env-var-value"); + }); + + test("should get response of runRequest-2", function() { + const val = bru.getVar('run-request-resp'); + expect(JSON.stringify(val)).to.equal(JSON.stringify({ + "data": "bruno", + "statusText": "OK", + "status": 200 + })); + }); +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/runRequest-2.bru b/packages/bruno-tests/collection/scripting/api/bru/runRequest-2.bru new file mode 100644 index 000000000..7a5f4d08d --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/runRequest-2.bru @@ -0,0 +1,21 @@ +meta { + name: runRequest-2 + type: http + seq: 11 +} + +post { + url: {{echo-host}} + body: text + auth: none +} + +body:text { + bruno +} + +script:pre-request { + bru.setVar('run-request-runtime-var', 'run-request-runtime-var-value'); + bru.setEnvVar('run-request-env-var', 'run-request-env-var-value'); + bru.setGlobalEnvVar('run-request-global-env-var', 'run-request-global-env-var-value'); +} From ff5683f19f705a7123c19f077ffa26ec0ce9fc7d Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Thu, 23 Jan 2025 17:54:04 +0530 Subject: [PATCH 014/114] add runRequest and runner utils functions to cli ~ add bru.runRequest support for cli ~ add bru.runner.skipRequest, bru.runner.stopExecution support for cli --- packages/bruno-cli/src/commands/run.js | 49 +++++++++- .../src/runner/run-single-request.js | 56 +++++++++-- packages/bruno-cli/src/utils/collection.js | 1 + .../collection/ping-another-one.bru | 15 +++ packages/bruno-tests/collection/ping.bru | 4 + .../scripting/api/bru/runRequest-1.bru | 5 +- .../scripting/api/bru/runRequest.bru | 96 +++++++++++++++++++ .../collection/scripting/api/bru/runner/1.bru | 19 ++++ .../collection/scripting/api/bru/runner/2.bru | 19 ++++ .../collection/scripting/api/bru/runner/3.bru | 11 +++ 10 files changed, 265 insertions(+), 10 deletions(-) create mode 100644 packages/bruno-tests/collection/ping-another-one.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/runRequest.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/runner/1.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/runner/2.bru create mode 100644 packages/bruno-tests/collection/scripting/api/bru/runner/3.bru diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 29edf63b9..4f82a86e6 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -11,6 +11,7 @@ const { rpad } = require('../utils/common'); const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru'); const { dotenvToJson } = require('@usebruno/lang'); const constants = require('../constants'); +const { findItemInCollection } = require('../utils/collection'); const command = 'run [filename]'; const desc = 'Run a request'; @@ -18,6 +19,7 @@ const printRunSummary = (results) => { let totalRequests = 0; let passedRequests = 0; let failedRequests = 0; + let skippedRequests = 0; let totalAssertions = 0; let passedAssertions = 0; let failedAssertions = 0; @@ -49,7 +51,10 @@ const printRunSummary = (results) => { failedAssertions += 1; } } - if (!hasAnyTestsOrAssertions && result.error) { + if (!hasAnyTestsOrAssertions && result.skipped) { + skippedRequests += 1; + } + else if (!hasAnyTestsOrAssertions && result.error) { failedRequests += 1; } else { passedRequests += 1; @@ -62,6 +67,9 @@ const printRunSummary = (results) => { if (failedRequests > 0) { requestSummary += `, ${chalk.red(`${failedRequests} failed`)}`; } + if (skippedRequests > 0) { + requestSummary += `, ${chalk.magenta(`${skippedRequests} skipped`)}`; + } requestSummary += `, ${totalRequests} total`; let assertSummary = `${rpad('Tests:', maxLength)} ${chalk.green(`${passedTests} passed`)}`; @@ -84,6 +92,7 @@ const printRunSummary = (results) => { totalRequests, passedRequests, failedRequests, + skippedRequests, totalAssertions, passedAssertions, failedAssertions, @@ -144,7 +153,7 @@ const createCollectionFromPath = (collectionPath) => { }); } } - return currentDirItems + return currentDirItems; }; collection.items = traverse(collectionPath); return collection; @@ -634,6 +643,34 @@ const handler = async function (argv) { } const runtime = getJsSandboxRuntime(sandbox); + + const runSingleRequestByPathname = async (relativeItemPathname) => { + return new Promise(async (resolve, reject) => { + let itemPathname = path.join(collectionPath, relativeItemPathname); + if (itemPathname && !itemPathname?.endsWith('.bru')) { + itemPathname = `${itemPathname}.bru`; + } + const bruJson = cloneDeep(findItemInCollection(collection, itemPathname)); + if (bruJson) { + const res = await runSingleRequest( + itemPathname, + bruJson, + collectionPath, + runtimeVariables, + envVars, + processEnvVars, + brunoConfig, + collectionRoot, + runtime, + collection, + runSingleRequestByPathname + ); + resolve(res?.response); + } + reject(`bru.runRequest: invalid request path - ${itemPathname}`); + }); + } + let currentRequestIndex = 0; let nJumps = 0; // count the number of jumps to avoid infinite loops while (currentRequestIndex < bruJsons.length) { @@ -651,7 +688,8 @@ const handler = async function (argv) { brunoConfig, collectionRoot, runtime, - collection + collection, + runSingleRequestByPathname ); results.push({ @@ -701,6 +739,11 @@ const handler = async function (argv) { // determine next request const nextRequestName = result?.nextRequestName; + + if (result?.shouldStopRunnerExecution) { + break; + } + if (nextRequestName !== undefined) { nJumps++; if (nJumps > 10000) { diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index b2bbc3795..021980dfd 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -40,11 +40,13 @@ const runSingleRequest = async function ( brunoConfig, collectionRoot, runtime, - collection + collection, + runSingleRequestByPathname ) { try { let request; let nextRequestName; + let shouldStopRunnerExecution = false; let item = { pathname: path.join(collectionPath, filename), ...bruJson @@ -68,11 +70,41 @@ const runSingleRequest = async function ( collectionPath, onConsoleLog, processEnvVars, - scriptingConfig + scriptingConfig, + runSingleRequestByPathname ); if (result?.nextRequestName !== undefined) { nextRequestName = result.nextRequestName; } + + if (result?.stopExecution) { + shouldStopRunnerExecution = true; + } + + if (result?.skipRequest) { + return { + test: { + filename: filename + }, + request: { + method: request.method, + url: request.url, + headers: request.headers, + data: request.data + }, + response: { + status: 'skipped', + statusText: 'request skipped via pre-request script', + data: null, + responseTime: 0 + }, + error: 'Request has been skipped from pre-request script', + skipped: true, + assertionResults: [], + testResults: [], + shouldStopRunnerExecution + }; + } } // interpolate variables inside request @@ -323,7 +355,8 @@ const runSingleRequest = async function ( error: err?.message || err?.errors?.map(e => e?.message)?.at(0) || err?.code || 'Request Failed!', assertionResults: [], testResults: [], - nextRequestName: nextRequestName + nextRequestName: nextRequestName, + shouldStopRunnerExecution }; } } @@ -363,11 +396,16 @@ const runSingleRequest = async function ( collectionPath, null, processEnvVars, - scriptingConfig + scriptingConfig, + runSingleRequestByPathname ); if (result?.nextRequestName !== undefined) { nextRequestName = result.nextRequestName; } + + if (result?.stopExecution) { + shouldStopRunnerExecution = true; + } } // run assertions @@ -408,13 +446,18 @@ const runSingleRequest = async function ( collectionPath, null, processEnvVars, - scriptingConfig + scriptingConfig, + runSingleRequestByPathname ); testResults = get(result, 'results', []); if (result?.nextRequestName !== undefined) { nextRequestName = result.nextRequestName; } + + if (result?.stopExecution) { + shouldStopRunnerExecution = true; + } } if (testResults?.length) { @@ -447,7 +490,8 @@ const runSingleRequest = async function ( error: null, assertionResults, testResults, - nextRequestName: nextRequestName + nextRequestName: nextRequestName, + shouldStopRunnerExecution }; } catch (err) { console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`)); diff --git a/packages/bruno-cli/src/utils/collection.js b/packages/bruno-cli/src/utils/collection.js index 365732c48..64e17cb39 100644 --- a/packages/bruno-cli/src/utils/collection.js +++ b/packages/bruno-cli/src/utils/collection.js @@ -204,5 +204,6 @@ module.exports = { mergeHeaders, mergeVars, mergeScripts, + findItemInCollection, getTreePathFromCollectionToItem } \ No newline at end of file diff --git a/packages/bruno-tests/collection/ping-another-one.bru b/packages/bruno-tests/collection/ping-another-one.bru new file mode 100644 index 000000000..84c1412a8 --- /dev/null +++ b/packages/bruno-tests/collection/ping-another-one.bru @@ -0,0 +1,15 @@ +meta { + name: ping-another-one + type: http + seq: 2 +} + +get { + url: {{host}}/ping + body: none + auth: none +} + +script:pre-request { + throw new Error('this should not execute in a collection run'); +} diff --git a/packages/bruno-tests/collection/ping.bru b/packages/bruno-tests/collection/ping.bru index 3abc7a2d4..8f4f3c6f7 100644 --- a/packages/bruno-tests/collection/ping.bru +++ b/packages/bruno-tests/collection/ping.bru @@ -9,3 +9,7 @@ get { body: none auth: none } + +script:pre-request { + bru.runner.stopExecution(); +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/runRequest-1.bru b/packages/bruno-tests/collection/scripting/api/bru/runRequest-1.bru index 599f80c4c..95b87239f 100644 --- a/packages/bruno-tests/collection/scripting/api/bru/runRequest-1.bru +++ b/packages/bruno-tests/collection/scripting/api/bru/runRequest-1.bru @@ -43,7 +43,10 @@ tests { test("should get global env var set in runRequest-2", function() { const val = bru.getGlobalEnvVar("run-request-global-env-var"); - expect(val).to.equal("run-request-global-env-var-value"); + const executionMode = req.getExecutionMode(); + if (executionMode == 'runner') { + expect(val).to.equal("run-request-global-env-var-value"); + } }); test("should get response of runRequest-2", function() { diff --git a/packages/bruno-tests/collection/scripting/api/bru/runRequest.bru b/packages/bruno-tests/collection/scripting/api/bru/runRequest.bru new file mode 100644 index 000000000..7eb0e332c --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/runRequest.bru @@ -0,0 +1,96 @@ +meta { + name: runRequest + type: http + seq: 2 +} + +post { + url: {{host}}/api/echo/json + body: json + auth: none +} + +headers { + foo: bar +} + +auth:basic { + username: asd + password: j +} + +auth:bearer { + token: +} + +body:json { + { + "hello": "bruno" + } +} + +assert { + res.status: eq 200 +} + +script:pre-request { + bru.setVar("runRequest-ping-res-1", null); + bru.setVar("runRequest-ping-res-2", null); + bru.setVar("runRequest-ping-res-3", null); + + let pingRes = await bru.runRequest('ping'); + bru.setVar('runRequest-ping-res-1', { + data: pingRes?.data, + statusText: pingRes?.statusText, + status: pingRes?.status + }); +} + +script:post-response { + let pingRes = await bru.runRequest('ping'); + bru.setVar('runRequest-ping-res-2', { + data: pingRes?.data, + statusText: pingRes?.statusText, + status: pingRes?.status + }); +} + +tests { + const pingRes = await bru.runRequest('ping'); + bru.setVar('runRequest-ping-res-3', { + data: pingRes?.data, + statusText: pingRes?.statusText, + status: pingRes?.status + }); + + test("should run request and return valid response in pre-request script", function() { + const expectedPingRes = { + data: "pong", + statusText: "OK", + status: 200 + }; + const pingRes = bru.getVar('runRequest-ping-res-1'); + expect(pingRes).to.eql(expectedPingRes); + }); + + test("should run request and return valid response in post-response script", function() { + const expectedPingRes = { + data: "pong", + statusText: "OK", + status: 200 + }; + const pingRes = bru.getVar('runRequest-ping-res-2'); + expect(pingRes).to.eql(expectedPingRes); + }); + + test("should run request and return valid response in tests script", function() { + const expectedPingRes = { + data: "pong", + statusText: "OK", + status: 200 + }; + const pingRes = bru.getVar('runRequest-ping-res-3'); + expect(pingRes).to.eql(expectedPingRes); + }); + +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/runner/1.bru b/packages/bruno-tests/collection/scripting/api/bru/runner/1.bru new file mode 100644 index 000000000..97a7edbb6 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/runner/1.bru @@ -0,0 +1,19 @@ +meta { + name: 1 + type: http + seq: 1 +} + +post { + url: https://echo.usebruno.com + body: none + auth: none +} + +script:pre-request { + bru.setVar('bru-runner-req', 1); +} + +script:post-response { + bru.setVar('bru.runner.skipRequest', true); +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/runner/2.bru b/packages/bruno-tests/collection/scripting/api/bru/runner/2.bru new file mode 100644 index 000000000..b1be74b22 --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/runner/2.bru @@ -0,0 +1,19 @@ +meta { + name: 2 + type: http + seq: 2 +} + +post { + url: https://echo.usebruno.com + body: none + auth: none +} + +script:pre-request { + bru.runner.skipRequest(); +} + +script:post-response { + bru.setVar('bru.runner.skipRequest', false); +} diff --git a/packages/bruno-tests/collection/scripting/api/bru/runner/3.bru b/packages/bruno-tests/collection/scripting/api/bru/runner/3.bru new file mode 100644 index 000000000..4abe00b4c --- /dev/null +++ b/packages/bruno-tests/collection/scripting/api/bru/runner/3.bru @@ -0,0 +1,11 @@ +meta { + name: 3 + type: http + seq: 3 +} + +post { + url: https://echo.usebruno.com + body: none + auth: none +} From 51e087efbaa7972d7933a1ad3c2468af3b42ee98 Mon Sep 17 00:00:00 2001 From: Pooja Belaramani Date: Wed, 29 Jan 2025 13:11:18 +0530 Subject: [PATCH 015/114] add: hint word for runRequest, sendNextRequest, skipRequest, getTestResults --- packages/bruno-app/src/components/CodeEditor/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index d589e6436..aa633f067 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -74,13 +74,17 @@ if (!SERVER_RENDERED) { 'bru.setNextRequest(requestName)', 'req.disableParsingResponseJson()', 'bru.getRequestVar(key)', + 'bru.runRequest(requestName)', + 'bru.sendNextRequest()', + 'bru.skipRequest()', + 'bru.getTestResults()', 'bru.sleep(ms)', 'bru.getGlobalEnvVar(key)', 'bru.setGlobalEnvVar(key, value)', 'bru.runner', 'bru.runner.setNextRequest(requestName)', 'bru.runner.skipRequest()', - 'bru.runner.stopExecution()' + 'bru.runner.stopExecution()', ]; CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => { const cursor = editor.getCursor(); From 08139a8f3ebd37ba9c1ef83a07f8da71c0a0e163 Mon Sep 17 00:00:00 2001 From: Pooja Belaramani Date: Wed, 29 Jan 2025 13:21:03 +0530 Subject: [PATCH 016/114] fix --- packages/bruno-app/src/components/CodeEditor/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index aa633f067..2f9ca9cdd 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -74,9 +74,8 @@ if (!SERVER_RENDERED) { 'bru.setNextRequest(requestName)', 'req.disableParsingResponseJson()', 'bru.getRequestVar(key)', - 'bru.runRequest(requestName)', - 'bru.sendNextRequest()', - 'bru.skipRequest()', + 'bru.runRequest(requestPathName)', + 'bru.getAssertionResults()', 'bru.getTestResults()', 'bru.sleep(ms)', 'bru.getGlobalEnvVar(key)', From 650cb47a8b4d4b087a3625075fb5ec15fe8bd123 Mon Sep 17 00:00:00 2001 From: Pooja Belaramani Date: Tue, 28 Jan 2025 13:55:51 +0530 Subject: [PATCH 017/114] fix: resolve function recusion --- .../src/utils/importers/openapi-collection.js | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/bruno-app/src/utils/importers/openapi-collection.js b/packages/bruno-app/src/utils/importers/openapi-collection.js index d579402cd..12bddaa54 100644 --- a/packages/bruno-app/src/utils/importers/openapi-collection.js +++ b/packages/bruno-app/src/utils/importers/openapi-collection.js @@ -229,26 +229,27 @@ const transformOpenapiRequestItem = (request) => { return brunoRequestItem; }; -const resolveRefs = (spec, components = spec?.components, visitedItems = new Set()) => { +const resolveRefs = (spec, components = spec?.components, cache = new Map()) => { if (!spec || typeof spec !== 'object') { return spec; } + if (cache.has(spec)) { + return cache.get(spec); + } + if (Array.isArray(spec)) { - return spec.map((item) => resolveRefs(item, components, visitedItems)); + return spec.map(item => resolveRefs(item, components, cache)); } if ('$ref' in spec) { const refPath = spec.$ref; - if (visitedItems.has(refPath)) { - return spec; - } else { - visitedItems.add(refPath); + if (cache.has(refPath)) { + return cache.get(refPath); } if (refPath.startsWith('#/components/')) { - // Local reference within components const refKeys = refPath.replace('#/components/', '').split('/'); let ref = components; @@ -256,25 +257,26 @@ const resolveRefs = (spec, components = spec?.components, visitedItems = new Set if (ref && ref[key]) { ref = ref[key]; } else { - // Handle invalid references gracefully? return spec; } } - return resolveRefs(ref, components, visitedItems); - } else { - // Handle external references (not implemented here) - // You would need to fetch the external reference and resolve it. - // Example: Fetch and resolve an external reference from a URL. + cache.set(refPath, {}); + const resolved = resolveRefs(ref, components, cache); + cache.set(refPath, resolved); + return resolved; } + return spec; } - // Recursively resolve references in nested objects - for (const prop in spec) { - spec[prop] = resolveRefs(spec[prop], components, new Set(visitedItems)); + const resolved = {}; + cache.set(spec, resolved); + + for (const [key, value] of Object.entries(spec)) { + resolved[key] = resolveRefs(value, components, cache); } - return spec; + return resolved; }; const groupRequestsByTags = (requests) => { From 062ab00a665ea0ab83ec5e140bf46613b50601b6 Mon Sep 17 00:00:00 2001 From: Pooja Belaramani Date: Thu, 30 Jan 2025 10:12:36 +0530 Subject: [PATCH 018/114] fix: ensure API key values are converted to strings in Postman collection importer. --- packages/bruno-app/src/utils/importers/postman-collection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js index b5a685e71..eea9b0d55 100644 --- a/packages/bruno-app/src/utils/importers/postman-collection.js +++ b/packages/bruno-app/src/utils/importers/postman-collection.js @@ -422,7 +422,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) = brunoRequestItem.request.auth.mode = 'apikey'; brunoRequestItem.request.auth.apikey = { key: authValues.key, - value: authValues.value, + value: authValues.value.toString(), // Convert the value to a string as Postman's schema does not rigidly define the type of it, placement: "header" //By default we are placing the apikey values in headers! } } From 4598acd0687427870ae672ad0285e280ee1ede8c Mon Sep 17 00:00:00 2001 From: Pooja Belaramani Date: Thu, 30 Jan 2025 13:03:13 +0530 Subject: [PATCH 019/114] fix --- packages/bruno-app/src/utils/importers/postman-collection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js index eea9b0d55..89ab15c7e 100644 --- a/packages/bruno-app/src/utils/importers/postman-collection.js +++ b/packages/bruno-app/src/utils/importers/postman-collection.js @@ -422,7 +422,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) = brunoRequestItem.request.auth.mode = 'apikey'; brunoRequestItem.request.auth.apikey = { key: authValues.key, - value: authValues.value.toString(), // Convert the value to a string as Postman's schema does not rigidly define the type of it, + value: authValues.value?.toString(), // Convert the value to a string as Postman's schema does not rigidly define the type of it, placement: "header" //By default we are placing the apikey values in headers! } } From ec5a5c9b569c28d4081eaa8849d0611974733983 Mon Sep 17 00:00:00 2001 From: Pragadesh-45 Date: Tue, 4 Feb 2025 17:49:40 +0530 Subject: [PATCH 020/114] fix: correct variable used in collection name update --- packages/bruno-electron/src/ipc/collection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index c7454c113..472a31bc5 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -141,7 +141,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection // Change new name of collection let brunoConfig = JSON.parse(content); brunoConfig.name = collectionName; - const cont = await stringifyJson(json); + const cont = await stringifyJson(brunoConfig); // write the bruno.json to new dir await writeFile(path.join(dirPath, 'bruno.json'), cont); From 8abf8ff9c84a75fe390aa2bac739246ce9b3d60f Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Tue, 4 Feb 2025 19:52:58 +0530 Subject: [PATCH 021/114] skipped request should not be considered as errors in junit reports --- packages/bruno-cli/src/reporters/junit.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/bruno-cli/src/reporters/junit.js b/packages/bruno-cli/src/reporters/junit.js index 30fb51939..e4a622722 100644 --- a/packages/bruno-cli/src/reporters/junit.js +++ b/packages/bruno-cli/src/reporters/junit.js @@ -62,7 +62,10 @@ const makeJUnitOutput = async (results, outputPath) => { suite.testcase.push(testcase); }); - if (result.error) { + if (result?.skipped) { + suite['@skipped'] = 1; + } + else if (result.error) { suite['@errors'] = 1; suite['@tests'] = 1; suite.testcase = [ From 324b7cec51674062a35b2bd3d2065a8f8ecb5016 Mon Sep 17 00:00:00 2001 From: Marcos Adriano Date: Mon, 3 Feb 2025 10:52:45 -0300 Subject: [PATCH 022/114] feat: raw binary files handling in request body (#3734) -------------- Co-authored-by: lohit Co-authored-by: Anoop M D --- .../src/components/FilePickerEditor/index.js | 8 +- .../RequestPane/Binary/StyledWrapper.js | 65 +++++++ .../components/RequestPane/Binary/index.js | 173 ++++++++++++++++++ .../RequestBody/RequestBodyMode/index.js | 9 + .../RequestPane/RequestBody/index.js | 5 + .../ReduxStore/slices/collections/actions.js | 7 +- .../ReduxStore/slices/collections/index.js | 89 ++++++++- .../bruno-app/src/utils/codegenerator/har.js | 59 ++++-- .../bruno-app/src/utils/collections/export.js | 1 + .../bruno-app/src/utils/collections/index.js | 23 ++- .../bruno-app/src/utils/curl/curl-to-json.js | 39 +++- .../src/utils/curl/curl-to-json.spec.js | 36 ++++ packages/bruno-app/src/utils/curl/index.js | 8 +- .../bruno-app/src/utils/importers/common.js | 1 + packages/bruno-electron/src/ipc/collection.js | 5 +- .../bruno-electron/src/ipc/network/index.js | 16 +- .../src/ipc/network/prepare-request.js | 34 +++- .../bruno-electron/src/utils/filesystem.js | 4 +- packages/bruno-lang/v2/src/bruToJson.js | 42 ++++- packages/bruno-lang/v2/src/jsonToBru.js | 26 +++ .../bruno-lang/v2/tests/fixtures/request.bru | 5 + .../bruno-lang/v2/tests/fixtures/request.json | 16 ++ .../bruno-schema/src/collections/index.js | 17 +- .../binaryFile/binary-file-types.bru | 27 +++ .../collection/binaryFile/binary-file.json | 9 + 25 files changed, 669 insertions(+), 55 deletions(-) create mode 100644 packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestPane/Binary/index.js create mode 100644 packages/bruno-tests/collection/binaryFile/binary-file-types.bru create mode 100644 packages/bruno-tests/collection/binaryFile/binary-file.json diff --git a/packages/bruno-app/src/components/FilePickerEditor/index.js b/packages/bruno-app/src/components/FilePickerEditor/index.js index 797771bbb..d976a3e79 100644 --- a/packages/bruno-app/src/components/FilePickerEditor/index.js +++ b/packages/bruno-app/src/components/FilePickerEditor/index.js @@ -6,7 +6,7 @@ import { IconX } from '@tabler/icons'; import { isWindowsOS } from 'utils/common/platform'; import slash from 'utils/common/slash'; -const FilePickerEditor = ({ value, onChange, collection }) => { +const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false}) => { value = value || []; const dispatch = useDispatch(); const filenames = value @@ -20,7 +20,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => { const title = filenames.map((v) => `- ${v}`).join('\n'); const browse = () => { - dispatch(browseFiles()) + dispatch(browseFiles([],[''])) .then((filePaths) => { // If file is in the collection's directory, then we use relative path // Otherwise, we use the absolute path @@ -49,7 +49,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => { if (filenames.length == 1) { return filenames[0]; } - return filenames.length + ' files selected'; + return filenames.length + ' file(s) selected'; }; return filenames.length > 0 ? ( @@ -66,7 +66,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
) : ( ); }; diff --git a/packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js new file mode 100644 index 000000000..35adfcc1f --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js @@ -0,0 +1,65 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + table { + width: 100%; + border-collapse: collapse; + font-weight: 600; + table-layout: fixed; + + thead, + td { + border: 1px solid ${(props) => props.theme.table.border}; + } + + thead { + color: ${(props) => props.theme.table.thead.color}; + font-size: 0.8125rem; + user-select: none; + } + td { + padding: 6px 10px; + + &:nth-child(1) { + width: 30%; + } + + &:nth-child(2) { + width: 45%; + } + + &:nth-child(3) { + width: 25%; + } + + &:nth-child(4) { + width: 70px; + } + } + } + + .btn-add-param { + font-size: 0.8125rem; + } + + input[type='text'] { + width: 100%; + border: solid 1px transparent; + outline: none !important; + color: ${(props) => props.theme.table.input.color}; + background: transparent; + + &:focus { + outline: none !important; + border: solid 1px transparent; + } + } + + input[type='radio'] { + cursor: pointer; + position: relative; + top: 1px; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Binary/index.js b/packages/bruno-app/src/components/RequestPane/Binary/index.js new file mode 100644 index 000000000..77bbda8d5 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Binary/index.js @@ -0,0 +1,173 @@ +import React from 'react'; +import get from 'lodash/get'; +import cloneDeep from 'lodash/cloneDeep'; +import { IconTrash } from '@tabler/icons'; +import { useDispatch } from 'react-redux'; +import { useTheme } from 'providers/Theme'; +import { + addBinaryFile, + updateBinaryFile, + deleteBinaryFile +} from 'providers/ReduxStore/slices/collections'; +import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; +import FilePickerEditor from 'components/FilePickerEditor'; +import SingleLineEditor from 'components/SingleLineEditor/index'; +import { isArray } from 'lodash'; +import path from 'node:path'; +import { useState } from 'react'; + +const Binary = ({ item, collection }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + const params = item.draft ? get(item, 'draft.request.body.binaryFile') : get(item, 'request.body.binaryFile'); + + const [enabledFileUid, setEnableFileUid] = useState(params && params.length ? params[0].uid : ''); + + const addFile = () => { + dispatch( + addBinaryFile({ + itemUid: item.uid, + collectionUid: collection.uid, + type: 'binaryFile', + value: [''], + }) + ); + }; + + const onSave = () => dispatch(saveRequest(item.uid, collection.uid)); + const handleRun = () => dispatch(sendRequest(item, collection.uid)); + + const handleParamChange = (e, _param, type) => { + + const param = cloneDeep(_param); + + switch (type) { + + case 'value': { + param.value = isArray(e.target.value) && e.target.value.length > 0 ? e.target.value : ['']; + param.name = param.value.length === 0 ? '': path.basename(param.value[0], path.extname(param.value[0])); + break; + } + case 'contentType': { + param.contentType = e.target.value; + break; + } + case 'enabled': { + param.enabled = e.target.checked; + + setEnableFileUid(param.uid); + + break; + } + } + dispatch( + updateBinaryFile({ + param: param, + itemUid: item.uid, + collectionUid: collection.uid + }) + ); + }; + + const handleRemoveParams = (param) => { + dispatch( + deleteBinaryFile({ + paramUid: param.uid, + itemUid: item.uid, + collectionUid: collection.uid + }) + ); + }; + + return ( + + + + + + + + + + + + {params && params.length + ? params.map((param, index) => { + return ( + + + + + + + ); + }) + : null} + +
File
Content-Type
Enabled
+ + handleParamChange( + { + target: { + value: newValue + } + }, + param, + 'value' + ) + } + collection={collection} + /> + + + handleParamChange( + { + target: { + value: newValue + } + }, + param, + 'contentType' + ) + } + onRun={handleRun} + collection={collection} + /> + +
+ handleParamChange(e, param, 'enabled')} + /> +
+
+
+ +
+
+
+ +
+
+ ); +}; +export default Binary; diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js index 29b66d58d..95b3b6a55 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js @@ -128,6 +128,15 @@ const RequestBodyMode = ({ item, collection }) => { SPARQL
Other
+
{ + dropdownTippyRef.current.hide(); + onModeChange('binaryFile'); + }} + > + Binary File +
{ diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js index ca60c8662..9a71a4ac3 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js @@ -8,6 +8,7 @@ import { useTheme } from 'providers/Theme'; import { updateRequestBody } from 'providers/ReduxStore/slices/collections'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; +import Binary from '../Binary/index'; const RequestBody = ({ item, collection }) => { const dispatch = useDispatch(); @@ -62,6 +63,10 @@ const RequestBody = ({ item, collection }) => { ); } + if (bodyMode === 'binaryFile') { + return + } + if (bodyMode === 'formUrlEncoded') { return ; } 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 cc53b3339..f089dbddc 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -758,7 +758,8 @@ export const newHttpRequest = (params) => (dispatch, getState) => { xml: null, sparql: null, multipartForm: null, - formUrlEncoded: null + formUrlEncoded: null, + binaryFile: null }, auth: auth ?? { mode: 'none' @@ -1038,12 +1039,12 @@ export const browseDirectory = () => (dispatch, getState) => { }; export const browseFiles = - (filters = []) => + (filters = [], properties = ['multiSelections']) => (dispatch, getState) => { const { ipcRenderer } = window; return new Promise((resolve, reject) => { - ipcRenderer.invoke('renderer:browse-files', filters).then(resolve).catch(reject); + ipcRenderer.invoke('renderer:browse-files', undefined, undefined, undefined, filters, properties).then(resolve).catch(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 6a795171f..bdecc1543 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -18,6 +18,8 @@ import { import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url'; import { getDirectoryName, getSubdirectoriesFromRoot, PATH_SEPARATOR } from 'utils/common/platform'; import toast from 'react-hot-toast'; +import mime from 'mime-types'; +import path from 'node:path'; const initialState = { collections: [], @@ -873,25 +875,89 @@ export const collectionsSlice = createSlice({ } } }, - moveMultipartFormParam: (state, action) => { + moveMultipartFormParam: (state, action) => { + // Ensure item.draft is a deep clone of item if not already present + if (!item.draft) { + item.draft = cloneDeep(item); + } + + // Extract payload data + const { updateReorderedItem } = action.payload; + const params = item.draft.request.body.multipartForm; + + item.draft.request.body.multipartForm = updateReorderedItem.map((uid) => { + return params.find((param) => param.uid === uid); + }); + }, + addBinaryFile: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + + if (collection) { + const item = findItemInCollection(collection, action.payload.itemUid); + + if (item && isItemARequest(item)) { + if (!item.draft) { + item.draft = cloneDeep(item); + } + item.draft.request.body.binaryFile = item.draft.request.body.binaryFile || []; + + item.draft.request.body.binaryFile.push({ + uid: uuid(), + type: action.payload.type, + name: '', + value: [''], + contentType: '', + enabled: false + }); + } + } + }, + updateBinaryFile: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); if (collection) { const item = findItemInCollection(collection, action.payload.itemUid); if (item && isItemARequest(item)) { - // Ensure item.draft is a deep clone of item if not already present if (!item.draft) { item.draft = cloneDeep(item); } - // Extract payload data - const { updateReorderedItem } = action.payload; - const params = item.draft.request.body.multipartForm; - - item.draft.request.body.multipartForm = updateReorderedItem.map((uid) => { - return params.find((param) => param.uid === uid); + item.draft.request.body.binaryFile = item.draft.request.body.binaryFile.map((p) => { + p.enabled = false; + return p; }); + + const param = find(item.draft.request.body.binaryFile, (p) => p.uid === action.payload.param.uid); + + if (param) { + + const contentType = mime.contentType(path.extname(action.payload.param.value[0])); + + param.type = action.payload.param.type; + param.name = action.payload.param.name; + param.value = action.payload.param.value; + param.contentType = action.payload.param.contentType || contentType || ''; + param.enabled = action.payload.param.enabled; + } + } + } + }, + deleteBinaryFile: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + + if (collection) { + const item = findItemInCollection(collection, action.payload.itemUid); + + if (item && isItemARequest(item)) { + if (!item.draft) { + item.draft = cloneDeep(item); + } + + item.draft.request.body.binaryFile = filter( + item.draft.request.body.binaryFile, + (p) => p.uid !== action.payload.paramUid + ); } } }, @@ -951,6 +1017,10 @@ export const collectionsSlice = createSlice({ item.draft.request.body.sparql = action.payload.content; break; } + case 'binaryFile': { + item.draft.request.body.binaryFile = action.payload.content; + break; + } case 'formUrlEncoded': { item.draft.request.body.formUrlEncoded = action.payload.content; break; @@ -1952,6 +2022,9 @@ export const { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam, + addBinaryFile, + updateBinaryFile, + deleteBinaryFile, moveMultipartFormParam, updateRequestAuthMode, updateRequestBodyMode, diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index 479fcd67a..19e4ea4de 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -14,6 +14,8 @@ const createContentType = (mode) => { return 'application/json'; case 'multipartForm': return 'multipart/form-data'; + case 'binaryFile': + return 'application/octet-stream'; default: return ''; } @@ -60,26 +62,48 @@ const createPostData = (body, type) => { } const contentType = createContentType(body.mode); - if (body.mode === 'formUrlEncoded' || body.mode === 'multipartForm') { - return { - mimeType: contentType, - params: body[body.mode] - .filter((param) => param.enabled) - .map((param) => ({ - name: param.name, - value: param.value, - ...(param.type === 'file' && { fileName: param.value }) - })) - }; - } else { - return { - mimeType: contentType, - text: body[body.mode] - }; + + switch (body.mode) { + case 'formUrlEncoded': + case 'multipartForm': + return { + mimeType: contentType, + params: body[body.mode] + .filter((param) => param.enabled) + .map((param) => ({ + name: param.name, + value: param.value, + ...(param.type === 'file' && { fileName: param.value }) + })) + }; + case 'binaryFile': + const binary = { + mimeType: 'application/octet-stream', + // mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType, + params: body[body.mode] + .filter((param) => param.enabled) + .map((param) => ({ + name: param.name, + value: param.value, + fileName: param.value + })) + }; + + console.log('curl-binary', binary); + return binary; + default: + return { + mimeType: contentType, + text: body[body.mode] + }; } }; export const buildHarRequest = ({ request, headers, type }) => { + + console.log('buildHarRequest', request, headers, type); + + console.log('buildHarRequest-postData', createPostData(request.body, type)); return { method: request.method, url: encodeURI(request.url), @@ -89,6 +113,7 @@ export const buildHarRequest = ({ request, headers, type }) => { queryString: createQuery(request.params), postData: createPostData(request.body, type), headersSize: 0, - bodySize: 0 + bodySize: 0, + binary: true }; }; diff --git a/packages/bruno-app/src/utils/collections/export.js b/packages/bruno-app/src/utils/collections/export.js index 5ef7b1b49..b7ca9f5ba 100644 --- a/packages/bruno-app/src/utils/collections/export.js +++ b/packages/bruno-app/src/utils/collections/export.js @@ -14,6 +14,7 @@ export const deleteUidsInItems = (items) => { each(get(item, 'request.vars.assertions'), (a) => delete a.uid); each(get(item, 'request.body.multipartForm'), (param) => delete param.uid); each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid); + each(get(item, 'request.body.binaryFile'), (param) => delete param.uid); } if (item.items && item.items.length) { diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 956616710..309c245eb 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -281,6 +281,19 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} }); }; + const copyBinaryFileParams = (params = []) => { + return map(params, (param) => { + return { + uid: param.uid, + type: param.type, + name: param.name, + value: param.value, + contentType: param.contentType, + enabled: param.enabled + } + }); + } + const copyItems = (sourceItems, destItems) => { each(sourceItems, (si) => { if (!isItemAFolder(si) && !isItemARequest(si) && si.type !== 'js') { @@ -308,7 +321,8 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} graphql: si.request.body.graphql, sparql: si.request.body.sparql, formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded), - multipartForm: copyMultipartFormParams(si.request.body.multipartForm) + multipartForm: copyMultipartFormParams(si.request.body.multipartForm), + binaryFile: copyBinaryFileParams(si.request.body.binaryFile) }, script: si.request.script, vars: si.request.vars, @@ -661,6 +675,10 @@ export const humanizeRequestBodyMode = (mode) => { label = 'SPARQL'; break; } + case 'binaryFile': { + label = 'Binary File'; + break; + } case 'formUrlEncoded': { label = 'Form URL Encoded'; break; @@ -761,6 +779,7 @@ export const refreshUidsInItem = (item) => { each(get(item, 'request.params'), (param) => (param.uid = uuid())); each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid())); each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid())); + each(get(item, 'request.body.binaryFile'), (param) => (param.uid = uuid())); return item; }; @@ -771,11 +790,13 @@ export const deleteUidsInItem = (item) => { const headers = get(item, 'request.headers', []); const bodyFormUrlEncoded = get(item, 'request.body.formUrlEncoded', []); const bodyMultipartForm = get(item, 'request.body.multipartForm', []); + const binaryFile = get(item, 'request.body.binaryFile', []); params.forEach((param) => delete param.uid); headers.forEach((header) => delete header.uid); bodyFormUrlEncoded.forEach((param) => delete param.uid); bodyMultipartForm.forEach((param) => delete param.uid); + binaryFile.forEach((param) => delete param.uid); return item; }; diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js index c1398ab14..5a2c62022 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.js @@ -9,6 +9,7 @@ import parseCurlCommand from './parse-curl'; import * as querystring from 'query-string'; import * as jsesc from 'jsesc'; +import * as path from 'path'; function getContentType(headers = {}) { const contentType = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type'); @@ -99,9 +100,36 @@ function getMultipleDataString(request, parsedQueryString) { function getFilesString(request) { const data = {}; - data.files = {}; data.data = {}; + if (request.isDataBinary){ + + let filePath = '' + + if(request.data.startsWith('@')){ + filePath = request.data.slice(1); + }else{ + filePath = request.data; + } + + const fileName = path.basename(filePath); + + data.data = [ + { + name: repr(fileName), + value: [repr(filePath)], + enabled: true, + contentType: request.headers['Content-Type'], + type: 'binaryFile' + } + ]; + + return data; + + } + + data.files = {}; + for (const multipartKey in request.multipartUploads) { const multipartValue = request.multipartUploads[multipartKey]; if (multipartValue.startsWith('@')) { @@ -140,6 +168,7 @@ const curlToJson = (curlCommand) => { requestJson.url = request.urlWithoutQuery; requestJson.raw_url = request.url; requestJson.method = request.method; + requestJson.isDataBinary = request.isDataBinary; if (request.cookies) { const cookies = {}; @@ -163,11 +192,11 @@ const curlToJson = (curlCommand) => { requestJson.queries = getQueries(request); } - if (typeof request.data === 'string' || typeof request.data === 'number') { - Object.assign(requestJson, getDataString(request)); - } else if (request.multipartUploads) { + else if (request.multipartUploads || request.isDataBinary) { Object.assign(requestJson, getFilesString(request)); - } + } else if (typeof request.data === 'string' || typeof request.data === 'number') { + Object.assign(requestJson, getDataString(request)); + } if (request.insecure) { requestJson.insecure = false; diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js index 2d9785154..6f8206139 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js @@ -86,4 +86,40 @@ describe('curlToJson', () => { method: 'get' }); }); + + it('should return a parse a curl with a post body with binary file type', () => { + const curlCommand = `curl 'https://www.usebruno.com' + -H 'Accept: application/json, text/plain, */*' + -H 'Accept-Language: en-US,en;q=0.9,hi;q=0.8' + -H 'Content-Type: application/json;charset=utf-8' + -H 'Origin: https://www.usebruno.com' + -H 'Referer: https://www.usebruno.com/' + --data-binary '@/path/to/file' + `; + + const result = curlToJson(curlCommand); + + expect(result).toEqual({ + url: 'https://www.usebruno.com', + raw_url: 'https://www.usebruno.com', + method: 'post', + headers: { + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8', + 'Content-Type': 'application/json;charset=utf-8', + Origin: 'https://www.usebruno.com', + Referer: 'https://www.usebruno.com/' + }, + isDataBinary: true, + data: [ + { + name: 'file', + value: ['/path/to/file'], + enabled: true, + contentType: 'application/json;charset=utf-8', + type: 'binaryFile' + } + ] + }); + }); }); diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js index f486df56b..d91588178 100644 --- a/packages/bruno-app/src/utils/curl/index.js +++ b/packages/bruno-app/src/utils/curl/index.js @@ -50,14 +50,18 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque sparql: null, multipartForm: null, formUrlEncoded: null, - graphql: null + graphql: null, + binaryFile: null }; if (parsedBody && contentType && typeof contentType === 'string') { if (requestType === 'graphql-request' && (contentType.includes('application/json') || contentType.includes('application/graphql'))) { body.mode = 'graphql'; body.graphql = parseGraphQL(parsedBody); - } else if (contentType.includes('application/json')) { + } else if (requestType === 'http-request' && request.isDataBinary) { + body.mode = 'binaryFile'; + body.binaryFile = parsedBody; + }else if (contentType.includes('application/json')) { body.mode = 'json'; body.json = convertToCodeMirrorJson(parsedBody); } else if (contentType.includes('xml')) { diff --git a/packages/bruno-app/src/utils/importers/common.js b/packages/bruno-app/src/utils/importers/common.js index 88c4c7872..af187cc82 100644 --- a/packages/bruno-app/src/utils/importers/common.js +++ b/packages/bruno-app/src/utils/importers/common.js @@ -35,6 +35,7 @@ export const updateUidsInCollection = (_collection) => { each(get(item, 'request.assertions'), (a) => (a.uid = uuid())); each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid())); each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid())); + each(get(item, 'request.body.binaryFile'), (param) => (param.uid = uuid())); if (item.items && item.items.length) { updateItemUids(item.items); diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 472a31bc5..c1a05a49a 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -62,9 +62,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection }); // browse directory for file - ipcMain.handle('renderer:browse-files', async (event, pathname, request, filters) => { + ipcMain.handle('renderer:browse-files', async (event, pathname, request, filters, properties) => { try { - const filePaths = await browseFiles(mainWindow, filters); + + const filePaths = await browseFiles(mainWindow, filters, properties); return filePaths; } catch (error) { diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 4a2b29c7d..dbc36d846 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -575,16 +575,20 @@ const registerNetworkIpc = (mainWindow) => { cancelTokenUid }); - const request = prepareRequest(item, collection); + const abortController = new AbortController(); + + const collectionRoot = get(collection, 'root', {}); + const request = await prepareRequest(item, collection, abortController); request.__bruno__executionMode = 'standalone'; + const brunoConfig = getBrunoConfig(collectionUid); const scriptingConfig = get(brunoConfig, 'scripts', {}); scriptingConfig.runtime = getJsSandboxRuntime(collection); try { - const controller = new AbortController(); - request.signal = controller.signal; - saveCancelToken(cancelTokenUid, controller); + request.signal = abortController.signal; + + saveCancelToken(cancelTokenUid, abortController); await runPreRequest( request, @@ -614,7 +618,7 @@ const registerNetworkIpc = (mainWindow) => { url: request.url, method: request.method, headers: request.headers, - data: safeParseJSON(safeStringifyJSON(request.data)), + data: request.mode == 'binaryFile'? undefined: safeParseJSON(safeStringifyJSON(request.data)) , timestamp: Date.now() }, collectionUid, @@ -1036,7 +1040,7 @@ const registerNetworkIpc = (mainWindow) => { ...eventData }); - const request = prepareRequest(item, collection); + const request = await prepareRequest(item, collection, abortController); request.__bruno__executionMode = 'runner'; const requestUid = uuid(); diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 6c7672e7d..297033c6d 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -1,8 +1,10 @@ const { get, each, filter } = require('lodash'); const decomment = require('decomment'); const crypto = require('node:crypto'); +const fs = require('node:fs/promises'); const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars } = require('../../utils/collection'); const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data'); +const path = require('node:path'); const setAuthHeaders = (axiosRequest, request, collectionRoot) => { @@ -174,7 +176,7 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { return axiosRequest; }; -const prepareRequest = (item, collection) => { +const prepareRequest = async (item, collection, abortController) => { const request = item.draft ? item.draft.request : item.request; const collectionRoot = get(collection, 'root', {}); const collectionPath = collection.pathname; @@ -251,6 +253,36 @@ const prepareRequest = (item, collection) => { axiosRequest.data = request.body.sparql; } + if (request.body.mode === 'binaryFile') { + + if (!contentTypeDefined) { + axiosRequest.headers['content-type'] = 'application/octet-stream'; + } + + if (request.body.binaryFile && request.body.binaryFile.length > 0) { + + axiosRequest.headers['content-type'] = request.body.binaryFile[0].contentType; + + let filePath = request.body.binaryFile[0].value[0]; + + if (filePath && filePath !== '') { + + if (!path.isAbsolute(filePath)) { + + filePath = path.join(collectionPath, filePath); + } + + const file = await fs.readFile(filePath, abortController) + + axiosRequest.data = file + + if(axiosRequest.headers['content-type'].includes('application/json')) { + axiosRequest.data = JSON.parse(file) + } + } + } + } + if (request.body.mode === 'formUrlEncoded') { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index 0ab6bbf0a..d37742be4 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -121,9 +121,9 @@ const browseDirectory = async (win) => { return isDirectory(resolvedPath) ? resolvedPath : false; }; -const browseFiles = async (win, filters) => { +const browseFiles = async (win, filters, properties) => { const { filePaths } = await dialog.showOpenDialog(win, { - properties: ['openFile', 'multiSelections'], + properties: ['openFile', ...properties], filters }); diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 2fe5fb472..e7b0c5772 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -25,7 +25,7 @@ const grammar = ohm.grammar(`Bru { BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)* auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body - bodyforms = bodyformurlencoded | bodymultipart + bodyforms = bodyformurlencoded | bodymultipart | bodybinaryfile params = paramspath | paramsquery nl = "\\r"? "\\n" @@ -102,7 +102,8 @@ const grammar = ohm.grammar(`Bru { bodyformurlencoded = "body:form-urlencoded" dictionary bodymultipart = "body:multipart-form" dictionary - + bodybinaryfile = "body:binary-file" dictionary + script = scriptreq | scriptres scriptreq = "script:pre-request" st* "{" nl* textblock tagend scriptres = "script:post-response" st* "{" nl* textblock tagend @@ -173,6 +174,19 @@ const multipartExtractContentType = (pair) => { } }; +const binaryFileExtractContentType = (pair) => { + if (_.isString(pair.value)) { + const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/); + if (match != null && match.length > 2) { + pair.value = match[1]; + pair.contentType = match[2]; + } else { + pair.contentType = ''; + } + } +}; + + const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => { const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); @@ -190,6 +204,23 @@ const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) = }); }; +const mapPairListToKeyValPairsBinaryFile = (pairList = [], parseEnabled = true) => { + const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); + + return pairs.map((pair) => { + binaryFileExtractContentType(pair); + + if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) { + let filestr = pair.value.replace(/^@file\(/, '').replace(/\)$/, ''); + pair.type = 'binaryFile'; + pair.value = filestr != '' ? filestr.split('|') : ['']; + } + + return pair; + }); +}; + + const concatArrays = (objValue, srcValue) => { if (_.isArray(objValue) && _.isArray(srcValue)) { return objValue.concat(srcValue); @@ -574,6 +605,13 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, + bodybinaryfile(_1, dictionary) { + return { + body: { + binaryFile: mapPairListToKeyValPairsBinaryFile(dictionary.ast) + } + }; + }, body(_1, _2, _3, _4, textblock, _5) { return { http: { diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 62b31c2f9..c4e2ba323 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -313,6 +313,32 @@ ${indentString(body.sparql)} bru += '\n}\n\n'; } + + if (body && body.binaryFile && body.binaryFile.length) { + bru += `body:binary-file {`; + const binaryFiles = enabled(body.binaryFile).concat(disabled(body.binaryFile)); + + if (binaryFiles.length) { + bru += `\n${indentString( + binaryFiles + .map((item) => { + const enabled = item.enabled ? '' : '~'; + const contentType = + item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : ''; + + if (item.type === 'binaryFile') { + let filestr = item.value[0] || ''; + const value = `@file(${filestr})`; + return `${enabled}${item.name}: ${value}${contentType}`; + } + }) + .join('\n') + )}`; + } + + bru += '\n}\n\n'; + } + if (body && body.graphql && body.graphql.query) { bru += `body:graphql {\n`; bru += `${indentString(body.graphql.query)}`; diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index 1a3efeab7..5f7183f34 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -102,6 +102,11 @@ body:multipart-form { ~message: hello } +body:binary-file { + file: @file(path/to/file.json) @contentType(application/json) + ~file2: @file(path/to/file2.json) @contentType(application/json) +} + body:graphql { { launchesPast { diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index 9c8ed143d..ad7a45495 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -137,6 +137,22 @@ "enabled": false, "type": "text" } + ], + "binaryFile" : [ + { + "name": "file", + "value": ["path/to/file.json"], + "enabled": true, + "type": "binaryFile", + "contentType": "application/json" + }, + { + "name": "file2", + "value": ["path/to/file2.json"], + "enabled": false, + "type": "binaryFile", + "contentType": "application/json" + } ] }, "vars": { diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index b6e044ae4..19d98afbb 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -74,9 +74,21 @@ const multipartFormSchema = Yup.object({ .noUnknown(true) .strict(); + +const binaryFileSchema = Yup.object({ + uid: uidSchema, + type: Yup.string().oneOf(['binaryFile']).required('type is required'), + name: Yup.string().nullable(), + value: Yup.array().of(Yup.string().nullable()).nullable(), + contentType: Yup.string().nullable(), + enabled: Yup.boolean() +}) + .noUnknown(true) + .strict(); + const requestBodySchema = Yup.object({ mode: Yup.string() - .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql']) + .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql', 'binaryFile']) .required('mode is required'), json: Yup.string().nullable(), text: Yup.string().nullable(), @@ -84,7 +96,8 @@ const requestBodySchema = Yup.object({ sparql: Yup.string().nullable(), formUrlEncoded: Yup.array().of(keyValueSchema).nullable(), multipartForm: Yup.array().of(multipartFormSchema).nullable(), - graphql: graphqlBodySchema.nullable() + graphql: graphqlBodySchema.nullable(), + binaryFile: Yup.array().of(binaryFileSchema).nullable() }) .noUnknown(true) .strict(); diff --git a/packages/bruno-tests/collection/binaryFile/binary-file-types.bru b/packages/bruno-tests/collection/binaryFile/binary-file-types.bru new file mode 100644 index 000000000..93275971f --- /dev/null +++ b/packages/bruno-tests/collection/binaryFile/binary-file-types.bru @@ -0,0 +1,27 @@ +meta { + name: binary-files-types + type: http + seq: 1 +} + +post { + url: {{host}}/api/binaryFile/binary-file-types + body: binaryFile + auth: none +} + +body:binary-file { + file1: @file() @contentType() + file2: @file(binaryFile/binary-file.json) @contentType() + file3: @file(binaryFile/binary-file.json) @contentType(application/json) +} + +assert { + res.status: eq 200 + res.body.find(p=>p.name === 'file1').value[0]: isUndefined + res.body.find(p=>p.name === 'file1').contentType: isUndefined + res.body.find(p=>p.name === 'file2').value[0]: eq binaryFile/binary-file.json + res.body.find(p=>p.name === 'file2').contentType: eq isUndefined + res.body.find(p=>p.name === 'file3').value[0]: eq binaryFile/binary-file.json + res.body.find(p=>p.name === 'file3').contentType: eq application/json +} diff --git a/packages/bruno-tests/collection/binaryFile/binary-file.json b/packages/bruno-tests/collection/binaryFile/binary-file.json new file mode 100644 index 000000000..2ff269bff --- /dev/null +++ b/packages/bruno-tests/collection/binaryFile/binary-file.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "bruno-testing", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file From dd239629581e3ec549b528ec90185fc7c55817ef Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <84461672+sanjai0py@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:14:23 +0530 Subject: [PATCH 023/114] improvements (#3930) Co-authored-by: Sanjai Kumar --- packages/bruno-app/package.json | 2 +- .../src/components/FilePickerEditor/index.js | 15 ++- .../components/RequestPane/Binary/index.js | 103 ++++++++---------- .../RequestBody/RequestBodyMode/index.js | 2 +- .../ReduxStore/slices/collections/actions.js | 9 +- .../ReduxStore/slices/collections/index.js | 82 +++++++------- .../bruno-app/src/utils/codegenerator/har.js | 32 ++++-- .../bruno-app/src/utils/collections/index.js | 8 +- .../bruno-app/src/utils/curl/curl-to-json.js | 24 ++-- packages/bruno-electron/src/ipc/collection.js | 9 +- .../bruno-electron/src/ipc/network/index.js | 6 +- .../src/ipc/network/prepare-request.js | 35 +++--- .../bruno-electron/src/utils/collection.js | 2 + packages/bruno-lang/v2/src/bruToJson.js | 21 ++-- packages/bruno-lang/v2/src/jsonToBru.js | 18 ++- .../bruno-lang/v2/tests/fixtures/request.bru | 3 +- .../bruno-lang/v2/tests/fixtures/request.json | 21 ++-- .../bruno-schema/src/collections/index.js | 6 +- .../collection/echo/multiline/echo binary.bru | 15 +++ packages/bruno-tests/src/echo/index.js | 11 ++ packages/bruno-tests/src/index.js | 1 + 21 files changed, 221 insertions(+), 204 deletions(-) create mode 100644 packages/bruno-tests/collection/echo/multiline/echo binary.bru diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index c824f13a8..7203bb816 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -33,7 +33,7 @@ "graphiql": "3.7.1", "graphql": "^16.6.0", "graphql-request": "^3.7.0", - "httpsnippet": "^3.0.6", + "httpsnippet": "^3.0.9", "i18next": "24.1.2", "idb": "^7.0.0", "immer": "^9.0.15", diff --git a/packages/bruno-app/src/components/FilePickerEditor/index.js b/packages/bruno-app/src/components/FilePickerEditor/index.js index d976a3e79..be7d689a3 100644 --- a/packages/bruno-app/src/components/FilePickerEditor/index.js +++ b/packages/bruno-app/src/components/FilePickerEditor/index.js @@ -6,10 +6,9 @@ import { IconX } from '@tabler/icons'; import { isWindowsOS } from 'utils/common/platform'; import slash from 'utils/common/slash'; -const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false}) => { - value = value || []; +const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false }) => { const dispatch = useDispatch(); - const filenames = value + const filenames = (isSingleFilePicker ? [value] : value || []) .filter((v) => v != null && v != '') .map((v) => { const separator = isWindowsOS() ? '\\' : '/'; @@ -20,7 +19,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa const title = filenames.map((v) => `- ${v}`).join('\n'); const browse = () => { - dispatch(browseFiles([],[''])) + dispatch(browseFiles([], [!isSingleFilePicker ? "multiSelections": ""])) .then((filePaths) => { // If file is in the collection's directory, then we use relative path // Otherwise, we use the absolute path @@ -34,7 +33,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa return filePath; }); - onChange(filePaths); + onChange(isSingleFilePicker ? filePaths[0] : filePaths); }) .catch((error) => { console.error(error); @@ -42,7 +41,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa }; const clear = () => { - onChange([]); + onChange(isSingleFilePicker ? '' : []); }; const renderButtonText = (filenames) => { @@ -66,9 +65,9 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
) : ( ); }; -export default FilePickerEditor; +export default FilePickerEditor; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestPane/Binary/index.js b/packages/bruno-app/src/components/RequestPane/Binary/index.js index 77bbda8d5..f4a6eb007 100644 --- a/packages/bruno-app/src/components/RequestPane/Binary/index.js +++ b/packages/bruno-app/src/components/RequestPane/Binary/index.js @@ -1,21 +1,13 @@ -import React from 'react'; -import get from 'lodash/get'; -import cloneDeep from 'lodash/cloneDeep'; +import React, { useState, useEffect } from 'react'; +import { get, cloneDeep, isArray } from 'lodash'; import { IconTrash } from '@tabler/icons'; import { useDispatch } from 'react-redux'; import { useTheme } from 'providers/Theme'; -import { - addBinaryFile, - updateBinaryFile, - deleteBinaryFile -} from 'providers/ReduxStore/slices/collections'; +import { addBinaryFile, updateBinaryFile, deleteBinaryFile } from 'providers/ReduxStore/slices/collections/index'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; -import FilePickerEditor from 'components/FilePickerEditor'; +import FilePickerEditor from 'components/FilePickerEditor/index'; import SingleLineEditor from 'components/SingleLineEditor/index'; -import { isArray } from 'lodash'; -import path from 'node:path'; -import { useState } from 'react'; const Binary = ({ item, collection }) => { const dispatch = useDispatch(); @@ -29,35 +21,28 @@ const Binary = ({ item, collection }) => { addBinaryFile({ itemUid: item.uid, collectionUid: collection.uid, - type: 'binaryFile', - value: [''], }) ); }; const onSave = () => dispatch(saveRequest(item.uid, collection.uid)); const handleRun = () => dispatch(sendRequest(item, collection.uid)); - + const handleParamChange = (e, _param, type) => { - const param = cloneDeep(_param); - switch (type) { - - case 'value': { - param.value = isArray(e.target.value) && e.target.value.length > 0 ? e.target.value : ['']; - param.name = param.value.length === 0 ? '': path.basename(param.value[0], path.extname(param.value[0])); + case 'filePath': { + param.filePath = e.target.filePath; + param.contentType = ""; break; } case 'contentType': { - param.contentType = e.target.value; + param.contentType = e.target.contentType; break; } - case 'enabled': { - param.enabled = e.target.checked; - - setEnableFileUid(param.uid); - + case 'selected': { + param.selected = e.target.selected; + setEnableFileUid(param.uid) break; } } @@ -85,9 +70,15 @@ const Binary = ({ item, collection }) => { - - - + + + @@ -97,26 +88,26 @@ const Binary = ({ item, collection }) => { return ( @@ -170,4 +161,4 @@ const Binary = ({ item, collection }) => { ); }; -export default Binary; +export default Binary; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js index 95b3b6a55..6e5d575c4 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js @@ -135,7 +135,7 @@ const RequestBodyMode = ({ item, collection }) => { onModeChange('binaryFile'); }} > - Binary File + File / Binary
(dispatch, getState) => { export const browseFiles = (filters = [], properties = ['multiSelections']) => - (dispatch, getState) => { + (_dispatch, _getState) => { const { ipcRenderer } = window; return new Promise((resolve, reject) => { - ipcRenderer.invoke('renderer:browse-files', undefined, undefined, undefined, filters, properties).then(resolve).catch(reject); + ipcRenderer + .invoke('renderer:browse-files', filters, properties) + .then(resolve) + .catch(reject); }); - }; +}; export const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => { const state = getState(); 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 bdecc1543..25bf4fa68 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -875,80 +875,82 @@ export const collectionsSlice = createSlice({ } } }, - moveMultipartFormParam: (state, action) => { - // Ensure item.draft is a deep clone of item if not already present - if (!item.draft) { - item.draft = cloneDeep(item); - } - - // Extract payload data - const { updateReorderedItem } = action.payload; - const params = item.draft.request.body.multipartForm; - - item.draft.request.body.multipartForm = updateReorderedItem.map((uid) => { - return params.find((param) => param.uid === uid); - }); - }, - addBinaryFile: (state, action) => { + moveMultipartFormParam: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); if (collection) { const item = findItemInCollection(collection, action.payload.itemUid); + if (item && isItemARequest(item)) { + // Ensure item.draft is a deep clone of item if not already present + if (!item.draft) { + item.draft = cloneDeep(item); + } + + // Extract payload data + const { updateReorderedItem } = action.payload; + const params = item.draft.request.body.multipartForm; + + item.draft.request.body.multipartForm = updateReorderedItem.map((uid) => { + return params.find((param) => param.uid === uid); + }); + } + } + }, + addBinaryFile: (state, action) => { + const collection = findCollectionByUid(state.collections, action.payload.collectionUid); + + if (collection) { + const item = findItemInCollection(collection, action.payload.itemUid); + if (item && isItemARequest(item)) { if (!item.draft) { item.draft = cloneDeep(item); } item.draft.request.body.binaryFile = item.draft.request.body.binaryFile || []; - + item.draft.request.body.binaryFile.push({ uid: uuid(), - type: action.payload.type, - name: '', - value: [''], + filePath: '', contentType: '', - enabled: false + selected: false }); } } }, updateBinaryFile: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); - + if (collection) { const item = findItemInCollection(collection, action.payload.itemUid); - + if (item && isItemARequest(item)) { if (!item.draft) { item.draft = cloneDeep(item); } - - item.draft.request.body.binaryFile = item.draft.request.body.binaryFile.map((p) => { - p.enabled = false; - return p; - }); - + const param = find(item.draft.request.body.binaryFile, (p) => p.uid === action.payload.param.uid); - + if (param) { - - const contentType = mime.contentType(path.extname(action.payload.param.value[0])); - - param.type = action.payload.param.type; - param.name = action.payload.param.name; - param.value = action.payload.param.value; + const contentType = mime.contentType(path.extname(action.payload.param.filePath)); + param.filePath = action.payload.param.filePath; param.contentType = action.payload.param.contentType || contentType || ''; - param.enabled = action.payload.param.enabled; + param.selected = action.payload.param.selected; + + item.draft.request.body.binaryFile = item.draft.request.body.binaryFile.map((p) => { + p.selected = p.uid === param.uid; + return p; + }); } } } }, deleteBinaryFile: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); - + if (collection) { const item = findItemInCollection(collection, action.payload.itemUid); - + if (item && isItemARequest(item)) { if (!item.draft) { item.draft = cloneDeep(item); @@ -958,6 +960,10 @@ export const collectionsSlice = createSlice({ item.draft.request.body.binaryFile, (p) => p.uid !== action.payload.paramUid ); + + if (item.draft.request.body.binaryFile.length > 0) { + item.draft.request.body.binaryFile[0].selected = true; + } } } }, diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index 19e4ea4de..8514588ab 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -65,6 +65,23 @@ const createPostData = (body, type) => { switch (body.mode) { case 'formUrlEncoded': + return { + mimeType: contentType, + text: new URLSearchParams( + body[body.mode] + .filter((param) => param.enabled) + .reduce((acc, param) => { + acc[param.name] = param.value; + return acc; + }, {}) + ).toString(), + params: body[body.mode] + .filter((param) => param.enabled) + .map((param) => ({ + name: param.name, + value: param.value + })) + }; case 'multipartForm': return { mimeType: contentType, @@ -78,18 +95,13 @@ const createPostData = (body, type) => { }; case 'binaryFile': const binary = { - mimeType: 'application/octet-stream', - // mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType, + mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType, params: body[body.mode] - .filter((param) => param.enabled) + .filter((param) => param.selected) .map((param) => ({ - name: param.name, - value: param.value, - fileName: param.value + value: param.filePath, })) }; - - console.log('curl-binary', binary); return binary; default: return { @@ -100,10 +112,6 @@ const createPostData = (body, type) => { }; export const buildHarRequest = ({ request, headers, type }) => { - - console.log('buildHarRequest', request, headers, type); - - console.log('buildHarRequest-postData', createPostData(request.body, type)); return { method: request.method, url: encodeURI(request.url), diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 309c245eb..fa4a2acd4 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -285,11 +285,9 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} return map(params, (param) => { return { uid: param.uid, - type: param.type, - name: param.name, - value: param.value, + filePath: param.filePath, contentType: param.contentType, - enabled: param.enabled + selected: param.selected } }); } @@ -676,7 +674,7 @@ export const humanizeRequestBodyMode = (mode) => { break; } case 'binaryFile': { - label = 'Binary File'; + label = 'File / Binary'; break; } case 'formUrlEncoded': { diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js index 5a2c62022..7683b5cda 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.js @@ -102,30 +102,24 @@ function getFilesString(request) { data.data = {}; - if (request.isDataBinary){ + if (request.isDataBinary) { + let filePath = ''; - let filePath = '' - - if(request.data.startsWith('@')){ + if (request.data.startsWith('@')) { filePath = request.data.slice(1); - }else{ + } else { filePath = request.data; } - const fileName = path.basename(filePath); - data.data = [ { - name: repr(fileName), - value: [repr(filePath)], - enabled: true, + filePath: repr(filePath), contentType: request.headers['Content-Type'], - type: 'binaryFile' + selected: true, } ]; return data; - } data.files = {}; @@ -190,13 +184,11 @@ const curlToJson = (curlCommand) => { if (request.query) { requestJson.queries = getQueries(request); - } - - else if (request.multipartUploads || request.isDataBinary) { + } else if (request.multipartUploads || request.isDataBinary) { Object.assign(requestJson, getFilesString(request)); } else if (typeof request.data === 'string' || typeof request.data === 'number') { Object.assign(requestJson, getDataString(request)); - } + } if (request.insecure) { requestJson.insecure = false; diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index c1a05a49a..07f15926f 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -62,14 +62,11 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection }); // browse directory for file - ipcMain.handle('renderer:browse-files', async (event, pathname, request, filters, properties) => { + ipcMain.handle('renderer:browse-files', async (_, filters, properties) => { try { - - const filePaths = await browseFiles(mainWindow, filters, properties); - - return filePaths; + return await browseFiles(mainWindow, filters, properties); } catch (error) { - return Promise.reject(error); + throw error; } }); diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index dbc36d846..baa9f098b 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -576,18 +576,14 @@ const registerNetworkIpc = (mainWindow) => { }); const abortController = new AbortController(); - - const collectionRoot = get(collection, 'root', {}); const request = await prepareRequest(item, collection, abortController); request.__bruno__executionMode = 'standalone'; - const brunoConfig = getBrunoConfig(collectionUid); const scriptingConfig = get(brunoConfig, 'scripts', {}); scriptingConfig.runtime = getJsSandboxRuntime(collection); try { request.signal = abortController.signal; - saveCancelToken(cancelTokenUid, abortController); await runPreRequest( @@ -618,7 +614,7 @@ const registerNetworkIpc = (mainWindow) => { url: request.url, method: request.method, headers: request.headers, - data: request.mode == 'binaryFile'? undefined: safeParseJSON(safeStringifyJSON(request.data)) , + data: request.mode == 'binaryFile'? "": safeParseJSON(safeStringifyJSON(request.data)) , timestamp: Date.now() }, collectionUid, diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 297033c6d..70d8017d1 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -1,4 +1,4 @@ -const { get, each, filter } = require('lodash'); +const { get, each, filter, find } = require('lodash'); const decomment = require('decomment'); const crypto = require('node:crypto'); const fs = require('node:fs/promises'); @@ -254,30 +254,25 @@ const prepareRequest = async (item, collection, abortController) => { } if (request.body.mode === 'binaryFile') { - if (!contentTypeDefined) { - axiosRequest.headers['content-type'] = 'application/octet-stream'; + axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads } - - if (request.body.binaryFile && request.body.binaryFile.length > 0) { - - axiosRequest.headers['content-type'] = request.body.binaryFile[0].contentType; - - let filePath = request.body.binaryFile[0].value[0]; - - if (filePath && filePath !== '') { - + + const binaryFile = find(request.body.binaryFile, (param) => param.selected); + if (binaryFile) { + let { filePath, contentType } = binaryFile; + + axiosRequest.headers['content-type'] = contentType; + if (filePath) { if (!path.isAbsolute(filePath)) { - filePath = path.join(collectionPath, filePath); } - - const file = await fs.readFile(filePath, abortController) - - axiosRequest.data = file - - if(axiosRequest.headers['content-type'].includes('application/json')) { - axiosRequest.data = JSON.parse(file) + + try { + const fileContent = await fs.readFile(filePath); + axiosRequest.data = fileContent; + } catch (error) { + console.error('Error reading file:', error); } } } diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index 96e75acae..46624677c 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -240,6 +240,7 @@ const hydrateRequestWithUuid = (request, pathname) => { const assertions = get(request, 'request.assertions', []); const bodyFormUrlEncoded = get(request, 'request.body.formUrlEncoded', []); const bodyMultipartForm = get(request, 'request.body.multipartForm', []); + const binaryFile = get(request, 'request.body.binaryFile', []); params.forEach((param) => (param.uid = uuid())); headers.forEach((header) => (header.uid = uuid())); @@ -248,6 +249,7 @@ const hydrateRequestWithUuid = (request, pathname) => { assertions.forEach((assertion) => (assertion.uid = uuid())); bodyFormUrlEncoded.forEach((param) => (param.uid = uuid())); bodyMultipartForm.forEach((param) => (param.uid = uuid())); + binaryFile.forEach((param) => (param.uid = uuid())); return request; }; diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index e7b0c5772..3b228036f 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -177,9 +177,9 @@ const multipartExtractContentType = (pair) => { const binaryFileExtractContentType = (pair) => { if (_.isString(pair.value)) { const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/); - if (match != null && match.length > 2) { - pair.value = match[1]; - pair.contentType = match[2]; + if (match && match.length > 2) { + pair.value = match[1].trim(); + pair.contentType = match[2].trim(); } else { pair.contentType = ''; } @@ -206,21 +206,25 @@ const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) = const mapPairListToKeyValPairsBinaryFile = (pairList = [], parseEnabled = true) => { const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); - return pairs.map((pair) => { binaryFileExtractContentType(pair); if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) { - let filestr = pair.value.replace(/^@file\(/, '').replace(/\)$/, ''); - pair.type = 'binaryFile'; - pair.value = filestr != '' ? filestr.split('|') : ['']; + let filePath = pair.value.replace(/^@file\(/, '').replace(/\)$/, ''); + pair.filePath = filePath; + pair.selected = pair.enabled + + // Remove pair.value as it only contains the file path reference + delete pair.value; + // Remove pair.name as it is auto-generated (e.g., file1, file2, file3, etc.) + delete pair.name; + delete pair.enabled; } return pair; }); }; - const concatArrays = (objValue, srcValue) => { if (_.isArray(objValue) && _.isArray(srcValue)) { return objValue.concat(srcValue); @@ -746,3 +750,4 @@ const parser = (input) => { }; module.exports = parser; + \ No newline at end of file diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index c4e2ba323..164ea6a35 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -2,8 +2,8 @@ const _ = require('lodash'); const { indentString } = require('../../v1/src/utils'); -const enabled = (items = []) => items.filter((item) => item.enabled); -const disabled = (items = []) => items.filter((item) => !item.enabled); +const enabled = (items = [], key = "enabled") => items.filter((item) => item[key]); +const disabled = (items = [], key = "enabled") => items.filter((item) => !item[key]); // remove the last line if two new lines are found const stripLastLine = (text) => { @@ -316,21 +316,19 @@ ${indentString(body.sparql)} if (body && body.binaryFile && body.binaryFile.length) { bru += `body:binary-file {`; - const binaryFiles = enabled(body.binaryFile).concat(disabled(body.binaryFile)); + const binaryFiles = enabled(body.binaryFile, "selected").concat(disabled(body.binaryFile, "selected")); if (binaryFiles.length) { bru += `\n${indentString( binaryFiles .map((item) => { - const enabled = item.enabled ? '' : '~'; + const selected = item.selected ? '' : '~'; const contentType = item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : ''; - - if (item.type === 'binaryFile') { - let filestr = item.value[0] || ''; - const value = `@file(${filestr})`; - return `${enabled}${item.name}: ${value}${contentType}`; - } + const filePath = item.filePath || ''; + const value = `@file(${filePath})`; + const itemName = "file"; + return `${selected}${itemName}: ${value}${contentType}`; }) .join('\n') )}`; diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index 5f7183f34..59f37ac89 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -104,7 +104,8 @@ body:multipart-form { body:binary-file { file: @file(path/to/file.json) @contentType(application/json) - ~file2: @file(path/to/file2.json) @contentType(application/json) + file: @file(path/to/file.json) @contentType(application/json) + ~file: @file(path/to/file2.json) @contentType(application/json) } body:graphql { diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index ad7a45495..166040509 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -140,18 +140,19 @@ ], "binaryFile" : [ { - "name": "file", - "value": ["path/to/file.json"], - "enabled": true, - "type": "binaryFile", - "contentType": "application/json" + "filePath": "path/to/file.json", + "contentType": "application/json", + "selected": true }, { - "name": "file2", - "value": ["path/to/file2.json"], - "enabled": false, - "type": "binaryFile", - "contentType": "application/json" + "filePath": "path/to/file.json", + "contentType": "application/json", + "selected": true + }, + { + "filePath": "path/to/file2.json", + "contentType": "application/json", + "selected": false } ] }, diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 19d98afbb..fdf88b38c 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -77,11 +77,9 @@ const multipartFormSchema = Yup.object({ const binaryFileSchema = Yup.object({ uid: uidSchema, - type: Yup.string().oneOf(['binaryFile']).required('type is required'), - name: Yup.string().nullable(), - value: Yup.array().of(Yup.string().nullable()).nullable(), + filePath: Yup.string().nullable(), contentType: Yup.string().nullable(), - enabled: Yup.boolean() + selected: Yup.boolean() }) .noUnknown(true) .strict(); diff --git a/packages/bruno-tests/collection/echo/multiline/echo binary.bru b/packages/bruno-tests/collection/echo/multiline/echo binary.bru new file mode 100644 index 000000000..d11b30413 --- /dev/null +++ b/packages/bruno-tests/collection/echo/multiline/echo binary.bru @@ -0,0 +1,15 @@ +meta { + name: echo binary + type: http + seq: 1 +} + +post { + url: {{echo-host}} + body: binaryFile + auth: none +} + +body:binary-file { + file: @file(bruno.png) @contentType(image/png) +} diff --git a/packages/bruno-tests/src/echo/index.js b/packages/bruno-tests/src/echo/index.js index ba9b403ae..89a9208e0 100644 --- a/packages/bruno-tests/src/echo/index.js +++ b/packages/bruno-tests/src/echo/index.js @@ -19,6 +19,17 @@ router.post('/xml-raw', (req, res) => { return res.send(req.rawBody); }); +router.post('/bin', (req, res) => { + const rawBody = req.body; + + if (!rawBody || rawBody.length === 0) { + return res.status(400).send('No data received'); + } + + res.set('Content-Type', req.headers['content-type'] || 'application/octet-stream'); + res.send(rawBody); +}); + router.get('/bom-json-test', (req, res) => { const jsonData = { message: 'Hello!', diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js index a09cb434b..a482fc128 100644 --- a/packages/bruno-tests/src/index.js +++ b/packages/bruno-tests/src/index.js @@ -10,6 +10,7 @@ const multipartRouter = require('./multipart'); const app = new express(); const port = process.env.PORT || 8080; +app.use(express.raw({type: '*/*', limit: '100mb'})); app.use(cors()); app.use(xmlParser()); app.use(bodyParser.text()); From af182a9c0035be957b800a6bc459064eb758d120 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar Date: Tue, 4 Feb 2025 17:41:24 +0530 Subject: [PATCH 024/114] refactor: rename binaryFile to file and update related references --- .../{Binary => FileBody}/StyledWrapper.js | 0 .../RequestPane/{Binary => FileBody}/index.js | 14 ++--- .../RequestBody/RequestBodyMode/index.js | 2 +- .../RequestPane/RequestBody/index.js | 8 +-- .../ReduxStore/slices/collections/actions.js | 2 +- .../ReduxStore/slices/collections/index.js | 32 +++++------ .../bruno-app/src/utils/codegenerator/har.js | 7 +-- .../bruno-app/src/utils/collections/export.js | 2 +- .../bruno-app/src/utils/collections/index.js | 12 ++-- .../src/utils/curl/curl-to-json.spec.js | 2 +- packages/bruno-app/src/utils/curl/index.js | 6 +- .../bruno-app/src/utils/importers/common.js | 2 +- .../bruno-electron/src/ipc/network/index.js | 6 +- .../src/ipc/network/prepare-request.js | 8 +-- .../bruno-electron/src/utils/collection.js | 4 +- .../bruno-electron/src/utils/filesystem.js | 16 ++---- packages/bruno-lang/v2/src/bruToJson.js | 14 ++--- packages/bruno-lang/v2/src/jsonToBru.js | 10 ++-- .../bruno-lang/v2/tests/fixtures/request.bru | 2 +- .../bruno-lang/v2/tests/fixtures/request.json | 2 +- .../bruno-schema/src/collections/index.js | 6 +- .../binaryFile/binary-file-types.bru | 27 --------- .../collection/binaryFile/binary-file.json | 9 --- .../echo/echo file body/echo file body.bru | 40 ++++++++++++++ .../echo file body/echo image file body.bru | 22 ++++++++ .../echo file body/echo json file body.bru | 55 +++++++++++++++++++ .../echo file body/echo text file body.bru | 30 ++++++++++ .../collection/echo/multiline/echo binary.bru | 4 +- packages/bruno-tests/collection/file.txt | 3 + 29 files changed, 226 insertions(+), 121 deletions(-) rename packages/bruno-app/src/components/RequestPane/{Binary => FileBody}/StyledWrapper.js (100%) rename packages/bruno-app/src/components/RequestPane/{Binary => FileBody}/index.js (93%) delete mode 100644 packages/bruno-tests/collection/binaryFile/binary-file-types.bru delete mode 100644 packages/bruno-tests/collection/binaryFile/binary-file.json create mode 100644 packages/bruno-tests/collection/echo/echo file body/echo file body.bru create mode 100644 packages/bruno-tests/collection/echo/echo file body/echo image file body.bru create mode 100644 packages/bruno-tests/collection/echo/echo file body/echo json file body.bru create mode 100644 packages/bruno-tests/collection/echo/echo file body/echo text file body.bru create mode 100644 packages/bruno-tests/collection/file.txt diff --git a/packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/FileBody/StyledWrapper.js similarity index 100% rename from packages/bruno-app/src/components/RequestPane/Binary/StyledWrapper.js rename to packages/bruno-app/src/components/RequestPane/FileBody/StyledWrapper.js diff --git a/packages/bruno-app/src/components/RequestPane/Binary/index.js b/packages/bruno-app/src/components/RequestPane/FileBody/index.js similarity index 93% rename from packages/bruno-app/src/components/RequestPane/Binary/index.js rename to packages/bruno-app/src/components/RequestPane/FileBody/index.js index f4a6eb007..d97953aa5 100644 --- a/packages/bruno-app/src/components/RequestPane/Binary/index.js +++ b/packages/bruno-app/src/components/RequestPane/FileBody/index.js @@ -3,22 +3,22 @@ import { get, cloneDeep, isArray } from 'lodash'; import { IconTrash } from '@tabler/icons'; import { useDispatch } from 'react-redux'; import { useTheme } from 'providers/Theme'; -import { addBinaryFile, updateBinaryFile, deleteBinaryFile } from 'providers/ReduxStore/slices/collections/index'; +import { addFile as _addFile, updateFile, deleteFile } from 'providers/ReduxStore/slices/collections/index'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; import FilePickerEditor from 'components/FilePickerEditor/index'; import SingleLineEditor from 'components/SingleLineEditor/index'; -const Binary = ({ item, collection }) => { +const FileBody = ({ item, collection }) => { const dispatch = useDispatch(); const { storedTheme } = useTheme(); - const params = item.draft ? get(item, 'draft.request.body.binaryFile') : get(item, 'request.body.binaryFile'); + const params = item.draft ? get(item, 'draft.request.body.file') : get(item, 'request.body.file'); const [enabledFileUid, setEnableFileUid] = useState(params && params.length ? params[0].uid : ''); const addFile = () => { dispatch( - addBinaryFile({ + _addFile({ itemUid: item.uid, collectionUid: collection.uid, }) @@ -47,7 +47,7 @@ const Binary = ({ item, collection }) => { } } dispatch( - updateBinaryFile({ + updateFile({ param: param, itemUid: item.uid, collectionUid: collection.uid @@ -57,7 +57,7 @@ const Binary = ({ item, collection }) => { const handleRemoveParams = (param) => { dispatch( - deleteBinaryFile({ + deleteFile({ paramUid: param.uid, itemUid: item.uid, collectionUid: collection.uid @@ -161,4 +161,4 @@ const Binary = ({ item, collection }) => { ); }; -export default Binary; \ No newline at end of file +export default FileBody; \ No newline at end of file diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js index 6e5d575c4..db73597df 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js @@ -132,7 +132,7 @@ const RequestBodyMode = ({ item, collection }) => { className="dropdown-item" onClick={() => { dropdownTippyRef.current.hide(); - onModeChange('binaryFile'); + onModeChange('file'); }} > File / Binary diff --git a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js index 9a71a4ac3..8f7fa8465 100644 --- a/packages/bruno-app/src/components/RequestPane/RequestBody/index.js +++ b/packages/bruno-app/src/components/RequestPane/RequestBody/index.js @@ -8,7 +8,7 @@ import { useTheme } from 'providers/Theme'; import { updateRequestBody } from 'providers/ReduxStore/slices/collections'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import StyledWrapper from './StyledWrapper'; -import Binary from '../Binary/index'; +import FileBody from '../FileBody/index'; const RequestBody = ({ item, collection }) => { const dispatch = useDispatch(); @@ -63,8 +63,8 @@ const RequestBody = ({ item, collection }) => { ); } - if (bodyMode === 'binaryFile') { - return + if (bodyMode === 'file') { + return } if (bodyMode === 'formUrlEncoded') { @@ -77,4 +77,4 @@ const RequestBody = ({ item, collection }) => { return No Body; }; -export default RequestBody; +export default RequestBody; \ No newline at end of file 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 880536019..d251112c7 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -759,7 +759,7 @@ export const newHttpRequest = (params) => (dispatch, getState) => { sparql: null, multipartForm: null, formUrlEncoded: null, - binaryFile: null + file: null }, auth: auth ?? { mode: 'none' 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 25bf4fa68..3ae0fa4e5 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -897,7 +897,7 @@ export const collectionsSlice = createSlice({ } } }, - addBinaryFile: (state, action) => { + addFile: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); if (collection) { @@ -907,9 +907,9 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - item.draft.request.body.binaryFile = item.draft.request.body.binaryFile || []; + item.draft.request.body.file = item.draft.request.body.file || []; - item.draft.request.body.binaryFile.push({ + item.draft.request.body.file.push({ uid: uuid(), filePath: '', contentType: '', @@ -918,7 +918,7 @@ export const collectionsSlice = createSlice({ } } }, - updateBinaryFile: (state, action) => { + updateFile: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); if (collection) { @@ -929,7 +929,7 @@ export const collectionsSlice = createSlice({ item.draft = cloneDeep(item); } - const param = find(item.draft.request.body.binaryFile, (p) => p.uid === action.payload.param.uid); + const param = find(item.draft.request.body.file, (p) => p.uid === action.payload.param.uid); if (param) { const contentType = mime.contentType(path.extname(action.payload.param.filePath)); @@ -937,7 +937,7 @@ export const collectionsSlice = createSlice({ param.contentType = action.payload.param.contentType || contentType || ''; param.selected = action.payload.param.selected; - item.draft.request.body.binaryFile = item.draft.request.body.binaryFile.map((p) => { + item.draft.request.body.file = item.draft.request.body.file.map((p) => { p.selected = p.uid === param.uid; return p; }); @@ -945,7 +945,7 @@ export const collectionsSlice = createSlice({ } } }, - deleteBinaryFile: (state, action) => { + deleteFile: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); if (collection) { @@ -956,13 +956,13 @@ export const collectionsSlice = createSlice({ item.draft = cloneDeep(item); } - item.draft.request.body.binaryFile = filter( - item.draft.request.body.binaryFile, + item.draft.request.body.file = filter( + item.draft.request.body.file, (p) => p.uid !== action.payload.paramUid ); - if (item.draft.request.body.binaryFile.length > 0) { - item.draft.request.body.binaryFile[0].selected = true; + if (item.draft.request.body.file.length > 0) { + item.draft.request.body.file[0].selected = true; } } } @@ -1023,8 +1023,8 @@ export const collectionsSlice = createSlice({ item.draft.request.body.sparql = action.payload.content; break; } - case 'binaryFile': { - item.draft.request.body.binaryFile = action.payload.content; + case 'file': { + item.draft.request.body.file = action.payload.content; break; } case 'formUrlEncoded': { @@ -2028,9 +2028,9 @@ export const { addMultipartFormParam, updateMultipartFormParam, deleteMultipartFormParam, - addBinaryFile, - updateBinaryFile, - deleteBinaryFile, + addFile, + updateFile, + deleteFile, moveMultipartFormParam, updateRequestAuthMode, updateRequestBodyMode, diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index 8514588ab..110f82db5 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -14,7 +14,7 @@ const createContentType = (mode) => { return 'application/json'; case 'multipartForm': return 'multipart/form-data'; - case 'binaryFile': + case 'file': return 'application/octet-stream'; default: return ''; @@ -93,8 +93,8 @@ const createPostData = (body, type) => { ...(param.type === 'file' && { fileName: param.value }) })) }; - case 'binaryFile': - const binary = { + case 'file': + return { mimeType: body[body.mode].filter((param) => param.enabled)[0].contentType, params: body[body.mode] .filter((param) => param.selected) @@ -102,7 +102,6 @@ const createPostData = (body, type) => { value: param.filePath, })) }; - return binary; default: return { mimeType: contentType, diff --git a/packages/bruno-app/src/utils/collections/export.js b/packages/bruno-app/src/utils/collections/export.js index b7ca9f5ba..3d15fdd07 100644 --- a/packages/bruno-app/src/utils/collections/export.js +++ b/packages/bruno-app/src/utils/collections/export.js @@ -14,7 +14,7 @@ export const deleteUidsInItems = (items) => { each(get(item, 'request.vars.assertions'), (a) => delete a.uid); each(get(item, 'request.body.multipartForm'), (param) => delete param.uid); each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid); - each(get(item, 'request.body.binaryFile'), (param) => delete param.uid); + each(get(item, 'request.body.file'), (param) => delete param.uid); } if (item.items && item.items.length) { diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index fa4a2acd4..eb53cfb48 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -281,7 +281,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} }); }; - const copyBinaryFileParams = (params = []) => { + const copyFileParams = (params = []) => { return map(params, (param) => { return { uid: param.uid, @@ -320,7 +320,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {} sparql: si.request.body.sparql, formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded), multipartForm: copyMultipartFormParams(si.request.body.multipartForm), - binaryFile: copyBinaryFileParams(si.request.body.binaryFile) + file: copyFileParams(si.request.body.file) }, script: si.request.script, vars: si.request.vars, @@ -673,7 +673,7 @@ export const humanizeRequestBodyMode = (mode) => { label = 'SPARQL'; break; } - case 'binaryFile': { + case 'file': { label = 'File / Binary'; break; } @@ -777,7 +777,7 @@ export const refreshUidsInItem = (item) => { each(get(item, 'request.params'), (param) => (param.uid = uuid())); each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid())); each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid())); - each(get(item, 'request.body.binaryFile'), (param) => (param.uid = uuid())); + each(get(item, 'request.body.file'), (param) => (param.uid = uuid())); return item; }; @@ -788,13 +788,13 @@ export const deleteUidsInItem = (item) => { const headers = get(item, 'request.headers', []); const bodyFormUrlEncoded = get(item, 'request.body.formUrlEncoded', []); const bodyMultipartForm = get(item, 'request.body.multipartForm', []); - const binaryFile = get(item, 'request.body.binaryFile', []); + const file = get(item, 'request.body.file', []); params.forEach((param) => delete param.uid); headers.forEach((header) => delete header.uid); bodyFormUrlEncoded.forEach((param) => delete param.uid); bodyMultipartForm.forEach((param) => delete param.uid); - binaryFile.forEach((param) => delete param.uid); + file.forEach((param) => delete param.uid); return item; }; diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js index 6f8206139..4c3194c2e 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js @@ -117,7 +117,7 @@ describe('curlToJson', () => { value: ['/path/to/file'], enabled: true, contentType: 'application/json;charset=utf-8', - type: 'binaryFile' + type: 'file' } ] }); diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js index d91588178..ad4f1edf6 100644 --- a/packages/bruno-app/src/utils/curl/index.js +++ b/packages/bruno-app/src/utils/curl/index.js @@ -51,7 +51,7 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque multipartForm: null, formUrlEncoded: null, graphql: null, - binaryFile: null + file: null }; if (parsedBody && contentType && typeof contentType === 'string') { @@ -59,8 +59,8 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque body.mode = 'graphql'; body.graphql = parseGraphQL(parsedBody); } else if (requestType === 'http-request' && request.isDataBinary) { - body.mode = 'binaryFile'; - body.binaryFile = parsedBody; + body.mode = 'file'; + body.file = parsedBody; }else if (contentType.includes('application/json')) { body.mode = 'json'; body.json = convertToCodeMirrorJson(parsedBody); diff --git a/packages/bruno-app/src/utils/importers/common.js b/packages/bruno-app/src/utils/importers/common.js index af187cc82..9d370a455 100644 --- a/packages/bruno-app/src/utils/importers/common.js +++ b/packages/bruno-app/src/utils/importers/common.js @@ -35,7 +35,7 @@ export const updateUidsInCollection = (_collection) => { each(get(item, 'request.assertions'), (a) => (a.uid = uuid())); each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid())); each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid())); - each(get(item, 'request.body.binaryFile'), (param) => (param.uid = uuid())); + each(get(item, 'request.body.file'), (param) => (param.uid = uuid())); if (item.items && item.items.length) { updateItemUids(item.items); diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index baa9f098b..427b3ab65 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -28,7 +28,7 @@ const { makeAxiosInstance } = require('./axios-instance'); const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper'); const { addDigestInterceptor } = require('./digestauth-helper'); const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util'); -const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); +const { chooseFileToSave, writeFile } = require('../../utils/filesystem'); const { getCookieStringForUrl, addCookieToJar, getDomainsWithCookies } = require('../../utils/cookies'); const { resolveOAuth2AuthorizationCodeAccessToken, @@ -614,7 +614,7 @@ const registerNetworkIpc = (mainWindow) => { url: request.url, method: request.method, headers: request.headers, - data: request.mode == 'binaryFile'? "": safeParseJSON(safeStringifyJSON(request.data)) , + data: request.mode == 'file'? "": safeParseJSON(safeStringifyJSON(request.data)) , timestamp: Date.now() }, collectionUid, @@ -1371,7 +1371,7 @@ const registerNetworkIpc = (mainWindow) => { if (encoding === 'utf-8') { await writeFile(filePath, data); } else { - await writeBinaryFile(filePath, data); + await writeFile(filePath, data, true); } } } catch (error) { diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 70d8017d1..705d19f22 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -253,14 +253,14 @@ const prepareRequest = async (item, collection, abortController) => { axiosRequest.data = request.body.sparql; } - if (request.body.mode === 'binaryFile') { + if (request.body.mode === 'file') { if (!contentTypeDefined) { axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads } - const binaryFile = find(request.body.binaryFile, (param) => param.selected); - if (binaryFile) { - let { filePath, contentType } = binaryFile; + const bodyFile = find(request.body.file, (param) => param.selected); + if (bodyFile) { + let { filePath, contentType } = bodyFile; axiosRequest.headers['content-type'] = contentType; if (filePath) { diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index 46624677c..bb8d17e97 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -240,7 +240,7 @@ const hydrateRequestWithUuid = (request, pathname) => { const assertions = get(request, 'request.assertions', []); const bodyFormUrlEncoded = get(request, 'request.body.formUrlEncoded', []); const bodyMultipartForm = get(request, 'request.body.multipartForm', []); - const binaryFile = get(request, 'request.body.binaryFile', []); + const file = get(request, 'request.body.file', []); params.forEach((param) => (param.uid = uuid())); headers.forEach((header) => (header.uid = uuid())); @@ -249,7 +249,7 @@ const hydrateRequestWithUuid = (request, pathname) => { assertions.forEach((assertion) => (assertion.uid = uuid())); bodyFormUrlEncoded.forEach((param) => (param.uid = uuid())); bodyMultipartForm.forEach((param) => (param.uid = uuid())); - binaryFile.forEach((param) => (param.uid = uuid())); + file.forEach((param) => (param.uid = uuid())); return request; }; diff --git a/packages/bruno-electron/src/utils/filesystem.js b/packages/bruno-electron/src/utils/filesystem.js index d37742be4..16a07b6af 100644 --- a/packages/bruno-electron/src/utils/filesystem.js +++ b/packages/bruno-electron/src/utils/filesystem.js @@ -68,20 +68,13 @@ function normalizeWslPath(pathname) { return pathname.replace(/^\/wsl.localhost/, '\\\\wsl.localhost').replace(/\//g, '\\'); } -const writeFile = async (pathname, content) => { +const writeFile = async (pathname, content, isBinary = false) => { try { - fs.writeFileSync(pathname, content, { - encoding: 'utf8' + await fs.writeFile(pathname, content, { + encoding: !isBinary ? "utf-8" : null }); } catch (err) { - return Promise.reject(err); - } -}; - -const writeBinaryFile = async (pathname, content) => { - try { - fs.writeFileSync(pathname, content); - } catch (err) { + console.error(`Error writing file at ${pathname}:`, err); return Promise.reject(err); } }; @@ -265,7 +258,6 @@ module.exports = { isWSLPath, normalizeWslPath, writeFile, - writeBinaryFile, hasJsonExtension, hasBruExtension, createDirectory, diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 3b228036f..26a670681 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -25,7 +25,7 @@ const grammar = ohm.grammar(`Bru { BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)* auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body - bodyforms = bodyformurlencoded | bodymultipart | bodybinaryfile + bodyforms = bodyformurlencoded | bodymultipart | bodyfile params = paramspath | paramsquery nl = "\\r"? "\\n" @@ -102,7 +102,7 @@ const grammar = ohm.grammar(`Bru { bodyformurlencoded = "body:form-urlencoded" dictionary bodymultipart = "body:multipart-form" dictionary - bodybinaryfile = "body:binary-file" dictionary + bodyfile = "body:file" dictionary script = scriptreq | scriptres scriptreq = "script:pre-request" st* "{" nl* textblock tagend @@ -174,7 +174,7 @@ const multipartExtractContentType = (pair) => { } }; -const binaryFileExtractContentType = (pair) => { +const fileExtractContentType = (pair) => { if (_.isString(pair.value)) { const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/); if (match && match.length > 2) { @@ -204,10 +204,10 @@ const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) = }); }; -const mapPairListToKeyValPairsBinaryFile = (pairList = [], parseEnabled = true) => { +const mapPairListToKeyValPairsFile = (pairList = [], parseEnabled = true) => { const pairs = mapPairListToKeyValPairs(pairList, parseEnabled); return pairs.map((pair) => { - binaryFileExtractContentType(pair); + fileExtractContentType(pair); if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) { let filePath = pair.value.replace(/^@file\(/, '').replace(/\)$/, ''); @@ -609,10 +609,10 @@ const sem = grammar.createSemantics().addAttribute('ast', { } }; }, - bodybinaryfile(_1, dictionary) { + bodyfile(_1, dictionary) { return { body: { - binaryFile: mapPairListToKeyValPairsBinaryFile(dictionary.ast) + file: mapPairListToKeyValPairsFile(dictionary.ast) } }; }, diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 164ea6a35..0d0b1d9c2 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -314,13 +314,13 @@ ${indentString(body.sparql)} } - if (body && body.binaryFile && body.binaryFile.length) { - bru += `body:binary-file {`; - const binaryFiles = enabled(body.binaryFile, "selected").concat(disabled(body.binaryFile, "selected")); + if (body && body.file && body.file.length) { + bru += `body:file {`; + const files = enabled(body.file, "selected").concat(disabled(body.file, "selected")); - if (binaryFiles.length) { + if (files.length) { bru += `\n${indentString( - binaryFiles + files .map((item) => { const selected = item.selected ? '' : '~'; const contentType = diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index 59f37ac89..ad66c64e8 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -102,7 +102,7 @@ body:multipart-form { ~message: hello } -body:binary-file { +body:file { file: @file(path/to/file.json) @contentType(application/json) file: @file(path/to/file.json) @contentType(application/json) ~file: @file(path/to/file2.json) @contentType(application/json) diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index 166040509..1cfe98809 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -138,7 +138,7 @@ "type": "text" } ], - "binaryFile" : [ + "file" : [ { "filePath": "path/to/file.json", "contentType": "application/json", diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index fdf88b38c..e0d05d167 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -75,7 +75,7 @@ const multipartFormSchema = Yup.object({ .strict(); -const binaryFileSchema = Yup.object({ +const fileSchema = Yup.object({ uid: uidSchema, filePath: Yup.string().nullable(), contentType: Yup.string().nullable(), @@ -86,7 +86,7 @@ const binaryFileSchema = Yup.object({ const requestBodySchema = Yup.object({ mode: Yup.string() - .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql', 'binaryFile']) + .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql', 'file']) .required('mode is required'), json: Yup.string().nullable(), text: Yup.string().nullable(), @@ -95,7 +95,7 @@ const requestBodySchema = Yup.object({ formUrlEncoded: Yup.array().of(keyValueSchema).nullable(), multipartForm: Yup.array().of(multipartFormSchema).nullable(), graphql: graphqlBodySchema.nullable(), - binaryFile: Yup.array().of(binaryFileSchema).nullable() + file: Yup.array().of(fileSchema).nullable() }) .noUnknown(true) .strict(); diff --git a/packages/bruno-tests/collection/binaryFile/binary-file-types.bru b/packages/bruno-tests/collection/binaryFile/binary-file-types.bru deleted file mode 100644 index 93275971f..000000000 --- a/packages/bruno-tests/collection/binaryFile/binary-file-types.bru +++ /dev/null @@ -1,27 +0,0 @@ -meta { - name: binary-files-types - type: http - seq: 1 -} - -post { - url: {{host}}/api/binaryFile/binary-file-types - body: binaryFile - auth: none -} - -body:binary-file { - file1: @file() @contentType() - file2: @file(binaryFile/binary-file.json) @contentType() - file3: @file(binaryFile/binary-file.json) @contentType(application/json) -} - -assert { - res.status: eq 200 - res.body.find(p=>p.name === 'file1').value[0]: isUndefined - res.body.find(p=>p.name === 'file1').contentType: isUndefined - res.body.find(p=>p.name === 'file2').value[0]: eq binaryFile/binary-file.json - res.body.find(p=>p.name === 'file2').contentType: eq isUndefined - res.body.find(p=>p.name === 'file3').value[0]: eq binaryFile/binary-file.json - res.body.find(p=>p.name === 'file3').contentType: eq application/json -} diff --git a/packages/bruno-tests/collection/binaryFile/binary-file.json b/packages/bruno-tests/collection/binaryFile/binary-file.json deleted file mode 100644 index 2ff269bff..000000000 --- a/packages/bruno-tests/collection/binaryFile/binary-file.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "version": "1", - "name": "bruno-testing", - "type": "collection", - "ignore": [ - "node_modules", - ".git" - ] -} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/echo file body/echo file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo file body.bru new file mode 100644 index 000000000..b820e4808 --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo file body/echo file body.bru @@ -0,0 +1,40 @@ +meta { + name: echo file body + type: http + seq: 4 +} + +post { + url: {{echo-host}} + body: file + auth: none +} + +body:file { + file: @file(ping.bru) @contentType(bru) +} + +tests { + test("should return bru file body contents", function() { + const data = res.getBody(); + const expectedData = `meta { + name: ping + type: http + seq: 1 + } + + get { + url: {{host}}/ping + body: none + auth: none + } + `; + expect(res.getBody()).to.eql(expectedData); + }); + + + test("should return proper header", function() { + const contentType = res.getHeader('content-type'); + expect(contentType).to.eql('bru'); + }); +} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/echo file body/echo image file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo image file body.bru new file mode 100644 index 000000000..da1899a96 --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo file body/echo image file body.bru @@ -0,0 +1,22 @@ +meta { + name: echo image body + type: http + seq: 1 +} + +post { + url: {{echo-host}} + body: file + auth: none +} + +body:file { + file: @file(bruno.png) @contentType(image/png) +} + +tests { + test("should return proper header", function() { + const contentType = res.getHeader('content-type'); + expect(contentType).to.eql('image/png'); + }); +} diff --git a/packages/bruno-tests/collection/echo/echo file body/echo json file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo json file body.bru new file mode 100644 index 000000000..f2173a4b9 --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo file body/echo json file body.bru @@ -0,0 +1,55 @@ +meta { + name: echo json file body + type: http + seq: 2 +} + +post { + url: {{echo-host}} + body: file + auth: none +} + +body:file { + file: @file(file.json) @contentType(application/json; charset=utf-8) +} + +tests { + test("should return json file body contents buffer", function() { + const data = res.getBody(); + expect(res.getBody()).to.eql({ + "type": "Buffer", + "data": [ + 123, + 10, + 32, + 32, + 34, + 104, + 101, + 108, + 108, + 111, + 34, + 58, + 32, + 34, + 98, + 114, + 117, + 110, + 111, + 34, + 10, + 125, + 10 + ] + }); + }); + + test("should return proper header", function() { + const contentType = res.getHeader('content-type'); + expect(contentType).to.eql('application/json; charset=utf-8'); + }); + +} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/echo file body/echo text file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo text file body.bru new file mode 100644 index 000000000..707a096cd --- /dev/null +++ b/packages/bruno-tests/collection/echo/echo file body/echo text file body.bru @@ -0,0 +1,30 @@ +meta { + name: echo text file body + type: http + seq: 3 +} + +post { + url: {{echo-host}} + body: file + auth: none +} + +body:file { + file: @file(file.txt) @contentType(text/plain; charset=utf-8) +} + +tests { + test("should return json file body contents", function() { + const data = res.getBody(); + const expectedData = `file.txt + + hello, bruno`; + expect(res.getBody()).to.eql(expectedData); + }); + + test("should return proper header", function() { + const contentType = res.getHeader('content-type'); + expect(contentType).to.eql('text/plain; charset=utf-8'); + }); +} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/multiline/echo binary.bru b/packages/bruno-tests/collection/echo/multiline/echo binary.bru index d11b30413..704419886 100644 --- a/packages/bruno-tests/collection/echo/multiline/echo binary.bru +++ b/packages/bruno-tests/collection/echo/multiline/echo binary.bru @@ -6,10 +6,10 @@ meta { post { url: {{echo-host}} - body: binaryFile + body: file auth: none } -body:binary-file { +body:file { file: @file(bruno.png) @contentType(image/png) } diff --git a/packages/bruno-tests/collection/file.txt b/packages/bruno-tests/collection/file.txt new file mode 100644 index 000000000..0a1443d43 --- /dev/null +++ b/packages/bruno-tests/collection/file.txt @@ -0,0 +1,3 @@ +file.txt + +hello, bruno From 8f604efc7e6a8405049c945f53f8761738111db3 Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 4 Feb 2025 21:56:34 +0530 Subject: [PATCH 025/114] fixes tests for the file body pr (#3940) fixes tests for bruno-app and bruno-electron --- packages/bruno-app/src/utils/curl/curl-to-json.spec.js | 6 ++---- .../bruno-electron/src/ipc/network/prepare-request.js | 6 +++--- .../bruno-electron/tests/network/prepare-request.spec.js | 8 +++++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js index 4c3194c2e..991150c57 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js @@ -113,11 +113,9 @@ describe('curlToJson', () => { isDataBinary: true, data: [ { - name: 'file', - value: ['/path/to/file'], - enabled: true, + filePath: '/path/to/file', contentType: 'application/json;charset=utf-8', - type: 'file' + selected: true } ] }); diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 705d19f22..05df2fde7 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -176,10 +176,10 @@ const setAuthHeaders = (axiosRequest, request, collectionRoot) => { return axiosRequest; }; -const prepareRequest = async (item, collection, abortController) => { +const prepareRequest = async (item, collection = {}, abortController) => { const request = item.draft ? item.draft.request : item.request; const collectionRoot = get(collection, 'root', {}); - const collectionPath = collection.pathname; + const collectionPath = collection?.pathname; const headers = {}; let contentTypeDefined = false; let url = request.url; @@ -191,7 +191,7 @@ const prepareRequest = async (item, collection, abortController) => { } }); - const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich'; + const scriptFlow = collection?.brunoConfig?.scripts?.flow ?? 'sandwich'; const requestTreePath = getTreePathFromCollectionToItem(collection, item); if (requestTreePath && requestTreePath.length > 0) { mergeHeaders(collection, request, requestTreePath); diff --git a/packages/bruno-electron/tests/network/prepare-request.spec.js b/packages/bruno-electron/tests/network/prepare-request.spec.js index a624d3ea5..34bedcc90 100644 --- a/packages/bruno-electron/tests/network/prepare-request.spec.js +++ b/packages/bruno-electron/tests/network/prepare-request.spec.js @@ -7,15 +7,17 @@ describe('prepare-request: prepareRequest', () => { describe('Decomments request body', () => { it('If request body is valid JSON', async () => { const body = { mode: 'json', json: '{\n"test": "{{someVar}}" // comment\n}' }; - const expected = '{\n"test": "{{someVar}}" \n}'; - const result = prepareRequest({ request: { body } }, {}); + const expected = `{ +\"test\": \"{{someVar}}\" +}`; + const result = await prepareRequest({ request: { body }, collection: { pathname: '' } }); expect(result.data).toEqual(expected); }); it('If request body is not valid JSON', async () => { const body = { mode: 'json', json: '{\n"test": {{someVar}} // comment\n}' }; const expected = '{\n"test": {{someVar}} \n}'; - const result = prepareRequest({ request: { body } }, {}); + const result = await prepareRequest({ request: { body }, collection: { pathname: '' } }); expect(result.data).toEqual(expected); }); From 038f2d1f0b6f3c06ef277f5f23f0ddf36e0748d7 Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 4 Feb 2025 22:08:54 +0530 Subject: [PATCH 026/114] temp. revert tests for file body (#3941) * temporarily revert tests for file body --- .../echo/echo file body/echo file body.bru | 40 -------------- .../echo file body/echo image file body.bru | 22 -------- .../echo file body/echo json file body.bru | 55 ------------------- .../echo file body/echo text file body.bru | 30 ---------- 4 files changed, 147 deletions(-) delete mode 100644 packages/bruno-tests/collection/echo/echo file body/echo file body.bru delete mode 100644 packages/bruno-tests/collection/echo/echo file body/echo image file body.bru delete mode 100644 packages/bruno-tests/collection/echo/echo file body/echo json file body.bru delete mode 100644 packages/bruno-tests/collection/echo/echo file body/echo text file body.bru diff --git a/packages/bruno-tests/collection/echo/echo file body/echo file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo file body.bru deleted file mode 100644 index b820e4808..000000000 --- a/packages/bruno-tests/collection/echo/echo file body/echo file body.bru +++ /dev/null @@ -1,40 +0,0 @@ -meta { - name: echo file body - type: http - seq: 4 -} - -post { - url: {{echo-host}} - body: file - auth: none -} - -body:file { - file: @file(ping.bru) @contentType(bru) -} - -tests { - test("should return bru file body contents", function() { - const data = res.getBody(); - const expectedData = `meta { - name: ping - type: http - seq: 1 - } - - get { - url: {{host}}/ping - body: none - auth: none - } - `; - expect(res.getBody()).to.eql(expectedData); - }); - - - test("should return proper header", function() { - const contentType = res.getHeader('content-type'); - expect(contentType).to.eql('bru'); - }); -} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/echo file body/echo image file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo image file body.bru deleted file mode 100644 index da1899a96..000000000 --- a/packages/bruno-tests/collection/echo/echo file body/echo image file body.bru +++ /dev/null @@ -1,22 +0,0 @@ -meta { - name: echo image body - type: http - seq: 1 -} - -post { - url: {{echo-host}} - body: file - auth: none -} - -body:file { - file: @file(bruno.png) @contentType(image/png) -} - -tests { - test("should return proper header", function() { - const contentType = res.getHeader('content-type'); - expect(contentType).to.eql('image/png'); - }); -} diff --git a/packages/bruno-tests/collection/echo/echo file body/echo json file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo json file body.bru deleted file mode 100644 index f2173a4b9..000000000 --- a/packages/bruno-tests/collection/echo/echo file body/echo json file body.bru +++ /dev/null @@ -1,55 +0,0 @@ -meta { - name: echo json file body - type: http - seq: 2 -} - -post { - url: {{echo-host}} - body: file - auth: none -} - -body:file { - file: @file(file.json) @contentType(application/json; charset=utf-8) -} - -tests { - test("should return json file body contents buffer", function() { - const data = res.getBody(); - expect(res.getBody()).to.eql({ - "type": "Buffer", - "data": [ - 123, - 10, - 32, - 32, - 34, - 104, - 101, - 108, - 108, - 111, - 34, - 58, - 32, - 34, - 98, - 114, - 117, - 110, - 111, - 34, - 10, - 125, - 10 - ] - }); - }); - - test("should return proper header", function() { - const contentType = res.getHeader('content-type'); - expect(contentType).to.eql('application/json; charset=utf-8'); - }); - -} \ No newline at end of file diff --git a/packages/bruno-tests/collection/echo/echo file body/echo text file body.bru b/packages/bruno-tests/collection/echo/echo file body/echo text file body.bru deleted file mode 100644 index 707a096cd..000000000 --- a/packages/bruno-tests/collection/echo/echo file body/echo text file body.bru +++ /dev/null @@ -1,30 +0,0 @@ -meta { - name: echo text file body - type: http - seq: 3 -} - -post { - url: {{echo-host}} - body: file - auth: none -} - -body:file { - file: @file(file.txt) @contentType(text/plain; charset=utf-8) -} - -tests { - test("should return json file body contents", function() { - const data = res.getBody(); - const expectedData = `file.txt - - hello, bruno`; - expect(res.getBody()).to.eql(expectedData); - }); - - test("should return proper header", function() { - const contentType = res.getHeader('content-type'); - expect(contentType).to.eql('text/plain; charset=utf-8'); - }); -} \ No newline at end of file From 722d9788caba9fb1ada104f804ee612ed3ec030b Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Thu, 6 Feb 2025 19:34:10 +0530 Subject: [PATCH 027/114] Feature: Improve tab UX (#3831) --------- Co-authored-by: ramki-bruno --- .../RequestTabs/RequestTab/SpecialTab.js | 8 +-- .../RequestTabs/RequestTab/index.js | 11 +-- .../Collection/CollectionItem/index.js | 42 +++++------ .../Sidebar/Collections/Collection/index.js | 32 ++++++--- .../src/providers/ReduxStore/index.js | 3 +- .../middlewares/draft/middleware.js | 55 +++++++++++++++ .../ReduxStore/middlewares/draft/utils.js | 21 ++++++ .../src/providers/ReduxStore/slices/tabs.js | 70 +++++++++++++------ packages/bruno-app/src/utils/tabs/index.js | 7 ++ 9 files changed, 189 insertions(+), 60 deletions(-) create mode 100644 packages/bruno-app/src/providers/ReduxStore/middlewares/draft/middleware.js create mode 100644 packages/bruno-app/src/providers/ReduxStore/middlewares/draft/utils.js diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js index 1cbb0aa05..b895c10fe 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js @@ -2,15 +2,15 @@ import React from 'react'; import CloseTabIcon from './CloseTabIcon'; import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons'; -const SpecialTab = ({ handleCloseClick, type, tabName }) => { +const SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick }) => { const getTabInfo = (type, tabName) => { switch (type) { case 'collection-settings': { return ( - <> +
Collection - +
); } case 'collection-overview': { @@ -31,7 +31,7 @@ const SpecialTab = ({ handleCloseClick, type, tabName }) => { } case 'folder-settings': { return ( -
+
{tabName || 'Folder'}
diff --git a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js index 2d74a4290..562fc319f 100644 --- a/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js +++ b/packages/bruno-app/src/components/RequestTabs/RequestTab/index.js @@ -1,6 +1,6 @@ import React, { useState, useRef, Fragment } from 'react'; import get from 'lodash/get'; -import { closeTabs } from 'providers/ReduxStore/slices/tabs'; +import { closeTabs, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections'; import { useTheme } from 'providers/Theme'; @@ -73,13 +73,13 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi if (['collection-settings', 'collection-overview', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) { return ( {tab.type === 'folder-settings' ? ( - + dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} /> ) : ( - + dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} /> )} ); @@ -144,8 +144,9 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi /> )}
dispatch(makeTabPermanent({ uid: tab.uid }))} onMouseUp={(e) => { if (!item.draft) return handleMouseUp(e); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 2bfece171..3da23bcf5 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -5,7 +5,7 @@ import classnames from 'classnames'; import { useDrag, useDrop } from 'react-dnd'; import { IconChevronRight, IconDots } from '@tabler/icons'; import { useSelector, useDispatch } from 'react-redux'; -import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs'; +import { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; import { moveItem, showInFolder, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections'; import Dropdown from 'components/Dropdown'; @@ -23,7 +23,9 @@ import { hideHomePage } from 'providers/ReduxStore/slices/app'; import toast from 'react-hot-toast'; import StyledWrapper from './StyledWrapper'; import NetworkError from 'components/ResponsePane/NetworkError/index'; -import CollectionItemIcon from './CollectionItemIcon/index'; +import { findItemInCollection } from 'utils/collections'; +import CollectionItemIcon from './CollectionItemIcon'; +import { scrollToTheActiveTab } from 'utils/tabs'; const CollectionItem = ({ item, collection, searchText }) => { const tabs = useSelector((state) => state.tabs.tabs); @@ -83,13 +85,6 @@ const CollectionItem = ({ item, collection, searchText }) => { 'item-hovered': isOver }); - const scrollToTheActiveTab = () => { - const activeTab = document.querySelector('.request-tab.active'); - if (activeTab) { - activeTab.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - }; - const handleRun = async () => { dispatch(sendRequest(item, collection.uid)).catch((err) => toast.custom((t) => toast.dismiss(t.id)} />, { @@ -99,10 +94,13 @@ const CollectionItem = ({ item, collection, searchText }) => { }; const handleClick = (event) => { + if (event.detail != 1) return; //scroll to the active tab setTimeout(scrollToTheActiveTab, 50); - - if (isItemARequest(item)) { + + const isRequest = isItemARequest(item); + + if (isRequest) { dispatch(hideHomePage()); if (itemIsOpenedInTabs(item, tabs)) { dispatch( @@ -112,20 +110,21 @@ const CollectionItem = ({ item, collection, searchText }) => { ); return; } + dispatch( addTab({ uid: item.uid, collectionUid: collection.uid, - requestPaneTab: getDefaultRequestPaneTab(item) + requestPaneTab: getDefaultRequestPaneTab(item), + type: 'request', }) ); - return; - } + } else { dispatch( addTab({ uid: item.uid, collectionUid: collection.uid, - type: 'folder-settings' + type: 'folder-settings', }) ); dispatch( @@ -134,9 +133,12 @@ const CollectionItem = ({ item, collection, searchText }) => { collectionUid: collection.uid }) ); + } }; - const handleFolderCollapse = () => { + const handleFolderCollapse = (e) => { + e.stopPropagation(); + e.preventDefault(); dispatch( collectionFolderClicked({ itemUid: item.uid, @@ -156,10 +158,6 @@ const CollectionItem = ({ item, collection, searchText }) => { } }; - const handleDoubleClick = (event) => { - setRenameItemModalOpen(true); - }; - let indents = range(item.depth); const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref); const isFolder = isItemAFolder(item); @@ -180,6 +178,10 @@ const CollectionItem = ({ item, collection, searchText }) => { } } + const handleDoubleClick = (event) => { + dispatch(makeTabPermanent({ uid: item.uid })) + }; + // we need to sort request items by seq property const sortRequestItems = (items = []) => { return items.sort((a, b) => a.seq - b.seq); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index 1b16f4eea..f8e4484bc 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -7,8 +7,8 @@ import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons'; import Dropdown from 'components/Dropdown'; import { collapseCollection } from 'providers/ReduxStore/slices/collections'; import { mountCollection, moveItemToRootOfCollection } from 'providers/ReduxStore/slices/collections/actions'; -import { useDispatch } from 'react-redux'; -import { addTab } from 'providers/ReduxStore/slices/tabs'; +import { useDispatch, useSelector } from 'react-redux'; +import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs'; import NewRequest from 'components/Sidebar/NewRequest'; import NewFolder from 'components/Sidebar/NewFolder'; import CollectionItem from './CollectionItem'; @@ -20,7 +20,8 @@ import { isItemAFolder, isItemARequest } from 'utils/collections'; import RenameCollection from './RenameCollection'; import StyledWrapper from './StyledWrapper'; import CloneCollection from './CloneCollection'; -import { areItemsLoading } from 'utils/collections'; +import { areItemsLoading, findItemInCollection } from 'utils/collections'; +import { scrollToTheActiveTab } from 'utils/tabs'; const Collection = ({ collection, searchText }) => { const [showNewFolderModal, setShowNewFolderModal] = useState(false); @@ -29,6 +30,7 @@ const Collection = ({ collection, searchText }) => { const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false); const [showExportCollectionModal, setShowExportCollectionModal] = useState(false); const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false); + const tabs = useSelector((state) => state.tabs.tabs); const dispatch = useDispatch(); const isLoading = areItemsLoading(collection); @@ -60,9 +62,11 @@ const Collection = ({ collection, searchText }) => { }); const handleClick = (event) => { + if (event.detail != 1) return; // Check if the click came from the chevron icon const isChevronClick = event.target.closest('svg')?.classList.contains('chevron-icon'); - + setTimeout(scrollToTheActiveTab, 50); + if (collection.mountStatus === 'unmounted') { dispatch(mountCollection({ collectionUid: collection.uid, @@ -70,20 +74,30 @@ const Collection = ({ collection, searchText }) => { brunoConfig: collection.brunoConfig })); } + dispatch(collapseCollection(collection.uid)); - - // Only open collection settings if not clicking the chevron + if(!isChevronClick) { dispatch( addTab({ - uid: uuid(), + uid: collection.uid, collectionUid: collection.uid, - type: 'collection-settings' + type: 'collection-settings', }) ); } }; + const handleDoubleClick = (event) => { + dispatch(makeTabPermanent({ uid: collection.uid })) + }; + + const handleCollectionCollapse = (e) => { + e.stopPropagation(); + e.preventDefault(); + dispatch(collapseCollection(collection.uid)); + } + const handleRightClick = (event) => { const _menuDropdown = menuDropdownTippyRef.current; if (_menuDropdown) { @@ -158,6 +172,7 @@ const Collection = ({ collection, searchText }) => {
{ strokeWidth={2} className={`chevron-icon ${iconClassName}`} style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }} + onClick={handleCollectionCollapse} />
)} diff --git a/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js b/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js index 17d79629e..b0815c018 100644 --- a/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js +++ b/packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js @@ -88,7 +88,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => { )} diff --git a/packages/bruno-app/src/components/InfoTip/index.js b/packages/bruno-app/src/components/InfoTip/index.js index 97eb63d4d..2af6034ed 100644 --- a/packages/bruno-app/src/components/InfoTip/index.js +++ b/packages/bruno-app/src/components/InfoTip/index.js @@ -1,7 +1,7 @@ import React from 'react'; import { Tooltip as ReactInfoTip } from 'react-tooltip'; -const InfoTip = ({ text, infotipId }) => { +const InfoTip = ({ html: _ignored, infotipId, ...props }) => { return ( <> { - + ); }; diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js index 777280eb0..3f7f7ef01 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js @@ -176,8 +176,7 @@ const QueryParams = ({ item, collection }) => {
Path -
Path variables are automatically added whenever the :name @@ -186,9 +185,7 @@ const QueryParams = ({ item, collection }) => { https://example.com/v1/users/:id
- `} - infotipId="path-param-InfoTip" - /> +
File
Content-Type
Enabled
+
File
+
+
Content-Type
+
+
Selected
+
- - handleParamChange( - { - target: { - value: newValue - } - }, - param, - 'value' - ) - } - collection={collection} - /> + + handleParamChange( + { + target: { + filePath: path + } + }, + param, + 'filePath' + ) + } + collection={collection} + /> { handleParamChange( { target: { - value: newValue + contentType: newValue } }, param, @@ -141,19 +132,19 @@ const Binary = ({ item, collection }) => { handleParamChange(e, param, 'enabled')} + onChange={(e) => handleParamChange(e, param, 'selected')} /> -
- +
+
Expr - +
Expr - +
diff --git a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js index c073135d3..cd3f83797 100644 --- a/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js +++ b/packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js @@ -98,7 +98,7 @@ const VarsTable = ({ item, collection, vars, varType }) => { ) : (
Expr - +
), accessor: 'value', width: '46%' }, { name: '', accessor: '', width: '14%' } diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js index 41d3e5ff2..1b048e731 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js @@ -127,7 +127,7 @@ const CloneCollection = ({ onClose, collection }) => { diff --git a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js index 6f05207d2..17f31f1f8 100644 --- a/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/CreateCollection/index.js @@ -120,7 +120,7 @@ const CreateCollection = ({ onClose }) => { From c997924c42d47b7473475b2a4e6d1a3403ee7530 Mon Sep 17 00:00:00 2001 From: ramki-bruno Date: Wed, 12 Feb 2025 20:48:19 +0530 Subject: [PATCH 061/114] Strengthen CSP --- packages/bruno-electron/src/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index 0cb75645d..522df6c68 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -30,8 +30,7 @@ const lastOpenedCollections = new LastOpenedCollections(); // Reference: https://content-security-policy.com/ const contentSecurityPolicy = [ "default-src 'self'", - "script-src * 'unsafe-inline' 'unsafe-eval'", - "connect-src * 'unsafe-inline'", + "connect-src 'self' https://*.posthog.com", "font-src 'self' https:", "frame-src data:", // this has been commented out to make oauth2 work From 200732bac59ea14ee49eb74bec18f0eb66792ff7 Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Fri, 14 Feb 2025 17:50:22 +0530 Subject: [PATCH 062/114] fix: space on collection docs --- .../src/components/CollectionSettings/Docs/StyledWrapper.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js index afe08bcba..4c3130e3d 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js +++ b/packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js @@ -1,11 +1,7 @@ import styled from 'styled-components'; const StyledWrapper = styled.div` - div.CodeMirror { - .CodeMirror-scroll { - padding-bottom: 0px; - } - } + .editing-mode { cursor: pointer; } From f8b4a0b85b4f3d43252b0eb376af83ba32317de9 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Fri, 14 Feb 2025 17:36:16 +0530 Subject: [PATCH 063/114] feat: notification visibility rules based on semver --- package-lock.json | 14 +- packages/bruno-app/package.json | 3 +- .../src/components/Notifications/index.js | 10 +- .../bruno-app/src/components/Sidebar/index.js | 5 +- packages/bruno-app/src/providers/App/index.js | 13 +- .../src/providers/App/useTelemetry.js | 14 +- .../ReduxStore/slices/notifications.js | 25 +++- .../ReduxStore/slices/notifications.spec.js | 133 ++++++++++++++++++ 8 files changed, 201 insertions(+), 16 deletions(-) create mode 100644 packages/bruno-app/src/providers/ReduxStore/slices/notifications.spec.js diff --git a/package-lock.json b/package-lock.json index 4f5bb0ac9..8d63dedff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24408,7 +24408,7 @@ }, "packages/bruno-app": { "name": "@usebruno/app", - "version": "0.3.0", + "version": "1.39.0", "dependencies": { "@babel/preset-env": "^7.26.0", "@fontsource/inter": "^5.0.15", @@ -24468,6 +24468,7 @@ "react-redux": "^7.2.9", "react-tooltip": "^5.5.2", "sass": "^1.46.0", + "semver": "^7.7.1", "strip-json-comments": "^5.0.1", "styled-components": "^5.3.3", "system": "^2.0.1", @@ -24525,6 +24526,17 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "packages/bruno-app/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "packages/bruno-cli": { "name": "@usebruno/cli", "version": "1.16.0", diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index 56a2eab1a..f3e6fe292 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -1,6 +1,6 @@ { "name": "@usebruno/app", - "version": "0.3.0", + "version": "1.39.0", "private": true, "scripts": { "dev": "rsbuild dev", @@ -69,6 +69,7 @@ "react-redux": "^7.2.9", "react-tooltip": "^5.5.2", "sass": "^1.46.0", + "semver": "^7.7.1", "strip-json-comments": "^5.0.1", "styled-components": "^5.3.3", "system": "^2.0.1", diff --git a/packages/bruno-app/src/components/Notifications/index.js b/packages/bruno-app/src/components/Notifications/index.js index f308ca96a..653d56959 100644 --- a/packages/bruno-app/src/components/Notifications/index.js +++ b/packages/bruno-app/src/components/Notifications/index.js @@ -3,6 +3,7 @@ import { useState } from 'react'; import StyledWrapper from './StyleWrapper'; import Modal from 'components/Modal/index'; import { useEffect } from 'react'; +import { useApp } from 'providers/App'; import { fetchNotifications, markAllNotificationsAsRead, @@ -17,6 +18,7 @@ const PAGE_SIZE = 5; const Notifications = () => { const dispatch = useDispatch(); + const { version } = useApp(); const notifications = useSelector((state) => state.notifications.notifications); const [showNotificationsModal, setShowNotificationsModal] = useState(false); @@ -29,7 +31,9 @@ const Notifications = () => { const unreadNotifications = notifications.filter((notification) => !notification.read); useEffect(() => { - dispatch(fetchNotifications()); + dispatch(fetchNotifications({ + currentVersion: version + })); }, []); useEffect(() => { @@ -96,7 +100,9 @@ const Notifications = () => { { - dispatch(fetchNotifications()); + dispatch(fetchNotifications({ + currentVersion: version + })); setShowNotificationsModal(true); }} aria-label="Check all Notifications" diff --git a/packages/bruno-app/src/components/Sidebar/index.js b/packages/bruno-app/src/components/Sidebar/index.js index 50e19c22e..9f476e3c8 100644 --- a/packages/bruno-app/src/components/Sidebar/index.js +++ b/packages/bruno-app/src/components/Sidebar/index.js @@ -5,6 +5,7 @@ import Preferences from 'components/Preferences'; import Cookies from 'components/Cookies'; import ToolHint from 'components/ToolHint'; import GoldenEdition from './GoldenEdition'; +import { useApp } from 'providers/App'; import { useState, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; @@ -20,7 +21,7 @@ const Sidebar = () => { const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth); const preferencesOpen = useSelector((state) => state.app.showPreferences); const [goldenEditionOpen, setGoldenEditionOpen] = useState(false); - + const { version } = useApp(); const [asideWidth, setAsideWidth] = useState(leftSidebarWidth); const [cookiesOpen, setCookiesOpen] = useState(false); @@ -184,7 +185,7 @@ const Sidebar = () => { Star */} -
v1.36.1
+
v{version}
diff --git a/packages/bruno-app/src/providers/App/index.js b/packages/bruno-app/src/providers/App/index.js index 7664ae03e..b06d1d3a8 100644 --- a/packages/bruno-app/src/providers/App/index.js +++ b/packages/bruno-app/src/providers/App/index.js @@ -6,11 +6,12 @@ import ConfirmAppClose from './ConfirmAppClose'; import useIpcEvents from './useIpcEvents'; import useTelemetry from './useTelemetry'; import StyledWrapper from './StyledWrapper'; +import { version } from '../../../package.json'; export const AppContext = React.createContext(); export const AppProvider = (props) => { - useTelemetry(); + useTelemetry({ version }); useIpcEvents(); const dispatch = useDispatch(); @@ -37,7 +38,7 @@ export const AppProvider = (props) => { }, []); return ( - + {props.children} @@ -46,4 +47,12 @@ export const AppProvider = (props) => { ); }; +export const useApp = () => { + const context = React.useContext(AppContext); + if (!context) { + throw new Error('useApp must be used within an AppProvider'); + } + return context; +}; + export default AppProvider; diff --git a/packages/bruno-app/src/providers/App/useTelemetry.js b/packages/bruno-app/src/providers/App/useTelemetry.js index 6b64e1279..712a6efb7 100644 --- a/packages/bruno-app/src/providers/App/useTelemetry.js +++ b/packages/bruno-app/src/providers/App/useTelemetry.js @@ -42,7 +42,7 @@ const getAnonymousTrackingId = () => { return id; }; -const trackStart = () => { +const trackStart = (version) => { if (isPlaywrightTestRunning()) { return; } @@ -58,16 +58,18 @@ const trackStart = () => { event: 'start', properties: { os: platformLib.os.family, - version: '1.38.1' + version: version } }); }; -const useTelemetry = () => { +const useTelemetry = ({ version }) => { useEffect(() => { - trackStart(); - setInterval(trackStart, 24 * 60 * 60 * 1000); - }, []); + if (posthogApiKey && posthogApiKey.length) { + trackStart(version); + setInterval(trackStart, 24 * 60 * 60 * 1000); + } + }, [posthogApiKey]); }; export default useTelemetry; diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js b/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js index ca6c232d8..062f367ca 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/notifications.js @@ -1,7 +1,7 @@ import toast from 'react-hot-toast'; import { createSlice } from '@reduxjs/toolkit'; import { getAppInstallDate } from 'utils/common/platform'; - +import semver from 'semver'; const getReadNotificationIds = () => { try { let readNotificationIdsString = window.localStorage.getItem('bruno.notifications.read'); @@ -27,6 +27,26 @@ const initialState = { readNotificationIds: getReadNotificationIds() || [] }; +export const filterNotificationsByVersion = (notifications, currentVersion) => { + try { + if (!notifications) return []; + + if (!currentVersion) return notifications; + + return notifications.filter(notification => { + const { minVersion, maxVersion } = notification; + if (!minVersion && !maxVersion) return true; + if (!minVersion) return semver.lte(currentVersion, maxVersion); + if (!maxVersion) return semver.gte(currentVersion, minVersion); + + return semver.gte(currentVersion, minVersion) && semver.lte(currentVersion, maxVersion); + }); + } catch (error) { + console.error(error); + return []; + } +}; + export const notificationSlice = createSlice({ name: 'notifications', initialState, @@ -86,13 +106,14 @@ export const notificationSlice = createSlice({ export const { setNotifications, setFetchingStatus, markNotificationAsRead, markAllNotificationsAsRead } = notificationSlice.actions; -export const fetchNotifications = () => (dispatch, getState) => { +export const fetchNotifications = ({currentVersion}) => (dispatch, getState) => { return new Promise((resolve) => { const { ipcRenderer } = window; dispatch(setFetchingStatus(true)); ipcRenderer .invoke('renderer:fetch-notifications') .then((notifications) => { + notifications = filterNotificationsByVersion(notifications, currentVersion); dispatch(setNotifications({ notifications })); dispatch(setFetchingStatus(false)); resolve(notifications); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/notifications.spec.js b/packages/bruno-app/src/providers/ReduxStore/slices/notifications.spec.js new file mode 100644 index 000000000..80e84d9bd --- /dev/null +++ b/packages/bruno-app/src/providers/ReduxStore/slices/notifications.spec.js @@ -0,0 +1,133 @@ +const { filterNotificationsByVersion } = require('./notifications'); + +describe('filterNotificationsByVersion - basic', () => { + it('should filter notifications by version', () => { + const notifications = [{ minVersion: '1.0.0', maxVersion: '1.1.0' }]; + const currentVersion = '1.0.5'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([{ minVersion: '1.0.0', maxVersion: '1.1.0' }]); + }); + + it('should gracefully handle no notifications', () => { + const notifications = []; + const currentVersion = '1.0.5'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([]); + }); + + it('should gracefully handle notifications are undefined', () => { + const notifications = undefined; + const currentVersion = '1.0.5'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([]); + }); + + it('should gracefully handle scenario when no current version is provided', () => { + const notifications = [{ minVersion: '1.0.0', maxVersion: '1.1.0' }]; + const filteredNotifications = filterNotificationsByVersion(notifications); + expect(filteredNotifications).toEqual(notifications); + }); + + it('should gracefully handle scenario minVersion is undefined', () => { + const notifications = [{ minVersion: undefined, maxVersion: '1.1.0' }]; + const currentVersion = '1.0.5'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual(notifications); + }); + + it('should gracefully handle scenario maxVersion is undefined', () => { + const notifications = [{ minVersion: '1.0.0', maxVersion: undefined }]; + const currentVersion = '1.0.5'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual(notifications); + }); + + it('should gracefully handle scenario minVersion and maxVersion are undefined', () => { + const notifications = [{ minVersion: undefined, maxVersion: undefined }]; + const currentVersion = '1.0.5'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual(notifications); + }); +}); + +describe('filterNotificationsByVersion - semver', () => { + it('should filter out notifications outside version range', () => { + const notifications = [ + { minVersion: '1.0.0', maxVersion: '1.1.0' }, // should be included + { minVersion: '2.0.0', maxVersion: '2.1.0' }, // should be filtered out + { minVersion: '0.5.0', maxVersion: '0.9.0' } // should be filtered out + ]; + const currentVersion = '1.0.5'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([ + { minVersion: '1.0.0', maxVersion: '1.1.0' } + ]); + }); + + it('should handle mixed valid and invalid version ranges', () => { + const notifications = [ + { minVersion: '1.0.0', maxVersion: '2.0.0' }, // should be included + { minVersion: '3.0.0', maxVersion: '4.0.0' }, // should be filtered out + { minVersion: '1.5.0', maxVersion: '1.8.0' }, // should be included + { minVersion: '0.1.0', maxVersion: '0.5.0' } // should be filtered out + ]; + const currentVersion = '1.6.0'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([ + { minVersion: '1.0.0', maxVersion: '2.0.0' }, + { minVersion: '1.5.0', maxVersion: '1.8.0' } + ]); + }); + + it('should handle edge cases of version ranges', () => { + const notifications = [ + { minVersion: '1.0.0', maxVersion: '1.0.0' }, // should be included + { minVersion: '1.0.1', maxVersion: '2.0.0' }, // should be filtered out + { minVersion: '0.9.9', maxVersion: '1.0.0' } // should be included + ]; + const currentVersion = '1.0.0'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([ + { minVersion: '1.0.0', maxVersion: '1.0.0' }, + { minVersion: '0.9.9', maxVersion: '1.0.0' } + ]); + }); +}); + +describe('filterNotificationsByVersion - undefined version bounds', () => { + it('should include notifications when minVersion is undefined and current version is below maxVersion', () => { + const notifications = [ + { minVersion: undefined, maxVersion: '2.0.0' } + ]; + const currentVersion = '1.5.0'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual(notifications); + }); + + it('should exclude notifications when minVersion is undefined and current version is above maxVersion', () => { + const notifications = [ + { minVersion: undefined, maxVersion: '2.0.0' } + ]; + const currentVersion = '2.1.0'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([]); + }); + + it('should include notifications when maxVersion is undefined and current version is above minVersion', () => { + const notifications = [ + { minVersion: '1.0.0', maxVersion: undefined } + ]; + const currentVersion = '2.0.0'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual(notifications); + }); + + it('should exclude notifications when maxVersion is undefined and current version is below minVersion', () => { + const notifications = [ + { minVersion: '1.0.0', maxVersion: undefined } + ]; + const currentVersion = '0.9.0'; + const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion); + expect(filteredNotifications).toEqual([]); + }); +}); \ No newline at end of file From dfb0b1b96627a978eb5c33378c026348d0004397 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Fri, 14 Feb 2025 21:23:26 +0530 Subject: [PATCH 064/114] fix: allow popus in notification iframes This is to allow is to allow users to open pages (like bruno downloads) from the app --- packages/bruno-app/src/components/Notifications/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/Notifications/index.js b/packages/bruno-app/src/components/Notifications/index.js index 653d56959..d11a6254f 100644 --- a/packages/bruno-app/src/components/Notifications/index.js +++ b/packages/bruno-app/src/components/Notifications/index.js @@ -193,7 +193,7 @@ const Notifications = () => { From 31409c6206a4ac4e03ef4cd6abaa4569d7f2c3db Mon Sep 17 00:00:00 2001 From: lohit Date: Tue, 18 Feb 2025 19:58:37 +0530 Subject: [PATCH 065/114] feat: reuse worker threads for bru file parsing (#4054) --- .../CollectionSettings/Overview/Info/index.js | 16 +++++++--- .../Overview/RequestsNotLoaded/index.js | 32 ++++++++++++++++++- .../RequestTabPanel/RequestNotLoaded/index.js | 8 ++--- .../ReduxStore/slices/collections/index.js | 4 ++- .../bruno-app/src/utils/collections/index.js | 14 ++++++++ .../src/bru/workers/scripts/bru-to-json.js | 22 +++++++------ .../src/bru/workers/scripts/json-to-bru.js | 23 +++++++------ packages/bruno-electron/src/ipc/collection.js | 6 ++-- packages/bruno-electron/src/workers/index.js | 18 ++++++++--- 9 files changed, 104 insertions(+), 39 deletions(-) diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js index 86bf2308f..a83850e91 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js @@ -1,10 +1,14 @@ -import React from 'react'; +import React from "react"; import { getTotalRequestCountInCollection } from 'utils/collections/'; -import { IconFolder, IconFileOff, IconWorld, IconApi } from '@tabler/icons'; +import { IconFolder, IconWorld, IconApi, IconClock } from '@tabler/icons'; +import { areItemsLoading, getItemsLoadStats } from "utils/collections/index"; const Info = ({ collection }) => { const totalRequestsInCollection = getTotalRequestCountInCollection(collection); + const isCollectionLoading = areItemsLoading(collection); + const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection); + return (
@@ -42,8 +46,10 @@ const Info = ({ collection }) => {
Requests
-
- {totalRequestsInCollection} request{totalRequestsInCollection !== 1 ? 's' : ''} in collection +
+ { + isCollectionLoading? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection` + }
@@ -53,4 +59,4 @@ const Info = ({ collection }) => { ); }; -export default Info; +export default Info; \ No newline at end of file diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js index c15b36cd8..4c7406580 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js @@ -2,8 +2,15 @@ import React from 'react'; import { flattenItems } from "utils/collections"; import { IconAlertTriangle } from '@tabler/icons'; import StyledWrapper from "./StyledWrapper"; +import { useDispatch, useSelector } from 'react-redux'; +import { isItemARequest, itemIsOpenedInTabs } from 'utils/tabs/index'; +import { getDefaultRequestPaneTab } from 'utils/collections/index'; +import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs'; +import { hideHomePage } from 'providers/ReduxStore/slices/app'; const RequestsNotLoaded = ({ collection }) => { + const dispatch = useDispatch(); + const tabs = useSelector((state) => state.tabs.tabs); const flattenedItems = flattenItems(collection.items); const itemsFailedLoading = flattenedItems?.filter(item => item?.partial && !item?.loading); @@ -11,6 +18,29 @@ const RequestsNotLoaded = ({ collection }) => { return null; } + const handleRequestClick = (item) => e => { + e.preventDefault(); + if (isItemARequest(item)) { + dispatch(hideHomePage()); + if (itemIsOpenedInTabs(item, tabs)) { + dispatch( + focusTab({ + uid: item.uid + }) + ); + return; + } + dispatch( + addTab({ + uid: item.uid, + collectionUid: collection.uid, + requestPaneTab: getDefaultRequestPaneTab(item) + }) + ); + return; + } + } + return (
@@ -31,7 +61,7 @@ const RequestsNotLoaded = ({ collection }) => {
{flattenedItems?.map((item, index) => ( item?.partial && !item?.loading ? ( - + diff --git a/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js b/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js index 289f3c879..7908dfc09 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js @@ -21,18 +21,18 @@ const RequestNotLoaded = ({ collection, item }) => { File Info -
+
Name:
{item?.name}
- +
Path:
{item?.pathname}
- +
Size:
{item?.size?.toFixed?.(2)} MB
@@ -67,7 +67,7 @@ const RequestNotLoaded = ({ collection, item }) => { {item?.loading && ( <> -
+
Loading... 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 5d994a606..905576a2f 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -59,7 +59,9 @@ export const collectionsSlice = createSlice({ updateCollectionMountStatus: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); if (collection) { - collection.mountStatus = action.payload.mountStatus; + if (action.payload.mountStatus) { + collection.mountStatus = action.payload.mountStatus; + } } }, setCollectionSecurityConfig: (state, action) => { diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index e119553e4..3ac612c62 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -137,6 +137,20 @@ export const areItemsLoading = (folder) => { }, false); } +export const getItemsLoadStats = (folder) => { + let loadingCount = 0; + let flattenedItems = flattenItems(folder.items); + flattenedItems?.forEach(i => { + if(i?.loading) { + loadingCount += 1; + } + }); + return { + loading: loadingCount, + total: flattenedItems?.length + }; +} + export const moveCollectionItem = (collection, draggedItem, targetItem) => { let draggedItemParent = findParentItemInCollection(collection, draggedItem.uid); diff --git a/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js b/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js index c1bbb44e7..92086c4b6 100644 --- a/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js +++ b/packages/bruno-electron/src/bru/workers/scripts/bru-to-json.js @@ -1,14 +1,16 @@ -const { workerData, parentPort } = require('worker_threads'); +const { parentPort } = require('worker_threads'); const { bruToJsonV2, } = require('@usebruno/lang'); -try { - const bru = workerData; - const json = bruToJsonV2(bru); - parentPort.postMessage(json); -} -catch(error) { - console.error(error); - parentPort.postMessage({ error: error?.message }); -} \ No newline at end of file +parentPort.on('message', (workerData) => { + try { + const bru = workerData; + const json = bruToJsonV2(bru); + parentPort.postMessage(json); + } + catch(error) { + console.error(error); + parentPort.postMessage({ error: error?.message }); + } +}); \ No newline at end of file diff --git a/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js b/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js index e08be60b9..c2a4f88e4 100644 --- a/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js +++ b/packages/bruno-electron/src/bru/workers/scripts/json-to-bru.js @@ -1,13 +1,16 @@ -const { workerData, parentPort } = require('worker_threads'); +const { parentPort } = require('worker_threads'); const { jsonToBruV2, } = require('@usebruno/lang'); -try { - const json = workerData; - const bru = jsonToBruV2(json); - parentPort.postMessage(bru); -} -catch(error) { - console.error(error); - parentPort.postMessage({ error: error?.message }); -} \ No newline at end of file + +parentPort.on('message', (workerData) => { + try { + const json = workerData; + const bru = jsonToBruV2(json); + parentPort.postMessage(bru); + } + catch(error) { + console.error(error); + parentPort.postMessage({ error: error?.message }); + } +}); \ No newline at end of file diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 27a34d861..43a3a1283 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -40,9 +40,9 @@ const collectionSecurityStore = new CollectionSecurityStore(); const uiStateSnapshotStore = new UiStateSnapshotStore(); // size and file count limits to determine whether the bru files in the collection should be loaded asynchronously or not. -const MAX_COLLECTION_SIZE_IN_MB = 5; -const MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB = 2; -const MAX_COLLECTION_FILES_COUNT = 100; +const MAX_COLLECTION_SIZE_IN_MB = 20; +const MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB = 5; +const MAX_COLLECTION_FILES_COUNT = 2000; const envHasSecrets = (environment = {}) => { const secrets = _.filter(environment.variables, (v) => v.secret); diff --git a/packages/bruno-electron/src/workers/index.js b/packages/bruno-electron/src/workers/index.js index 04836e9fc..d1d1a1b74 100644 --- a/packages/bruno-electron/src/workers/index.js +++ b/packages/bruno-electron/src/workers/index.js @@ -4,8 +4,18 @@ class WorkerQueue { constructor() { this.queue = []; this.isProcessing = false; + this.workers = {}; } + async getWorkerForScriptPath(scriptPath) { + if (!this.workers) this.workers = {}; + let worker = this.workers[scriptPath]; + if (!worker || worker.threadId === -1) { + this.workers[scriptPath] = worker = new Worker(scriptPath); + } + return worker; + } + async enqueue(task) { const { priority, scriptPath, data } = task; @@ -36,22 +46,20 @@ class WorkerQueue { } async runWorker({ scriptPath, data }) { - return new Promise((resolve, reject) => { - const worker = new Worker(scriptPath, { workerData: data }); + return new Promise(async (resolve, reject) => { + let worker = await this.getWorkerForScriptPath(scriptPath); + worker.postMessage(data); worker.on('message', (data) => { if (data?.error) { reject(new Error(data?.error)); } resolve(data); - worker.terminate(); }); worker.on('error', (error) => { reject(error); - worker.terminate(); }); worker.on('exit', (code) => { reject(new Error(`stopped with ${code} exit code`)); - worker.terminate(); }); }); } From c58604716ef8fe673f682a30b2c5d9699725e64f Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 18 Feb 2025 21:56:41 +0530 Subject: [PATCH 066/114] Update FeatureRequest.yaml (#3974) updated submittal questions --- .github/ISSUE_TEMPLATE/FeatureRequest.yaml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/FeatureRequest.yaml b/.github/ISSUE_TEMPLATE/FeatureRequest.yaml index 3a3997beb..161e56e9c 100644 --- a/.github/ISSUE_TEMPLATE/FeatureRequest.yaml +++ b/.github/ISSUE_TEMPLATE/FeatureRequest.yaml @@ -8,13 +8,23 @@ body: options: - label: I've searched existing issues and found nothing related to my issue. required: true + - type: checkboxes + attributes: + label: 'This feature' + options: + - label: blocks me from using Bruno + required: false + - label: would improve my quality of life in Bruno + required: false + - label: is something I've never seen an API client do before + required: false - type: markdown attributes: value: | Suggest an idea for this project. - type: textarea attributes: - label: Describe the feature you want to add + label: Describe the feature you want to add, and how it would change your usage of Bruno description: A clear and concise description of the feature you want to be added. validations: required: true @@ -23,4 +33,4 @@ body: label: Mockups or Images of the feature description: Add some images to support your feature. validations: - required: true + required: false From 2fc45de43089f26b15057a4c6a2d72c2ece18c7a Mon Sep 17 00:00:00 2001 From: Sanjai Kumar Date: Wed, 5 Feb 2025 18:00:19 +0530 Subject: [PATCH 067/114] chore: update BugReport template to include version and OS information --- .github/ISSUE_TEMPLATE/BugReport.yaml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/BugReport.yaml b/.github/ISSUE_TEMPLATE/BugReport.yaml index 4b6da7871..fb279dd97 100644 --- a/.github/ISSUE_TEMPLATE/BugReport.yaml +++ b/.github/ISSUE_TEMPLATE/BugReport.yaml @@ -10,20 +10,32 @@ body: attributes: label: 'I have checked the following:' options: - - label: I use the newest version of bruno. - required: true - - label: I've searched existing issues and found nothing related to my issue. + - label: I've [searched](https://github.com/usebruno/bruno/issues?q=is%3Aissue) existing issues and found nothing related to my issue. required: true + - type: input + attributes: + label: Bruno version + description: Please specify the version of Bruno you are using in which the issue occures. + placeholder: 1.38.1 + validations: + required: true + - type: input + attributes: + label: Operating System + description: Information about the operating system the issue occurs on. + placeholder: Windows / MacOS / Linux + validations: + required: true - type: textarea attributes: label: Describe the bug - description: A clear and concise description of the bug. + description: A clear and concise description of the bug and also include steps to reproduce it. validations: required: true - type: textarea attributes: label: .bru file to reproduce the bug - description: Attach your .bru file here that can reqroduce the problem. + description: Attach your .bru file here that can reproduce the problem. validations: required: false - type: textarea From b28b60d4a7bb3bdca3466e9c4e901025cdda3661 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar Date: Wed, 5 Feb 2025 18:09:24 +0530 Subject: [PATCH 068/114] chore: update BugReport template for clarity and additional information --- .github/ISSUE_TEMPLATE/BugReport.yaml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/BugReport.yaml b/.github/ISSUE_TEMPLATE/BugReport.yaml index fb279dd97..1cee7e028 100644 --- a/.github/ISSUE_TEMPLATE/BugReport.yaml +++ b/.github/ISSUE_TEMPLATE/BugReport.yaml @@ -6,38 +6,47 @@ body: attributes: value: | Thanks for taking the time to fill out this bug report! + + Before submitting, please make sure you've searched existing issues: + 👉 [Search existing issues](https://github.com/usebruno/bruno/issues?q=is%3Aissue) + - type: checkboxes attributes: label: 'I have checked the following:' options: - - label: I've [searched](https://github.com/usebruno/bruno/issues?q=is%3Aissue) existing issues and found nothing related to my issue. + - label: "I have searched existing issues and found nothing related to my issue." required: true + - type: input attributes: label: Bruno version - description: Please specify the version of Bruno you are using in which the issue occures. + description: Please specify the version of Bruno you are using in which the issue occurs. placeholder: 1.38.1 validations: required: true + - type: input attributes: label: Operating System description: Information about the operating system the issue occurs on. - placeholder: Windows / MacOS / Linux + placeholder: Windows / Mac / Linux validations: required: true + - type: textarea attributes: label: Describe the bug description: A clear and concise description of the bug and also include steps to reproduce it. validations: required: true + - type: textarea attributes: label: .bru file to reproduce the bug description: Attach your .bru file here that can reproduce the problem. validations: required: false + - type: textarea attributes: label: Screenshots/Live demo link From 7cacc255b4aa01ce859ebe0c8ed119bfe6616922 Mon Sep 17 00:00:00 2001 From: tlaloc911 <29415755+tlaloc911@users.noreply.github.com> Date: Thu, 20 Feb 2025 08:06:07 -0600 Subject: [PATCH 069/114] fix h1 and h2 style --- packages/bruno-app/src/components/MarkDown/StyledWrapper.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bruno-app/src/components/MarkDown/StyledWrapper.js b/packages/bruno-app/src/components/MarkDown/StyledWrapper.js index 0ac61b4e5..a7a174a69 100644 --- a/packages/bruno-app/src/components/MarkDown/StyledWrapper.js +++ b/packages/bruno-app/src/components/MarkDown/StyledWrapper.js @@ -15,14 +15,14 @@ const StyledMarkdownBodyWrapper = styled.div` margin: 0.67em 0; font-weight: var(--base-text-weight-semibold, 600); padding-bottom: 0.3em; - font-size: 1.3; + font-size: 1.3em; border-bottom: 1px solid var(--color-border-muted); } h2 { font-weight: var(--base-text-weight-semibold, 600); padding-bottom: 0.3em; - font-size: 1.2; + font-size: 1.2em; border-bottom: 1px solid var(--color-border-muted); } From 4977dbeb1120d1d3ab29b67647d4677af9a71814 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 25 Feb 2025 11:44:40 -0600 Subject: [PATCH 070/114] Update BugReport.yaml Added context around cause of bug, better placeholders for OS versions --- .github/ISSUE_TEMPLATE/BugReport.yaml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/BugReport.yaml b/.github/ISSUE_TEMPLATE/BugReport.yaml index 1cee7e028..f525bb7e6 100644 --- a/.github/ISSUE_TEMPLATE/BugReport.yaml +++ b/.github/ISSUE_TEMPLATE/BugReport.yaml @@ -17,6 +17,17 @@ body: - label: "I have searched existing issues and found nothing related to my issue." required: true + - type: checkboxes + attributes: + label: 'This bug is:' + options: + - label: making Bruno unusable for me + required: false + - label: slowing me down but I'm able to continue working + required: false + - label: annoying + required: false + - type: input attributes: label: Bruno version @@ -29,14 +40,14 @@ body: attributes: label: Operating System description: Information about the operating system the issue occurs on. - placeholder: Windows / Mac / Linux + placeholder: Windows 11 26100.3037 / macOS 15.1 (24B83) / Linux 6.13.1 validations: required: true - type: textarea attributes: label: Describe the bug - description: A clear and concise description of the bug and also include steps to reproduce it. + description: A clear and concise description of the bug and how it's effecting your work along with steps to reproduce. validations: required: true From a438c06b979a786f00fd78b81cf6c96de2fed5b2 Mon Sep 17 00:00:00 2001 From: sanish-bruno Date: Fri, 14 Feb 2025 19:27:14 +0530 Subject: [PATCH 071/114] fix: remove duplicate search components --- .../bruno-app/src/components/CodeEditor/index.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/bruno-app/src/components/CodeEditor/index.js b/packages/bruno-app/src/components/CodeEditor/index.js index 2f9ca9cdd..ed198086d 100644 --- a/packages/bruno-app/src/components/CodeEditor/index.js +++ b/packages/bruno-app/src/components/CodeEditor/index.js @@ -83,7 +83,7 @@ if (!SERVER_RENDERED) { 'bru.runner', 'bru.runner.setNextRequest(requestName)', 'bru.runner.skipRequest()', - 'bru.runner.stopExecution()', + 'bru.runner.stopExecution()' ]; CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => { const cursor = editor.getCursor(); @@ -174,11 +174,21 @@ export default class CodeEditor extends React.Component { } }, 'Cmd-F': (cm) => { + if (this._isSearchOpen()) { + // replace the older search component with the new one + const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top'); + search && search.remove(); + } cm.execCommand('findPersistent'); this._bindSearchHandler(); this._appendSearchResultsCount(); }, 'Ctrl-F': (cm) => { + if (this._isSearchOpen()) { + // replace the older search component with the new one + const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top'); + search && search.remove(); + } cm.execCommand('findPersistent'); this._bindSearchHandler(); this._appendSearchResultsCount(); @@ -365,6 +375,10 @@ export default class CodeEditor extends React.Component { } }; + _isSearchOpen = () => { + return document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top'); + }; + /** * Bind handler to search input to count number of search results */ From 51eda3f08cec6b4ccb2aba7defd546cb86634378 Mon Sep 17 00:00:00 2001 From: Tim Nikischin <49103409+nikischin@users.noreply.github.com> Date: Wed, 26 Feb 2025 08:27:33 +0100 Subject: [PATCH 072/114] Implement correct Runner title (#3854) Implement correct Runner title fixes: #3763 Use title instead of filepath in runner. --- .../src/components/RunnerResults/index.jsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/bruno-app/src/components/RunnerResults/index.jsx b/packages/bruno-app/src/components/RunnerResults/index.jsx index 4381c86b1..e8bf153a4 100644 --- a/packages/bruno-app/src/components/RunnerResults/index.jsx +++ b/packages/bruno-app/src/components/RunnerResults/index.jsx @@ -11,14 +11,14 @@ import ResponsePane from './ResponsePane'; import StyledWrapper from './StyledWrapper'; import { areItemsLoading } from 'utils/collections'; -const getRelativePath = (fullPath, pathname) => { +const getDisplayName = (fullPath, pathname, name) => { // convert to unix style path fullPath = slash(fullPath); pathname = slash(pathname); let relativePath = path.relative(fullPath, pathname); - const { dir, name } = path.parse(relativePath); - return path.join(dir, name); + const { dir } = path.parse(relativePath); + return [dir, name].filter(i => i).join('/'); }; export default function RunnerResults({ collection }) { @@ -58,7 +58,7 @@ export default function RunnerResults({ collection }) { type: info.type, filename: info.filename, pathname: info.pathname, - relativePath: getRelativePath(collection.pathname, info.pathname) + displayName: getDisplayName(collection.pathname, info.pathname, info.name) }; if (newItem.status !== 'error' && newItem.status !== 'skipped') { if (newItem.testResults) { @@ -186,7 +186,7 @@ export default function RunnerResults({ collection }) { - {item.relativePath} + {item.displayName} {item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? ( @@ -266,7 +266,7 @@ export default function RunnerResults({ collection }) {
- {selectedItem.relativePath} + {selectedItem.displayName} {selectedItem.testStatus === 'pass' ? ( @@ -275,7 +275,6 @@ export default function RunnerResults({ collection }) { )}
- {/*
{selectedItem.relativePath}
*/}
From 655eec09c15fa23c1bab455c5987a51063d1559c Mon Sep 17 00:00:00 2001 From: tlaloc911 <29415755+tlaloc911@users.noreply.github.com> Date: Wed, 26 Feb 2025 04:01:22 -0600 Subject: [PATCH 073/114] fix windows build (#4043) --- scripts/build-electron.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build-electron.js b/scripts/build-electron.js index 9825c3a09..ab44dcbdf 100644 --- a/scripts/build-electron.js +++ b/scripts/build-electron.js @@ -78,14 +78,14 @@ async function main() { console.log('The directory has been created successfully!'); // Copy build - await copyFolderIfExists('packages/bruno-app/out', 'packages/bruno-electron/web'); + await copyFolderIfExists('packages/bruno-app/dist', 'packages/bruno-electron/web'); // Change paths in next const files = await fs.readdir('packages/bruno-electron/web'); for (const file of files) { if (file.endsWith('.html')) { let content = await fs.readFile(`packages/bruno-electron/web/${file}`, 'utf8'); - content = content.replace(/\/_next\//g, '_next/'); + content = content.replace(/\/static/g, './static'); await fs.writeFile(`packages/bruno-electron/web/${file}`, content); } } From b399576dabf2067ee38cfe209b1ee0a97e527211 Mon Sep 17 00:00:00 2001 From: Jens Date: Mon, 3 Mar 2025 22:28:33 +1100 Subject: [PATCH 074/114] Update readme.md Updated link to Roadmap. --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index b0c23ecb3..68b2d49d5 100644 --- a/readme.md +++ b/readme.md @@ -138,7 +138,7 @@ Or any version control system of your choice ## Important Links 📌 - [Our Long Term Vision](https://github.com/usebruno/bruno/discussions/269) -- [Roadmap](https://github.com/usebruno/bruno/discussions/384) +- [Roadmap](https://www.usebruno.com/roadmap) - [Documentation](https://docs.usebruno.com) - [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno) - [Website](https://www.usebruno.com) From 233c57e6255560a1dafa32db04fd27562aacdcef Mon Sep 17 00:00:00 2001 From: therealrinku Date: Tue, 4 Mar 2025 12:00:39 +0545 Subject: [PATCH 075/114] feat: auto select body tab if it exists when params isnt active --- .../src/components/RequestPane/HttpRequestPane/index.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js index 09a665e9f..6cdf4a6d7 100644 --- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js @@ -15,6 +15,7 @@ import Tests from 'components/RequestPane/Tests'; import StyledWrapper from './StyledWrapper'; import { find, get } from 'lodash'; import Documentation from 'components/Documentation/index'; +import { useEffect } from 'react'; const ContentIndicator = () => { return ( @@ -111,6 +112,12 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => { requestVars.filter((request) => request.enabled).length + responseVars.filter((response) => response.enabled).length; + useEffect(() => { + if (activeParamsLength === 0 && body.mode !== 'none') { + selectTab('body'); + } + }, []); + return (
From 9d598db55eddf03fb2817ec6895b15e481cb0d72 Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Wed, 5 Mar 2025 16:04:31 +0530 Subject: [PATCH 076/114] share collection and import collection ui updates ~ added share collection option in the collection overview tab --- .../CollectionSettings/Overview/Info/index.js | 22 +++- .../src/components/Icons/OpenAPILogo/index.js | 104 ++++++++++++++++++ .../ShareCollection/StyledWrapper.js | 30 +++++ .../src/components/ShareCollection/index.js | 60 ++++++++++ .../Sidebar/Collections/Collection/index.js | 13 +-- .../Sidebar/ImportCollection/index.js | 63 ++++++++--- 6 files changed, 266 insertions(+), 26 deletions(-) create mode 100644 packages/bruno-app/src/components/Icons/OpenAPILogo/index.js create mode 100644 packages/bruno-app/src/components/ShareCollection/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ShareCollection/index.js diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js index a83850e91..efb616fdc 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js @@ -1,13 +1,20 @@ import React from "react"; import { getTotalRequestCountInCollection } from 'utils/collections/'; -import { IconFolder, IconWorld, IconApi, IconClock } from '@tabler/icons'; +import { IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons'; import { areItemsLoading, getItemsLoadStats } from "utils/collections/index"; +import { useState } from "react"; +import ShareCollection from "components/ShareCollection/index"; const Info = ({ collection }) => { const totalRequestsInCollection = getTotalRequestCountInCollection(collection); const isCollectionLoading = areItemsLoading(collection); const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection); + const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false); + + const handleToggleShowShareCollectionModal = (value) => (e) => { + toggleShowShareCollectionModal(value); + } return (
@@ -53,6 +60,19 @@ const Info = ({ collection }) => {
+ +
+
+ +
+
+
Share
+
+ Share Collection +
+
+
+ {showShareCollectionModal && }
diff --git a/packages/bruno-app/src/components/Icons/OpenAPILogo/index.js b/packages/bruno-app/src/components/Icons/OpenAPILogo/index.js new file mode 100644 index 000000000..b472b3d8c --- /dev/null +++ b/packages/bruno-app/src/components/Icons/OpenAPILogo/index.js @@ -0,0 +1,104 @@ +const OpenApiLogo = () => { + return ( + + + + + + + + + + + + + + + ); +}; + +export default OpenApiLogo; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ShareCollection/StyledWrapper.js b/packages/bruno-app/src/components/ShareCollection/StyledWrapper.js new file mode 100644 index 000000000..5e1e3be3d --- /dev/null +++ b/packages/bruno-app/src/components/ShareCollection/StyledWrapper.js @@ -0,0 +1,30 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div` + .tabs { + .tab { + padding: 6px 0px; + border: none; + border-bottom: solid 2px transparent; + margin-right: 1.25rem; + color: var(--color-tab-inactive); + cursor: pointer; + + &:focus, + &:active, + &:focus-within, + &:focus-visible, + &:target { + outline: none !important; + box-shadow: none !important; + } + + &.active { + color: ${(props) => props.theme.tabs.active.color} !important; + border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important; + } + } + } +`; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/ShareCollection/index.js b/packages/bruno-app/src/components/ShareCollection/index.js new file mode 100644 index 000000000..19f5f00be --- /dev/null +++ b/packages/bruno-app/src/components/ShareCollection/index.js @@ -0,0 +1,60 @@ +import React from 'react'; +import Modal from 'components/Modal'; +import { IconDownload } from '@tabler/icons'; +import StyledWrapper from './StyledWrapper'; +import Bruno from 'components/Bruno'; +import exportBrunoCollection from 'utils/collections/export'; +import exportPostmanCollection from 'utils/exporters/postman-collection'; +import { cloneDeep } from 'lodash'; +import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index'; + +const ShareCollection = ({ onClose, collection }) => { + const handleExportBrunoCollection = () => { + const collectionCopy = cloneDeep(collection); + exportBrunoCollection(transformCollectionToSaveToExportAsFile(collectionCopy)); + onClose(); + }; + + const handleExportPostmanCollection = () => { + const collectionCopy = cloneDeep(collection); + exportPostmanCollection(collectionCopy); + onClose(); + }; + + return ( + + +
+
+
+ +
+
+
Bruno Collection
+
Export in Bruno format
+
+
+ +
+
+ +
+
+
Postman Collection
+
Export in Postman format
+
+
+
+
+
+ ); +}; + +export default ShareCollection; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index f21f25ac3..521994afd 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -13,7 +13,6 @@ import NewRequest from 'components/Sidebar/NewRequest'; import NewFolder from 'components/Sidebar/NewFolder'; import CollectionItem from './CollectionItem'; import RemoveCollection from './RemoveCollection'; -import ExportCollection from './ExportCollection'; import { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search'; import { isItemAFolder, isItemARequest } from 'utils/collections'; @@ -22,15 +21,15 @@ import StyledWrapper from './StyledWrapper'; import CloneCollection from './CloneCollection'; import { areItemsLoading, findItemInCollection } from 'utils/collections'; import { scrollToTheActiveTab } from 'utils/tabs'; +import ShareCollection from 'components/ShareCollection/index'; const Collection = ({ collection, searchText }) => { const [showNewFolderModal, setShowNewFolderModal] = useState(false); const [showNewRequestModal, setShowNewRequestModal] = useState(false); const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false); const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false); - const [showExportCollectionModal, setShowExportCollectionModal] = useState(false); + const [showShareCollectionModal, setShowShareCollectionModal] = useState(false); const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false); - const tabs = useSelector((state) => state.tabs.tabs); const dispatch = useDispatch(); const isLoading = areItemsLoading(collection); const collectionRef = useRef(null); @@ -193,8 +192,8 @@ const Collection = ({ collection, searchText }) => { {showRemoveCollectionModal && ( setShowRemoveCollectionModal(false)} /> )} - {showExportCollectionModal && ( - setShowExportCollectionModal(false)} /> + {showShareCollectionModal && ( + setShowShareCollectionModal(false)} /> )} {showCloneCollectionModalOpen && ( setShowCloneCollectionModalOpen(false)} /> @@ -271,10 +270,10 @@ const Collection = ({ collection, searchText }) => { className="dropdown-item" onClick={(e) => { menuDropdownTippyRef.current.hide(); - setShowExportCollectionModal(true); + setShowShareCollectionModal(true); }} > - Export + Share
{ const [options, setOptions] = useState({ @@ -55,28 +58,52 @@ const ImportCollection = ({ onClose, handleSubmit }) => { } }); }; - const CollectionButton = ({ children, className, onClick }) => { - return ( - - ); - }; + return (

Select the type of your existing collection :

-
- Bruno Collection - Postman Collection - Insomnia Collection - OpenAPI V3 Spec -
+
+
+
+ +
+
+
Bruno Collection
+
Pick a Bruno collection JSON file.
+
+
+ +
+
+ +
+
+
Insomnia Collection
+
Pick a Insomnia collection JSON file.
+
+
+ +
+
+ +
+
+
Postman Collection
+
Pick a Postman collection JSON file.
+
+
+ +
+
+ +
+
+
OpenAPI v3 Collection
+
Pick an OpenAPI v3 JSON/YAML spec file.
+
+
+
{Object.entries(options || {}).map(([key, option]) => (
From 38cf2060753997b69b48a5c5d8f95c528908b1ae Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Thu, 6 Mar 2025 21:08:38 +0530 Subject: [PATCH 077/114] revert import colleciton modal ui changes --- .../Sidebar/ImportCollection/index.js | 63 ++++++------------- 1 file changed, 18 insertions(+), 45 deletions(-) diff --git a/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js b/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js index bd4d0e008..47f0f553e 100644 --- a/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js +++ b/packages/bruno-app/src/components/Sidebar/ImportCollection/index.js @@ -1,13 +1,10 @@ import React, { useState } from 'react'; -import { IconDownload } from '@tabler/icons'; import importBrunoCollection from 'utils/importers/bruno-collection'; import importPostmanCollection from 'utils/importers/postman-collection'; import importInsomniaCollection from 'utils/importers/insomnia-collection'; import importOpenapiCollection from 'utils/importers/openapi-collection'; import { toastError } from 'utils/common/error'; import Modal from 'components/Modal'; -import Bruno from 'components/Bruno/index'; -import OpenApiLogo from 'components/Icons/OpenAPILogo/index'; const ImportCollection = ({ onClose, handleSubmit }) => { const [options, setOptions] = useState({ @@ -58,52 +55,28 @@ const ImportCollection = ({ onClose, handleSubmit }) => { } }); }; - + const CollectionButton = ({ children, className, onClick }) => { + return ( + + ); + }; return (

Select the type of your existing collection :

-
-
-
- -
-
-
Bruno Collection
-
Pick a Bruno collection JSON file.
-
-
- -
-
- -
-
-
Insomnia Collection
-
Pick a Insomnia collection JSON file.
-
-
- -
-
- -
-
-
Postman Collection
-
Pick a Postman collection JSON file.
-
-
- -
-
- -
-
-
OpenAPI v3 Collection
-
Pick an OpenAPI v3 JSON/YAML spec file.
-
-
-
+
+ Bruno Collection + Postman Collection + Insomnia Collection + OpenAPI V3 Spec +
{Object.entries(options || {}).map(([key, option]) => (
From a1c133b30323d64666c936e3e3c86423d9170cea Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Thu, 6 Mar 2025 21:13:26 +0530 Subject: [PATCH 078/114] revert changes from another pr --- .../src/components/RequestPane/HttpRequestPane/index.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js index 6cdf4a6d7..09a665e9f 100644 --- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js @@ -15,7 +15,6 @@ import Tests from 'components/RequestPane/Tests'; import StyledWrapper from './StyledWrapper'; import { find, get } from 'lodash'; import Documentation from 'components/Documentation/index'; -import { useEffect } from 'react'; const ContentIndicator = () => { return ( @@ -112,12 +111,6 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => { requestVars.filter((request) => request.enabled).length + responseVars.filter((response) => response.enabled).length; - useEffect(() => { - if (activeParamsLength === 0 && body.mode !== 'none') { - selectTab('body'); - } - }, []); - return (
From 0876ad0dab87125845c71667c8ed62b6f0f3e42c Mon Sep 17 00:00:00 2001 From: lohxt1 Date: Thu, 6 Mar 2025 21:36:36 +0530 Subject: [PATCH 079/114] Revert "revert changes from another pr" This reverts commit 94dfaf45cdf497300576a24bbeda6430b8e677e4. --- .../src/components/RequestPane/HttpRequestPane/index.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js index 09a665e9f..6cdf4a6d7 100644 --- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js @@ -15,6 +15,7 @@ import Tests from 'components/RequestPane/Tests'; import StyledWrapper from './StyledWrapper'; import { find, get } from 'lodash'; import Documentation from 'components/Documentation/index'; +import { useEffect } from 'react'; const ContentIndicator = () => { return ( @@ -111,6 +112,12 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => { requestVars.filter((request) => request.enabled).length + responseVars.filter((response) => response.enabled).length; + useEffect(() => { + if (activeParamsLength === 0 && body.mode !== 'none') { + selectTab('body'); + } + }, []); + return (
From 253cb8b315afb169502ed70bf79b83ac5e784bd2 Mon Sep 17 00:00:00 2001 From: Anoop M D Date: Sat, 8 Mar 2025 17:19:29 +0530 Subject: [PATCH 080/114] chore: updated share collection color theme --- .../components/CollectionSettings/Overview/Info/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js index efb616fdc..751919cde 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js @@ -62,12 +62,12 @@ const Info = ({ collection }) => {
-
- +
+
Share
-
+
Share Collection
From 51c86bc0e9942e44e566f1a044e0ff7cf467ee72 Mon Sep 17 00:00:00 2001 From: sanish-bruno Date: Wed, 19 Feb 2025 11:28:41 +0530 Subject: [PATCH 081/114] Added UI to manage cookies --- package-lock.json | 14 + packages/bruno-app/package.json | 2 + .../src/components/Accordion/index.js | 62 +++ .../src/components/Accordion/styledWrapper.js | 29 ++ .../ModifyCookieModal/StyledWrapper.js | 5 + .../Cookies/ModifyCookieModal/index.js | 359 ++++++++++++++++++ .../src/components/Cookies/StyledWrapper.js | 41 ++ .../bruno-app/src/components/Cookies/index.js | 353 +++++++++++++++-- .../components/ToggleSwitch/StyledWrapper.js | 91 +++++ .../src/components/ToggleSwitch/index.js | 15 + .../src/providers/ReduxStore/slices/app.js | 38 ++ packages/bruno-electron/src/ipc/collection.js | 52 ++- packages/bruno-electron/src/utils/cookies.js | 115 +++++- 13 files changed, 1133 insertions(+), 43 deletions(-) create mode 100644 packages/bruno-app/src/components/Accordion/index.js create mode 100644 packages/bruno-app/src/components/Accordion/styledWrapper.js create mode 100644 packages/bruno-app/src/components/Cookies/ModifyCookieModal/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/Cookies/ModifyCookieModal/index.js create mode 100644 packages/bruno-app/src/components/ToggleSwitch/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ToggleSwitch/index.js diff --git a/package-lock.json b/package-lock.json index 8d63dedff..6e5aa5ec4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17161,6 +17161,18 @@ "node": "*" } }, + "node_modules/moment-timezone": { + "version": "0.5.47", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.47.tgz", + "integrity": "sha512-UbNt/JAWS0m/NJOebR0QMRHBk0hu03r5dx9GK8Cs0AS3I81yDcOc9k+DytPItgVvBP7J6Mf6U2n3BPAacAV9oA==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/mousetrap": { "version": "1.6.5", "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz", @@ -24446,6 +24458,8 @@ "lodash": "^4.17.21", "markdown-it": "^13.0.2", "markdown-it-replace-link": "^1.2.0", + "moment": "^2.30.1", + "moment-timezone": "^0.5.47", "mousetrap": "^1.6.5", "nanoid": "3.3.8", "path": "^0.12.7", diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index f3e6fe292..224bb24df 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -47,6 +47,8 @@ "lodash": "^4.17.21", "markdown-it": "^13.0.2", "markdown-it-replace-link": "^1.2.0", + "moment": "^2.30.1", + "moment-timezone": "^0.5.47", "mousetrap": "^1.6.5", "nanoid": "3.3.8", "path": "^0.12.7", diff --git a/packages/bruno-app/src/components/Accordion/index.js b/packages/bruno-app/src/components/Accordion/index.js new file mode 100644 index 000000000..db1f465c7 --- /dev/null +++ b/packages/bruno-app/src/components/Accordion/index.js @@ -0,0 +1,62 @@ +import React, { createContext, useContext, useState } from 'react'; +import { IconChevronDown } from '@tabler/icons'; +import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrapper'; + +const AccordionContext = createContext(); + +const Accordion = ({ children, defaultIndex }) => { + const [openIndex, setOpenIndex] = useState(defaultIndex); + + const toggleItem = (index) => { + setOpenIndex(openIndex === index ? null : index); + }; + + return ( + +
{children}
+
+ ); +}; + +const Item = ({ index, children, ...props }) => { + return ( + + {React.Children.map(children, (child) => React.cloneElement(child, { index }))} + + ); +}; + +export const Header = ({ index, children, ...props }) => { + const { openIndex, toggleItem } = useContext(AccordionContext); + const isOpen = openIndex === index; + + return ( + toggleItem(index)} {...props}> +
{children}
+ + +
+ ); +}; + +const Content = ({ index, children, ...props }) => { + const { openIndex } = useContext(AccordionContext); + const isOpen = openIndex === index; + + return ( + + {children} + + ); +}; + +Accordion.Item = Item; +Accordion.Header = Header; +Accordion.Content = Content; +export default Accordion; diff --git a/packages/bruno-app/src/components/Accordion/styledWrapper.js b/packages/bruno-app/src/components/Accordion/styledWrapper.js new file mode 100644 index 000000000..c09ed6f06 --- /dev/null +++ b/packages/bruno-app/src/components/Accordion/styledWrapper.js @@ -0,0 +1,29 @@ +import styled from 'styled-components'; + +const AccordionItem = styled.div` + border: 1px solid ${(props) => props.theme.input.border}; + border-radius: 4px; + overflow: hidden; + margin-bottom: 1rem; +`; + +const AccordionHeader = styled.button` + width: 100%; + display: flex; + padding: 1rem; + background: transparent; + cursor: pointer; + font-weight: 500; + + &:hover { + background-color: ${(props) => props.theme.plainGrid.hoverBg}; + } +`; + +const AccordionContent = styled.div` + padding: ${(props) => (props.isOpen ? '1rem' : '0')}; + max-height: ${(props) => (props.isOpen ? 'auto' : '0')}; + transition: all 0.2s ease-in-out; +`; + +export { AccordionItem, AccordionHeader, AccordionContent }; diff --git a/packages/bruno-app/src/components/Cookies/ModifyCookieModal/StyledWrapper.js b/packages/bruno-app/src/components/Cookies/ModifyCookieModal/StyledWrapper.js new file mode 100644 index 000000000..ec278887d --- /dev/null +++ b/packages/bruno-app/src/components/Cookies/ModifyCookieModal/StyledWrapper.js @@ -0,0 +1,5 @@ +import styled from 'styled-components'; + +const StyledWrapper = styled.div``; + +export default StyledWrapper; diff --git a/packages/bruno-app/src/components/Cookies/ModifyCookieModal/index.js b/packages/bruno-app/src/components/Cookies/ModifyCookieModal/index.js new file mode 100644 index 000000000..9e236d736 --- /dev/null +++ b/packages/bruno-app/src/components/Cookies/ModifyCookieModal/index.js @@ -0,0 +1,359 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { useFormik } from 'formik'; +import * as Yup from 'yup'; +import Modal from 'components/Modal/index'; +import { modifyCookie, addCookie, getParsedCookie, createCookieString } from 'providers/ReduxStore/slices/app'; +import { useDispatch } from 'react-redux'; +import toast from 'react-hot-toast'; +import ToggleSwitch from 'components/ToggleSwitch/index'; +import { IconInfoCircle } from '@tabler/icons'; +import moment from 'moment'; +import 'moment-timezone'; +import { Tooltip } from 'react-tooltip'; + +const removeEmptyValues = (obj) => { + return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== null && value !== undefined)); +}; + +const ModifyCookieModal = ({ onClose, domain, cookie }) => { + const dispatch = useDispatch(); + const [isRawMode, setIsRawMode] = useState(false); + const [cookieString, setCookieString] = useState(''); + const initialParseRef = useRef(false); + + const formik = useFormik({ + enableReinitialize: true, + initialValues: { + ...(cookie ? cookie : {}), + key: cookie?.key || '', + value: cookie?.value || '', + path: cookie?.path || '/', + domain: cookie?.domain || domain || '', + expires: cookie?.expires ? moment(cookie.expires).format(moment.HTML5_FMT.DATETIME_LOCAL) : '', + secure: cookie?.secure || false, + httpOnly: cookie?.httpOnly || false + }, + validationSchema: Yup.object({ + key: Yup.string().required('Key is required'), + value: Yup.string().required('Value is required'), + domain: Yup.string().required('Domain is required'), + secure: Yup.boolean(), + httpOnly: Yup.boolean(), + expires: Yup.mixed() + .nullable() + .transform((value) => { + if (!value || value === '') return null; + return moment(value).isValid() ? moment(value).toDate() : null; + }) + .test('future-date', 'Expiration date must be in the future', (value) => { + if (!value) return true; + return moment(value).isAfter(moment()); + }) + }), + onSubmit: (values) => { + const modValues = removeEmptyValues({ + ...(cookie ? cookie : {}), + ...values, + expires: values.expires + ? moment(values.expires).isValid() + ? moment(values.expires).toDate() + : Infinity + : Infinity + }); + + handleCookieDispatch(cookie, domain, modValues, onClose); + } + }); + + const title = cookie ? 'Modify Cookie' : 'Add Cookie'; + + const handleCookieDispatch = (cookie, domain, modValues, onClose) => { + if (cookie) { + dispatch(modifyCookie(domain, cookie, cookie.path, cookie.key, modValues)) + .then(() => { + toast.success('Cookie modified successfully'); + onClose(); + }) + .catch((err) => { + toast.error('An error occurred while modifying cookie'); + console.error(err); + }); + } else { + dispatch(addCookie(domain, modValues)) + .then(() => { + toast.success('Cookie added successfully'); + onClose(); + }) + .catch((err) => { + toast.error('An error occurred while adding cookie'); + console.error(err); + }); + } + }; + + const onSubmit = async () => { + try { + if (isRawMode) { + const cookieObj = await dispatch(getParsedCookie(cookieString)); + + const modifiedCookie = removeEmptyValues({ + ...formik.values, + ...cookieObj, + expires: cookieObj?.expires + ? moment(cookieObj.expires).isValid() + ? moment(cookieObj.expires).toDate() + : Infinity + : Infinity + }); + + if (!cookieObj) { + toast.error('Please enter a valid cookie string'); + return; + } + + formik.setValues( + (values) => ({ + ...values, + ...modifiedCookie, + expires: + modifiedCookie?.expires && moment(modifiedCookie.expires).isValid() + ? moment(new Date(modifiedCookie.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL) + : '' + }), + true + ); + + handleCookieDispatch(cookie, domain, modifiedCookie, onClose); + } else { + formik.handleSubmit(); + } + } catch (error) { + const errMsg = error.message || 'An error occurred while parsing cookie string'; + toast.error(errMsg); + } + }; + + useEffect(() => { + if (!isRawMode) return; + const loadCookieString = async () => { + if (cookie) { + const str = await dispatch(createCookieString(cookie)); + setCookieString(str); + } + return ''; + }; + + loadCookieString(); + }, [cookie, isRawMode]); + + // create the cookieString when raw mode is enabled + useEffect(() => { + if (isRawMode) { + const createCookieStr = async () => { + const str = await dispatch(createCookieString(formik.values)); + setCookieString(str); + }; + + createCookieStr(); + } + }, [isRawMode, formik.values]); + + useEffect(() => { + // Reset the ref when raw mode changes + if (isRawMode) { + initialParseRef.current = false; + return; + } + + const setParsedCookie = async () => { + if (!isRawMode && cookieString && !initialParseRef.current) { + try { + const cookieObj = await dispatch(getParsedCookie(cookieString)); + + if (!cookieObj) return; + + initialParseRef.current = true; + + formik.setValues( + (values) => ({ + ...values, + ...cookieObj, + expires: + cookieObj?.expires && moment(cookieObj.expires).isValid() + ? moment(new Date(cookieObj.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL) + : '' + }), + true + ); + } catch (error) { + const errMsg = error.message || 'An error occurred while parsing cookie string'; + toast.error(errMsg); + } + } + }; + + setParsedCookie(); + }, [isRawMode, cookieString, dispatch, formik]); + + return ( + +

{title}

+
+ { + setIsRawMode(e.target.checked); + }} + /> + +
+
+ } + > +
e.preventDefault()} className="p-6"> + {isRawMode ? ( +
+
+ + + +
+
{item?.pathname?.split(`${collection?.pathname}/`)?.[1]}