Compare commits

...

175 Commits

Author SHA1 Message Date
Sanjai Kumar
536b7393db refactor: update deprecation messages for Presets and Post Response Vars (#6230)
* refactor: update DeprecationWarning component to accept children and enhance deprecation messages for Presets and Post Response Vars

* refactor: update DeprecationWarning component to use props for feature names and links, enhancing deprecation messages across various components
2025-11-28 15:29:22 +05:30
Sanjai Kumar
172479edad feat: Add deprecation warnings for Presets and Post Response Vars (#6212)
* feat: add deprecation warnings for presets and post response vars

* refactor: update deprecation warnings for presets and post response vars to include version
2025-11-26 21:14:50 +05:30
sanish chirayath
486b91894c fix: fetching reflection adds draft state in gRPC (#6218) 2025-11-26 19:51:34 +05:30
sanish chirayath
ca8ef36f9f fix: grpc messages vanishes after save if the body contains variables (#6216) 2025-11-26 18:50:10 +05:30
Chirag Chandrashekhar
7ed474c8ba fix: add Error constructors to NodeVM context for jsonwebtoken tests (#6209)
When jsonwebtoken throws errors inside the NodeVM context, those errors
were instances of the VM's isolated Error class, which caused
instanceOf(Error) checks in tests to fail.

By adding Error constructors (Error, TypeError, ReferenceError,
SyntaxError, RangeError) from the global scope to the scriptContext,
errors thrown by jsonwebtoken and other modules now use the same Error
class that tests check against, ensuring instanceOf checks pass correctly.

This fixes jsonwebtoken test failures when using the NodeVM runtime.
2025-11-26 17:57:11 +05:30
Pooja
b0405b1e1a fix: variable name validation in brunoVarInfo (#6203)
* fix: variable name validation in brunoVarInfo
2025-11-26 00:50:57 +05:30
Bijin A B
c2d000e805 fix: disallow prompts with leading or trailing spaces (#6201) 2025-11-25 16:53:05 +05:30
sanish chirayath
6aaccabc04 fix: openapi import fails when requestbody content is empty (#6200) 2025-11-25 16:19:57 +05:30
Sid
daf23c9e2d feat: add coding standards documentation (#6141) 2025-11-25 13:30:26 +05:30
Pooja
f952688032 improve: add var functions (#6175)
* improve: add var functions

* Update packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js

---------

Co-authored-by: Bijin A B <bijin@usebruno.com>
2025-11-21 21:12:06 +05:30
Bijin A B
f429fa94e3 fix(security): prototype pollution vulnerability in js-yaml (#6168) 2025-11-21 17:42:31 +05:30
Pooja
fb420fcea4 fix: preserve draft state when creating variables via varInfoToolbar (#6152)
* fix: preserve draft state when creating variables via varInfoToolbar
2025-11-21 15:47:23 +05:30
Anoop M D
cc3d6a961a Merge pull request #6169 from pooja-bruno/fix/reduce-font-size-of-tabs-text
fix: reduce font size of tab test
2025-11-21 13:41:31 +05:30
pooja-bruno
27c37192b2 fix: reduce font size of tab test 2025-11-21 12:59:27 +05:30
Bijin A B
faa2ef5de2 Merge pull request #6162 from bijin-bruno/fix/bruno-to-postman-converter
fix: sync bruno to postman converter with enterprise edition
2025-11-20 19:13:33 +05:30
Sanjai Kumar
c05d56fd21 Improve "Close All Collections" community PR (#5994)
* refactor: move CollectionsBadge to a dedicaced folder

Co-authored-by: Jérémy Munsch <github@jeremydev.ovh>
2025-11-19 22:48:30 +05:30
Pragadesh-45
b4d19ab8ca fix: push event only if exec has content (#6121) 2025-11-19 12:09:18 +05:30
Siddharth Gelera (reaper)
0cedf48e68 feat: encapsulate tab boundaries into a hook for managing pane dimensions (#5878)
* feat: implement useTabPaneBoundaries hook for managing pane dimensions

* fix: replace hardcoded divisor with constant in useTabPaneBoundaries

* chore: un-needed event calls

* fix: remove redundant import of sendRequest

* update main rediff
2025-11-19 11:30:39 +05:30
Pooja
4e7bc1a351 fix: prevent import failure for Postman collections with missing response headers (#6129) 2025-11-19 07:53:18 +05:30
Sid
9d3c8b2401 feat: Allow ctrl/cmd + click to open URLs present in codemirror (#5930)
* feat: Allow ctrl/cmd + click to open URLs present in codemirror editors (#5160)

* Allow ctrl/cmd + click to open URLs

* fix for when user does cmd+tab, then comes back without it

---------

Co-authored-by: Sid <siddharth@usebruno.com>

* Feature/cmd click on links (#5927)

fix: clean up whitespace and formatting in linkAware functions

fix rediff

Feature/cmd click on links (#6132)

* Allow ctrl/cmd + click to open URLs

* fix for when user does cmd+tab, then comes back without it

* refactored the community contribution to match Autocomplete's implementation

* updated the code to resolve issues caused during merge conflict resolution with the use of makeLinkAware

* fix: updated the code to use lodash's debounce and removed redundant undefined checks

* fix: correct debouncing test expectation in linkAware.spec.js

The test was incorrectly expecting 3 setTimeout calls when debouncing
should only result in one active timeout. Updated the test to verify
debouncing behavior correctly by checking that setTimeout is called
with the correct delay, and that only one execution happens after
the debounce delay.

* fix: fixed merge issues in linkAware.js

* fix: fixed CodeMirror assignment to this.editor

* fix: formatting fixes

* fix: formatting fix

---------

Co-authored-by: abansal21 <37215457+abansal21@users.noreply.github.com>
Co-authored-by: Chirag Chandrashekhar <chirag@usebruno.com>

---------

Co-authored-by: Arun Bansal <37215457+abansal21@users.noreply.github.com>
Co-authored-by: Chirag Chandrashekhar <chirag@usebruno.com>
2025-11-18 17:56:37 +05:30
Sid
39dfd8d360 Feature/cmd click on links (#5927)
fix: clean up whitespace and formatting in linkAware functions

fix rediff

Feature/cmd click on links (#6132)

* Allow ctrl/cmd + click to open URLs

* fix for when user does cmd+tab, then comes back without it

* refactored the community contribution to match Autocomplete's implementation

* updated the code to resolve issues caused during merge conflict resolution with the use of makeLinkAware

* fix: updated the code to use lodash's debounce and removed redundant undefined checks

* fix: correct debouncing test expectation in linkAware.spec.js

The test was incorrectly expecting 3 setTimeout calls when debouncing
should only result in one active timeout. Updated the test to verify
debouncing behavior correctly by checking that setTimeout is called
with the correct delay, and that only one execution happens after
the debounce delay.

* fix: fixed merge issues in linkAware.js

* fix: fixed CodeMirror assignment to this.editor

* fix: formatting fixes

* fix: formatting fix

---------

Co-authored-by: abansal21 <37215457+abansal21@users.noreply.github.com>
Co-authored-by: Chirag Chandrashekhar <chirag@usebruno.com>
2025-11-18 17:44:24 +05:30
Arun Bansal
460832f3ed feat: Allow ctrl/cmd + click to open URLs present in codemirror editors (#5160)
* Allow ctrl/cmd + click to open URLs

* fix for when user does cmd+tab, then comes back without it

---------

Co-authored-by: Sid <siddharth@usebruno.com>
2025-11-18 17:43:56 +05:30
Sanjai Kumar
50442d960d feat: enhance HTML report generation by including environment name (#6055) 2025-11-18 16:09:57 +05:30
Abhishek S Lal
2ac41806a2 fix: update result structure to use 'name' instead of 'suitename' in JUnit output (#6120)
* fix: update result structure to use 'name' instead of 'suitename' in JUnit output
2025-11-18 12:31:41 +05:30
Bijin A B
e9111c0529 Merge pull request #6104 from bijin-bruno/feature/prompt-vars-extended 2025-11-17 22:02:11 +05:30
Bijin Bruno
48a09f6f50 feat: enhance support for prompt variables 2025-11-17 20:12:20 +05:30
Bijin A B
e613e4cbcd Merge pull request #5975 from abhishek-bruno/fix/reorder-item-when-deleting-v2
Refactor: Enhance Request Item sequencing
2025-11-17 16:19:00 +05:30
Pooja
4631eda281 Merge pull request #6069 from pooja-bruno/feat/add-edit-variable-in-place
feat: edit variable in place
2025-11-17 16:13:09 +05:30
Abhishek S Lal
3f7ab31b2b refactor: enhance deleteItem action to handle item reordering after deletion 2025-11-17 16:05:33 +05:30
Bijin A B
27a7b623c7 Merge pull request #6039 from sanish-bruno/feat/openapi-examples
fix: import multiple types of example formats from openapi
2025-11-17 14:12:35 +05:30
sanish-bruno
95bc670d8c fix: regex 2025-11-17 13:53:24 +05:30
sanish-bruno
6d8f428140 refactor 2025-11-17 13:53:24 +05:30
sanish-bruno
ed18cb6d90 fix: improve logic for and tests 2025-11-17 13:53:24 +05:30
sanish-bruno
bb83fbfb9d fix: add schema based example 2025-11-17 13:53:24 +05:30
Sid
ddfdeda4d6 Merge pull request #6074 from usebruno/feature/http-stream-internal
Feature: HTTP Streaming
2025-11-17 13:44:53 +05:30
skewnart
adb0b90457 fix: reorder request and directory when deleting item 2025-11-17 13:40:13 +05:30
Pooja
8c7888533a feat: support newlines in headers, params, and variables (#5795)
* feat: support newlines in headers, params, and variables

* add: collectin unit test

* fix: assertion and additional header multiline

* fix: assert

* rm: useEffect for header validation

* rm: comments

* fix: already encoded url

* rm: new line changes

* handle new line in url

* fix: lint error

* add: unit test for multi line test

* change: unit test

* mv: functions in util

* fix: drag icon position

* improve: arrow height

* improvements

* rm: getKeyString from assert

* fix: single line editor

* fix: import MultiLineEditor

* import getKeyString and getValueUrl

* add: getTableCell in utils

* rm: multiline key logic

* fix

* mv: getTableCell in locators.ts
2025-11-17 13:27:00 +05:30
Sid
0a188575a0 fix: update request cancel icon 2025-11-17 13:14:09 +05:30
Chirag Chandrashekhar
2be602d16c Feature/prompt save before collection close (#6062)
* added confirmation dialog before collection close for items in draft state

* chore: lint fix

---------

Co-authored-by: Sid <siddharth@usebruno.com>
2025-11-17 12:09:12 +05:30
Chirag Chandrashekhar
8ec1925b9f feat: add variable interpolation support for WebSocket requests (#6064)
* feat: add variable interpolation support for WebSocket requests

- Add WebSocket body interpolation in interpolateVars function
- Interpolate URL, headers, and all messages in request.body.ws array with full variable context
- Refactor sendWsRequest to use main process preparation (removes duplication)
- Add mode property to wsRequest object for proper request type detection
- Ensure consistent variable precedence matching HTTP/gRPC requests
- Centralize all interpolation logic in main process via prepareWsRequest

* Add Playwright tests for WebSocket variable interpolation

- Add tests for URL interpolation (wss://echo.{{url}}.org)
- Add tests for message content interpolation ({"test": "{{data}}"})
- Update test fixtures to use wss://echo.websocket.org echo server
- Add WEBSOCKET_FLOWS.md documentation
- Refactor queueWsMessage to handle variable interpolation in main process

* removed ws flow documentation

* chore: updated the network/index.js file to reduce merge conflicts by moving around code

* fix: added collection and item to WsQueryUrl Editor to fix available variable highlight

* chore: remove unnecessary whitespace in WebSocket event handlers

---------

Co-authored-by: Sid <siddharth@usebruno.com>
2025-11-17 12:02:25 +05:30
Bobby Bonestell
d28f2f32e9 feat: add support for prompt variables in the bruno app 2025-11-14 18:57:45 +05:30
Sid
76a1532695 Merge branch 'main' into feature/http-stream-internal 2025-11-14 17:01:09 +05:30
Siddharth Gelera (reaper)
efad149afc HTTP stream enhancements (#6077)
* feat: add stop request button in api url bar

* docs: add farsi translation

* fix: handle escaped forward slashes by fast-json-format library upgrade

* refactor: change ui to use one from Websockets

* chore: cleanup

* fix: lint issues

* Replace IconPlayerStop with IconSquareRoundedX

* update json request and response formatting logic

* chore: format changes

* chore: remove un-needed diffs

* chore: sanitize

* bugfix(#5939): curl import fails for custom content-types

* chore: remove un-needed diffs

* chore: enhance response handling for streaming

* fix: disable requestid check for tests and assertions to be updated after streaming result

* chore: housekeeping

* fix: streamline loading and cancel request icon logic

* chore: formatting

* fix: multiple co-pilot changes

* fix: handle in folders

* feat: add WaitGroup utility for managing concurrent tasks

* refactor: remove WaitGroup utility and clean up network IPC logic

* refactor: remove unused setTimeout import and clean up post script execution

* refactor: clean up post-response script execution logic

* undiff

* re-align

* refactor: streamline post-response script execution

- Cleaned up formatting and improved readability of the post-response script execution logic.
- Consolidated parameters in function calls for consistency.

* fix: keep original dataBuffer for saving response

---------

Co-authored-by: adarshajit <adarshajit@gmail.com>
Co-authored-by: sajadoncode <sajadoncode@gmail.com>
Co-authored-by: lohit-bruno <lohit@usebruno.com>
Co-authored-by: Bijin A B <bijin@usebruno.com>
Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com>
Co-authored-by: Anoop M D <anoop@usebruno.com>
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
Co-authored-by: Dawid Góra <dawidgora@icloud.com>
2025-11-14 16:57:29 +05:30
Sid
2d2a17c90f Merge pull request #6070 from usebruno/feature/collection-test-results-and-filtering-internal
Feature/collection test results and filtering internal
2025-11-14 13:46:32 +05:30
Sid
3d8d93f20d fix: add missing newline at end of file in RunnerResults component 2025-11-14 13:19:39 +05:30
Bijin A B
94c33e6833 Merge pull request #5653 from sanish-bruno/feat/support-grpc-v1-reflection
feat: support v1 reflection for grpc server reflection
2025-11-13 20:12:26 +05:30
sanish-bruno
2ef451c80b rm: adding metadata to credentials 2025-11-13 19:04:34 +05:30
sanish-bruno
044fcce49f fix 2025-11-13 19:04:34 +05:30
sanish-bruno
dffb600dab fix: set metadata 2025-11-13 19:04:34 +05:30
sanish-bruno
99478b7068 fix: add metadata for insecure methods calls 2025-11-13 19:04:34 +05:30
sanish-bruno
252fd386b7 add: metadata to creds 2025-11-13 19:04:33 +05:30
sanish-bruno
b982f6db16 refactor: replace grpc-reflection-js with grpc-js-reflection-client in grpc-client implementation
rm: comment

fix: type generation

feat: implement reflection client support for gRPC v1 and v1alpha in grpc-client

refactor: simplify reflection client handling in grpc-client by removing service list retrieval

refactor: enhance reflection client return structure in grpc-client to include service list

fix: lint
2025-11-13 19:04:33 +05:30
Bijin A B
3b4e5686b8 Merge pull request #5800 from sanish-bruno/add/grpc-make-request-tests
add: tests for grpc requests
2025-11-13 18:36:11 +05:30
Bijin Bruno
2ef1a1948b chore: minor folder structure refactor 2025-11-13 18:23:34 +05:30
sanish-bruno
f2273821b0 add: tests for grpc requests
feat: add common selectors to locator.ts

fix: add dataTestId prop

update locator
2025-11-13 18:10:30 +05:30
Bijin A B
8a22f6acb8 Merge pull request #6083 from dawidgora/bugfix/5939_curl-import-fails-for-custom-content-types
bugfix(#5939): curl import fails for custom content-types
2025-11-13 17:26:49 +05:30
Chirag Chandrashekhar
6049530634 refactor: update runner tests to use new filter implementation and reusable helpers (#6085) 2025-11-13 15:56:33 +05:30
Dawid Góra
5784b04129 bugfix(#5939): curl import fails for custom content-types 2025-11-13 08:50:29 +01:00
Anoop M D
fec37f43e0 Merge pull request #5993 from adarshajit/feature/stop-request-button-in-api-url-bar
feat: add stop request button in api url bar
2025-11-13 12:14:48 +05:30
Bijin A B
b8fef7b796 Merge pull request #6079 from lohit-bruno/json_response_formatting
fix: update json request and response formatting logic
2025-11-12 23:12:40 +05:30
lohit-bruno
04f8dba1b1 update json request and response formatting logic 2025-11-12 22:47:35 +05:30
Anoop M D
cd1500bd01 Replace IconPlayerStop with IconSquareRoundedX 2025-11-12 19:48:09 +05:30
Anoop M D
e8a8b5d220 Merge pull request #6027 from sajadoncode/feature/add-farsi-readme
docs: add farsi translation
2025-11-12 19:04:11 +05:30
Pragadesh-45
bc3dfc59f6 fix: lint issues 2025-11-12 18:35:14 +05:45
Bijin A B
2c399ca33c Merge pull request #6075 from lohit-bruno/upgrade_fast_json_format_library
fix: handle `escaped forward slashes` by `fast-json-format` library upgrade
2025-11-12 18:06:09 +05:30
lohit-bruno
ccac4d6112 fix: handle escaped forward slashes by fast-json-format library upgrade 2025-11-12 16:38:35 +05:30
DaviXavier
fc5093eab4 fix: #1884 - Fixes infinite loading issue for text/event-stream requests (#4472)
* #1884 - Add support for text/event-stream content-type

* #1884 - Fix bugs with streaming

Fix bug when streaming response is not ok
Fix bug when clearing response of streaming request
Show text signaling that the response is being streamed in the reponse status
Update response size when new data is streamed in

* #1884 - Fix multiple requests when spamming send button

* #1884 - Add time counter for streamed response and fix final time

* #1884 - Run post script only at end of streamed request

* #1884 - add support for automatic "upgrade" to streaming data

* #1884 - adjustments for stopwatch in stream implementation and remove unused imports

* #1884 - fix imports indentation in useIpcEvents.js

* #1884 - remove stream data ended export function from collections

---------

Co-authored-by: Siddharth Gelera <ahoy@barelyhuman.dev>
2025-11-12 15:56:26 +05:30
Bijin A B
631b05330d Merge pull request #6071 from barelyhuman/fix/restore-duplicate-hasher
test: Add test for restoring duplicate hashes in patternHasher
2025-11-12 15:45:17 +05:30
reaper
be34c86c47 fix: replace regex with replaceAll for secure string replace 2025-11-12 15:12:51 +05:30
reaper
67c9f1373e test: Add test for restoring duplicate hashes in patternHasher 2025-11-12 14:40:30 +05:30
Sid
6628f95677 fix: add missing newline at end of file in RunnerResults component 2025-11-12 14:15:06 +05:30
Chirag Chandrashekhar
44ed0b01d8 Test Runner UI Revamp (#6011)
* Moved collection results to runner title bar so they are move visible.
Added breakdown of test results within collection.
Added filtering based on passing/failing requests and tests by click on results text.

* feat: revamp Test Runner UI with unified filter and improved layout

- Add unified filter bar (All/Passed/Failed/Skipped) with counts and active indicator
- Implement filtering that filters both requests and tests within requests
- Move action buttons to top bar, prevent filter wrapping
- Add close button and placeholder to response view
- Update styling for light/dark modes with proper colors and typography

* refactored the RunnerResults component to be more clear and readable

* refactor: revert formatting changes while preserving new UI and filtering logic

- Restore original function formatting with return statements and braces
- Restore removed input attributes (autoCorrect, autoCapitalize, spellCheck)
- Revert ternary operator changes to match original code style
- Restore original variable names (savedConfiguration) and comments
- Restore original test results rendering structure
- Preserve new filter bar UI, filtering logic, and response view improvements

* fix: implement smart auto-scroll behavior in test runner

- Only auto-scroll when user is near the bottom (within 100px)
- Preserve user's scroll position if they've scrolled up to view content
- Move ref to actual scrollable container for proper scroll detection

* Update RunnerResults component

* chore: reformat

---------

Co-authored-by: Morgan English <morgan.english@canterbury.ac.nz>
Co-authored-by: Sid <siddharth@usebruno.com>
2025-11-12 14:10:36 +05:30
morgan-se
45cfbc5c49 Moved collection results to runner title bar so they are move visible. (#3808)
Added breakdown of test results within collection.
Added filtering based on passing/failing requests and tests by click on results text.

Co-authored-by: Morgan English <morgan.english@canterbury.ac.nz>
Co-authored-by: Sid <siddharth@usebruno.com>
2025-11-12 13:48:31 +05:30
Pooja
14bece8696 feat: Add tabs component for pre-request and post-response scripts (#5926) 2025-11-12 12:53:32 +05:30
Pooja
9e19244665 move: import setting into import collection modal (#5929) 2025-11-12 11:36:21 +05:30
Pooja
f439f2de9a add: draft for collection and folder settings (#5947) 2025-11-12 11:11:12 +05:30
Bijin A B
e844d35b03 Merge pull request #6051 from abhishek-bruno/fix/postman-export-missing-tests
fix: modify brunoToPostman function to include tests in event section
2025-11-11 14:22:12 +05:30
Abhishek S Lal
26e140aca0 feat(converters): add test scripts in bruno to postman export 2025-11-11 12:56:45 +05:30
Bijin A B
7bd6a9a915 Merge pull request #6054 from barelyhuman/fix/regex-index
fix: remove global flag from TEMPLATE_VAR_PATTERN to avoid falsey matches
2025-11-10 19:25:17 +05:30
Siddharth Gelera
e1045372d5 fix: reset TEMPLATE_VAR_PATTERN lastIndex in URL validatio 2025-11-10 19:24:14 +05:30
Siddharth Gelera
914b858024 fix: update TEMPLATE_VAR_PATTERN to remove global flag 2025-11-10 18:15:57 +05:30
Siddharth Gelera
36e9a9c137 fix: reset TEMPLATE_VAR_PATTERN lastIndex in URL validation 2025-11-10 18:14:00 +05:30
Bijin A B
995899dedb Merge pull request #6053 from lohit-bruno/upgrade_fast_json_format_library
chore: `fast-json-format` library upgrade
2025-11-10 17:35:18 +05:30
lohit-bruno
408dd6bccf chore: fast-json-format library upgrade 2025-11-10 16:58:14 +05:30
Anoop M D
ab7ead91d5 chore: remove references to Golden Edition from multiple language readme files 2025-11-09 17:43:34 +05:30
Abhishek S Lal
a186df3ac4 refactor: update UI interactions and improve test stability (#6042) 2025-11-08 01:54:50 +05:30
Abhishek S Lal
3fe5299d8e fix: httpbun dependencies removed (#6041)
* fix: standardize URL formatting in insomnia test files

* feat: add mix router for handling custom redirects and cookies

* fix: add validation for redirect count to prevent infinite loops

* fix: update test URLs to use local server and add query parameters for improved testing
2025-11-07 21:55:33 +05:30
lohit
57c08350b6 fix: handle prettifying json data with bruno variables (#6038) 2025-11-07 21:07:30 +05:30
Siddharth Gelera (reaper)
68b2625259 feat(common): add patternHasher utility for hashing and restoring string from special characters (#6032)
* feat: add patternHasher utility for variable hashing

This utility function hashes input strings containing variables and allows for restoration of the original string. It includes support for custom matchers and handles cases where no variables are matched.

* Update packages/bruno-common/src/utils/template-hasher.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Sid <siddharth@usebruno.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-07 19:09:55 +05:30
Dann
1719cee440 fix: request URL input field overflow issue (#6020) 2025-11-07 16:27:11 +05:30
lohit
ab4e9eb3bd fix: use fast-json-format for prettifying json (#6026) 2025-11-07 16:20:12 +05:30
sajadoncode
b15c421270 docs: add farsi translation 2025-11-07 09:36:09 +01:00
Siddharth Gelera (reaper)
24a36bc355 fix: ensure protocolVersion is a number in WebSocket options (#6013) 2025-11-06 19:05:35 +05:30
lohit
1ac128e35c fix(bru-2079): validations for global environments get/set functions (#6009) 2025-11-06 18:12:39 +05:30
sanish chirayath
4510cc3414 fix: assert value input is disabled (#6012) 2025-11-06 17:22:27 +05:30
sanish chirayath
f3cb0d4bae Fix/example naming (#6002)
* fix: example naming

* fix: request not being saved when initialized with empty url

* remove check for method

* fix: improve the logic for get initial name

* fix test
2025-11-05 22:56:03 +05:30
lohit
7b183887ce fix(bru-2096): handle options prop for req body apis in safe mode (#6001) 2025-11-05 20:41:01 +05:30
sanish chirayath
fa28ab9b50 fix: response is unable to be copied (#5995)
* fix: response is unable to be copied

* fix: enable selection of text on readonly

* fix: no cursor when readonly
2025-11-05 20:17:09 +05:30
Abhishek S Lal
60b437ef9d fix: update test URLs having httpbin. Add redirect chain endpoint to test server (#5989)
* fix: update test URLs from httpbin to echo.usebruno.com across multiple test files

* fix: standardize URL formatting in insomnia test files

* chore: standardize URL formatting in insomnia test files
2025-11-05 20:16:07 +05:30
sanish chirayath
1ef8852a01 fix: grpc timeline crash (#5999) 2025-11-05 20:14:11 +05:30
Siddharth Gelera (reaper)
17cc70f36e fix: improve URL validation in GenerateCodeItem (#5998)
* fix: improve URL validation in GenerateCodeItem

Replaced direct URL validation with a new function to check for valid URLs and missing interpolations.

* Update packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-05 20:11:00 +05:30
DrChiodo
2deee11718 fix: restore text selection and copy in read-only CodeEditor (#5983)
Fixes #5982

Changed CodeMirror readOnly option from 'nocursor' to boolean true
to allow text selection while maintaining read-only behavior.
Removed CSS rules that prevented text selection (user-select: none)
in read-only mode.

This restores the ability to copy text from the Response panel using
Ctrl+C or right-click context menu, which was broken in nightly builds
after v2.13.2.
2025-11-05 19:17:20 +05:30
sanish chirayath
bdc8f391b7 Feat/response examples testing fixes (#5974)
* fix: cloning

feat: add response saving limit

fix: status code stays OK when i save an example

* fix: lint

* fix: newly created examples are not opening in new tab

* fix: playwright tests

* fix: use itemUId to fins item

* fix: response save
2025-11-05 19:13:21 +05:30
adarshajit
1656e951fb feat: add stop request button in api url bar 2025-11-05 17:08:19 +05:30
sanish chirayath
d8adb59d04 chore: rename crashing (#5985) 2025-11-05 14:13:49 +05:30
lohit
de05fb6137 fix(bru-1142): import environment functionality validations and fixes (#5964) 2025-11-04 18:05:25 +05:30
Kyle Bloom
3e3884a6af chore: Minimal fix for start time (#5961) 2025-11-04 18:01:50 +05:30
Bijin A B
23843bb621 fix: flaky large response test and update app preferences for few tests (#5963)
* fix: flaky large response test and update app preferences for few tests

* refactor: update createCollection function to accept sandbox mode
2025-11-02 11:35:09 +05:30
Bijin A B
c85a1ec1a5 chore: refactor few flaky tests (#5958) 2025-11-01 08:05:53 +05:30
sanish chirayath
68cbb7d9df Feat/add import export support for examples (#5936)
* feat: enhance Bru grammar to support response blocks and examples

- Added new grammar rules for response headers, status, and body types (JSON, XML, text).
- Introduced parsing logic for example blocks, allowing multiple examples with various body types.
- Implemented tests for example parsing, including edge cases and complex examples with authentication.
- Created fixture files for simple and complex examples to validate parsing functionality.

feat: extend jsonToBru functionality to support response handling and examples

- Updated jsonToBru to include parsing for response headers, status, and body types (JSON, XML, text).
- Enhanced example handling to support multiple examples with various body types.
- Added comprehensive tests for example parsing, including edge cases and complex scenarios with authentication.
- Created fixture files for testing the new features and validating parsing functionality.

move: files to fixtures folder

refactor: simplify response body handling in Bru grammar and JSON conversion

- Removed specific body type handling (JSON, XML, text) from grammar and semantics.
- Updated response body parsing in jsonToBru to handle a unified response body format.
- Adjusted tests and fixtures to reflect changes in response body structure, ensuring compatibility with the new format.

feat: add response bookmarking functionality to ResponsePane

- Introduced ResponseBookmark component to allow users to save responses as examples.
- Added NameExampleModal for naming saved examples.
- Updated ResponsePane to include the new bookmarking feature.
- Implemented Redux actions to manage response examples in the collections state.
- Enhanced CollectionItem to display saved examples and allow for expansion.

fix: remove unnecessary padding from ExampleItem component

feat: implement delete and rename functionality for examples in ExampleItem component

- Added DeleteExampleModal for confirming deletion of examples.
- Integrated modal for renaming examples with state management.
- Enhanced ExampleItem to handle example deletion and renaming through modals.
- Updated Redux actions to support example updates and deletions in the collections state.

fix: example writing to  disc properly

fix: example parsing errors

fix: request with example parsing error

fix: handle examples in collections and requests

feat: implement response example functionality in the application

- Added ResponseExample component to handle displaying and editing response examples.
- Integrated ResponseExampleRequestPane and ResponseExampleResponsePane for structured request and response handling.
- Enhanced RequestTabPanel and RequestTab components to support response-example tabs.
- Introduced new styled components for better UI/UX in response examples.
- Updated theme files to include styles for response examples.
- Implemented URL bar for editing request URLs in response examples.
- Added functionality for managing headers and parameters in response examples.
- Improved overall structure and organization of response example components.

add styles for example url bar

feat: add Checkbox component and Table-v2 for enhanced UI

- Introduced a new Checkbox component for better user interaction in forms.
- Added Table-v2 component to improve table rendering and resizing functionality.
- Updated existing components to utilize the new Checkbox and Table-v2 for managing headers and parameters in response examples.
- Enhanced styling for better visual consistency across components.
- Updated theme files to include styles for the new components.

feat: implement custom scrollbar styles for response example components

fix: features

add actions , view more

feat: enhance response example functionality

- Added GenerateCodeItem component for generating code snippets from response examples.
- Integrated modal for code generation within ResponseExample component.
- Updated ResponseExampleTopBar to handle example name and description editing.
- Improved state management for response examples, including new actions for updating names and descriptions.
- Enhanced ResponseExampleRequestPane to support editing and saving request details.
- Refactored URL handling in ResponseExampleUrlBar to utilize example-specific data.
- Improved overall user experience with better UI elements and state management.

feat: enhance response example management and UI components

feat: enhance editing capabilities in response example components

feat: update multipart form parameter handling in response examples

feat: refactor response example parameter handling and enhance UI interactions

feat: introduce RadioButton component and update Checkbox usage in response examples

fix: styles

fix radio button styling

fixed radio button styles

feat: add create example from sidebar

feat: enhance ResponseExample components with layout adjustments and new HeightBoundContainer

feat: add Checkbox and RadioButton components with comprehensive tests for rendering, user interactions, and accessibility

feat: playwright test csaes

rm: comments

fix: linting

fix: tests

refactor: update response example tests and enhance functionality

fix: tests

fix: e2e-tests

refactor: implement hasRequestChanges utility for better change detection

rm: console

rm: consoles

fix: lint

fix: tests

fix: response header disabled by default issue

Feat/with bru example parser (#5892)

* fix: response header disabled by default issue

feat: new parsing logic

fix: change test cases to accomodate new brulang

add: path params features

rm:consoles

six: make tab permanent on double click

fix width

feat: add status editing

feat: review fixes

review fixes

fix: review fixes

fix: post review

mv: test files

fix: review

* fix: lint

* fix: review comments

* fix: icons folder strcuture

fix: tests

fix: lint

fix: unit tests

feat: body mode selector

fix: close all collections

rm: example

feat added tests. lang change

feat: add custom status text

fix: status update

feat: add body mode, update tests

add default name prefilled for example

fix: active tab styles, prefilled name, text fixes

fix : pkg lock

fix: review

fix: review comments

fix: hide cursor when readonly

fix: height

fix: null body

fix: response body parsing

fix: test cases

feat: add method support for examples

fix: reponse parsing

fix: update response body type when content type is updated

rm : commented code

feat: update parser logic

fix: organize files

feat: enhance examples handling in collection export and import

feat: postman imports fro examples

feat: enhance OpenAPI import functionality to support examples

feat: support postman export

fix: postman export import

fix: open api tests, remove requestbody related logic

rm: examples

fix:  move common attributes files

ui fixes

fix: clone issue

fix: create example from request menu

review fixes

more review fixes

mv: files, fix mode req error

organize files

fix:tests

fix: save dot issue

fix: bugs

fix: postman export

fix: import path params

* chore:improve modal handling in environment and response example tests

fix: test issues resolved

* chore: update response example tests to use new fixture files and improve cleanup logic

---------

Co-authored-by: Abhishek S Lal <abhishek@usebruno.com>
Co-authored-by: Bijin Bruno <bijin@usebruno.com>
2025-11-01 05:56:11 +05:30
Abhishek S Lal
396ff2b196 test: playwright tests for create request with http, gRPC, ws, graphql (#5952)
* feat: added tests for request creation

* fix: add folder.bru

* add: common locators to locators.ts file

* refactor: update locators for request actions and improve test assertions across GraphQL, gRPC, HTTP, and WebSocket request tests

* fix: updated locator logic for folder requests.

* chore: implement cleanup logic for GraphQL, gRPC, HTTP, and WebSocket request tests

---------

Co-authored-by: sanish-bruno <sanish@usebruno.com>
Co-authored-by: Bijin Bruno <bijin@usebruno.com>
2025-10-31 22:04:35 +05:30
lohit
6826e98945 fix(bru-1447): json response formatting using fast-json-format library (#5932) 2025-10-31 20:34:36 +05:30
Siddharth Gelera (reaper)
e47d1ed353 Fix: safe serialise TypedArrays to avoid loosing constructor information (#5941)
* fix: enhance cleanJson to support serialization of typed arrays

* fix: correctness of inference based checks

* fix: remove duplicate Uint8Array reference

* fix: correct export syntax for mixinTypedArrays

Updated the export statement to use 'exports' instead of 'export' for proper module export functionality.

* chore: code cleanup

* test: add basics tests for cleanJson
2025-10-31 17:23:48 +05:30
sanish chirayath
08c182a875 Feat: support bin header in gRPC (#5869)
* Fix -bin header handling in grpc

* fix: bin-header, tests

rm: tests

rm: unused

fix: bin header

fix: test

fix: test

rm: un-necessarycode

---------

Co-authored-by: Juan Pablo Orsay <jporsay@gmail.com>
2025-10-31 17:07:12 +05:30
Abhishek S Lal
f3afb4bf84 Fix/export folder and collection level scripts (#5942)
* fix: bruno to postman export - export pre and post request scripts on folder/collection level

* refactor: removed redundant code.

* fix: lint error - file should end with a new line

---------

Co-authored-by: Jakub Sadowski <jakubsadowski08@gmail.com>
Co-authored-by: Bijin A B <bijin@usebruno.com>
2025-10-30 19:54:07 +05:30
lohit
21e8615247 fix(bru-1142): collection and global environments import and export functionality updates (#5910) 2025-10-30 19:37:44 +05:30
Abhishek S Lal
6e8751a27a Fix/client cert passphrase issues (#5898)
* fix: added interpolation, warning and syntax highlight for passphrase input

Changes:
1) When users add plain text in passphrase, warning message will be shown.
2) Passphrase will be interpolated from environment
3) Syntax highlighting for variables added.

Closes #2685

* fix: global environment variables interpolation in cert passphrase implemented.

* refactor: indentation refactoring
2025-10-30 18:14:39 +05:30
Abhishek S Lal
c9a96ee94f feat: fuzzy search for grpc methods list (#5940)
* feat: implemented fuzzy search in grpc methods

Changes:
1) New SearchInput reusable component created.
2) Search input box added in grpc methods list.
3) Fuzzy search and keyboard navigation functionality implemented.

Closes #5683

* feat: e2e test cases added for new grpc method searchbox

* fix: package-lock json update

* fix: added missing collection files for testing

* fix: fixed lint issue

* chore: update package-lock json file

* fix: improve keyboard navigation and search handling in MethodDropdown

Changes:
1) Adjusted focused index logic for ArrowUp key to remove focus after first item.
2) Enhanced handleSearchChange logic to highlight first item when search text is not empty.

* feat: implemented fuzzy search in grpc methods

Changes:
1) New SearchInput reusable component created.
2) Search input box added in grpc methods list.
3) Fuzzy search and keyboard navigation functionality implemented.

Closes #5683

* feat: e2e test cases added for new grpc method searchbox

* fix: added missing collection files for testing

* fix: fixed lint issue

* chore: update package-lock json file

* fix: improve keyboard navigation and search handling in MethodDropdown

Changes:
1) Adjusted focused index logic for ArrowUp key to remove focus after first item.
2) Enhanced handleSearchChange logic to highlight first item when search text is not empty.

* test: updated test description and some code optimisation
2025-10-30 17:56:49 +05:30
naman-bruno
b69db7b44b fix: High CPU due to WMI queries (#5924)
* fix: High CPU due to WMI queries
2025-10-30 15:32:21 +05:30
naman-bruno
73caaef42b fix: crash on viewing large responses (#5647)
* fix: crash on viewing large responses
2025-10-30 13:29:53 +05:30
Sanjai Kumar
e68b2ae3b7 feat: Import Insomnia environments (#5716)
* feat: Implement environment conversion utilities for Insomnia to Bruno migration

fix tests

fix: test

feat: updated `toBrunoEnv` and merging functions to flatten environment data using dot-notation keys. added tests for `buildV5Environments` and `buildV4Environments` to verify flattened key behavior and shallow overrides.

chore: update package-lock.json

refactor: replace `flat` library with custom `flattenObject` utility for improved environment data flattening

chore: remove package-lock.json updates

feat: update `toBrunoEnv` to convert environment values to strings and adjust tests for flattened key behavior in Insomnia environment imports

refactor: update flattening logic to use JavaScript-style square bracket notation for arrays and adjust related tests

feat: enhance insomnia-to-bruno conversion by normalizing variables in requests, and add tests for v4 and v5 environment imports

refactor: improve variable naming and streamline environment building logic in `buildV5Environments` and `buildV4Environments` functions

test: add cleanup step to environment import tests and update expected version for new feature

* revert package-lock.json changes

* test: Add data-testid attributes to environment variable rows in EnvironmentVariables component
2025-10-29 19:04:09 +05:30
Pooja
cc7f1ea58f feat: add copy and paste functionality for requests (#5907) 2025-10-29 17:24:09 +05:30
Sid
6e8cd55b76 Refactor: Change how test runner handles pageWithUserData tests (#5922)
* refactor: change how test runner opens pageWithUserData instances

* fix: test move tabs

* fix: custom ca cert tests

* fix: update file patterns and improve error messages

* fix: improve electron app launch logic

* fix: update temporary directory handling for Electron app

* fix: ensure newline at end of file in index.ts

This change adds a newline at the end of the file to comply with coding standards.

* fix: improve error handling in recursiveCopy function

- Simplified error message when source path does not exist.
- Enhanced error handling to provide clearer guidance on usage of `page` fixture.

* fix(e2e): close collections after each tests

* fix: reuse the worker instance per file instead of per user data dir

* fix: revert ssl tests as serial run is fixed

* fix: change afterEach to afterAll for cleanup

fix: change afterEach to afterAll for cleanup

---------

Co-authored-by: Bijin Bruno <bijin@usebruno.com>
2025-10-29 14:32:45 +05:30
Tanishq Singla
384aabf2af Bugfix: Error importing curl with no space in header (#5897)
* fix: error importing curl when there is no space in header
2025-10-29 14:11:46 +05:30
Abhishek S Lal
a15dcdb133 fix: removed unwanted logging of global environment variables in the console (#5904) 2025-10-29 13:57:57 +05:30
Abhishek S Lal
18848cdb26 fix: added xml-formatter package in package.json of bruno-js (#5920)
* fix: added xml-formatter package in package.json of bruno-js

* fix: package lock json update
2025-10-28 18:42:34 +05:30
Siddharth Gelera (reaper)
29b90a7e0d Fix: Multi sub protocol support for web sockets (#5903)
* fix: manually split sub proto for `ws` compat

* feat: force persist multi map for protocols

* fix: remove unnecessary pause in subprotocol tests
2025-10-28 11:34:24 +05:30
Bijin A B
4fbe371eb0 fix: revert increasing playwright worker count (#5906)
* fix: revert increasing playwright worker count
2025-10-27 22:54:54 +05:30
Pragadesh-45
6fd2b8be6d fix: enhance cleanup process in MaskedEditor by adding destroy method and improving disable logic (#5748) 2025-10-27 18:00:52 +05:30
Chirag Chandrashekhar
be7f92d77f Resolved issue: https://github.com/usebruno/bruno/issues/4672 (#5900)
- Added interpolation to setVar method's value field.
- Added playwright test to test the fix.
- Added jest test to test out the fix.

---
Playwright - PASS
Jest - PASS
---
2025-10-27 16:26:49 +05:30
Sanjai Kumar
c5325c732f feat: enhance environment variable persistence handling (#5783)
* feat: enhance environment variable persistence handling

* feat: experiment playwright with multiple workers

---------

Co-authored-by: Bijin Bruno <bijin@usebruno.com>
2025-10-25 19:02:45 +05:30
Anton
a538b27f24 Import WSDL to collection (#5015)
* Import WSDL to bruno collection

* feat(wsdl-import): remove unused code and minor refactor

---------

Co-authored-by: Bijin Bruno <bijin@usebruno.com>
2025-10-25 15:20:18 +05:30
Anoop M D
77bb8f40fe Update readme.md (#5883)
Fix broken anchors for contribute and git headers.
2025-10-24 17:56:21 +05:30
kosarinin
8f1f5e3861 Update readme.md
Fix broken anchors for contribute and git headers.
2025-10-24 15:12:40 +03:00
Bijin A B
e9251a1f3f fix: add missing jsonwebtoken in package-lock (#5882) 2025-10-24 17:25:11 +05:30
Sid
3a011b2a18 Merge pull request #5881 from usebruno/bugfix/incorrect-space-encode-internal
Fixes: Generate Code does urlencoding twice
2025-10-24 17:17:40 +05:30
Chirag Chandrashekhar
fa1498e2a8 Bugfix/incorrect space encode (#5870)
* Fix the space encoding issue

* fix: incorrect space encoding

Fixed an issue in Code Generation for requests. The original fix was
raised in [PR](https://github.com/usebruno/bruno/pull/4478). The current
PR fixes some merge conflicts and resolves some unimported dependencies
error.

* test: add URL encoding tests for code generation feature

Add Playwright tests to verify proper URL encoding behavior in Bruno's
code generation dialog for both encoded and unencoded query parameters.

* moved the test script inside request

* updated the snippet generation code to reuse code and reduce redundancy

* removed redundant code and reverted autoformat

* reverted some auto formatted changes

* reverting format during commit hook

* chore: reset formatting

* chore: reformat

---------

Co-authored-by: Vipin Sundar <86339268+vipin-sundar@users.noreply.github.com>
Co-authored-by: Chirag Chandrashekhar <chiragchan@Chirags-MacBook-Air.local>
Co-authored-by: Sid <siddharth@usebruno.com>

chore: reformat
2025-10-24 16:29:22 +05:30
Siddharth Gelera (reaper)
77681ca51e fix: inherit vars and headers from the collection (#5876)
* fix: inherit vars and headers from the collection
2025-10-24 15:08:10 +05:30
Vipin Sundar
045141efaf Fix the space encoding issue (#4478) 2025-10-23 15:27:19 +05:30
anusree-bruno
c997b91698 added jsonwebtoken as inbuilt library (#5535)
* added jsonwebtoken as inbuilt library

* removed bundling

* handle callback in quickjs

* chore: tests folder restructure

* chore: lint fix

---------

Co-authored-by: Sid <siddharth@usebruno.com>
2025-10-22 14:57:19 +05:30
anusree-bruno
986d5b0b2a moved custom search to components folder (#5750)
* moved custom search to components folder

* renamed custom search

---------

Co-authored-by: Sid <siddharth@usebruno.com>
2025-10-22 14:56:57 +05:30
ganesh
a2a521477a add fix for runtime var color (#4254)
* added new changes

* adds color to light and dark theme file

* import theme obj and use variable runtime color

* fix: operator linebreak style for eslint

* chore: remove un-needed changes

---------

Co-authored-by: Sid <siddharth@usebruno.com>
2025-10-22 14:23:12 +05:30
Siddharth Gelera (reaper)
8e70adcbf9 fix: incomplete tests (#5824)
* fix: close support modal for other tests to reuse the window properly

* Update support-links.spec.js

* chore: reformat

---------

Co-authored-by: Sid <siddharth@usebruno.com>
2025-10-22 13:39:36 +05:30
Prasanth Baskar
87296776fa add arch linux install to readme (#4569)
Signed-off-by: bupd <bupdprasanth@gmail.com>
2025-10-19 03:19:16 +05:30
Alex
9df70cd759 Merge pull request #5809 from 0x416c6578/feature/minify-json-xml
Add `bru.utils.minifyXml` and `bru.utils.minifyJson`
2025-10-19 01:29:32 +05:30
Anoop M D
8f9fb3b3c9 Merge pull request #5163 from josbiz/fix--dot-on-proxy-options-when-unused
fix: dot on unused proxy settings
2025-10-19 01:15:21 +05:30
Anoop M D
6d018f5648 Merge pull request #5164 from josbiz/fix--dot-now-showed-on-used-preset-setting
fix: show dot on used preset setting
2025-10-19 01:14:00 +05:30
Anoop M D
789d0b23c0 added option to revert changes (#4503) 2025-10-19 01:09:07 +05:30
anusree-bruno
81e1e403e4 handle options in getBody for QuickJS VM (#4614) 2025-10-19 01:02:30 +05:30
Sanjai Kumar
ad2add4026 Added tests for replacing invalid variable characters in Postman collection Env (#4634)
---------

Co-authored-by: sanjai0py <sanjailucifer666@gmail.com>
Co-authored-by: Anoop M D <anoop@usebruno.com>
2025-10-19 01:00:19 +05:30
naman-bruno
02554c3ad9 Merge pull request #4279 from naman-bruno/feat/apikey-codegen
Add: API Key auth in code generator
2025-10-19 00:53:58 +05:30
Anoop M D
62815e3429 Merge pull request #5008 from anusree-bruno/feat/add-process-env-vars-to-gql-introspection
add process.env variable support to GraphQL introspection
2025-10-18 23:15:51 +05:30
Anoop M D
9859b69559 Merge pull request #5113 from apealpha/bugfix/3019-prettify-json-with-variables
fix(request): prettify JSON with variables
2025-10-18 22:51:10 +05:30
Anoop M D
440c688bbb Merge pull request #4708 from pooja-bruno/improve/use-common-getTreePathFromCollectionToItem-function
improve: use common getTreePathFromCollectionToItem function
2025-10-18 22:44:08 +05:30
Anoop M D
416eb754b7 Merge pull request #4747 from ZieglerZhu/bugfix/update-readme-cn
docs(readme): update readme_cn.md
2025-10-18 18:00:11 +05:30
Anoop M D
b85d6efa60 Merge pull request #5303 from usebruno/dependabot/github_actions/actions/checkout-5
build(deps): bump actions/checkout from 4 to 5
2025-10-18 16:49:35 +05:30
Blake Guilloud
19dea18629 Merge pull request #5829 from BlakeGuilloud/bugfix/5823-saving-url-in-response-pane
Bugfix/5823 saving url in response pane
2025-10-18 16:39:21 +05:30
Abhishek S Lal
636901c23d fix: resolve global env variable becoming undefined on script execution (#5816)
* fix: resolve global env variable becoming undefined on script execution

Fixes an issue where global disabled environment variables were becoming undefined during request execution when the pre request script is non-empty.

The update ensures that global variables persist as expected and are correctly referenced throughout the request lifecycle.

Closes #5772.

* feat: added test for checking proper global env update through scripts

* refactor: updated comments for more readability and added a new data-testid in modal.
2025-10-17 21:50:50 +05:30
lohit
a4b1941817 fix(bru-2035): form-urlencoded logic updates (#5820) 2025-10-17 18:22:43 +05:30
Sid
7d8fde9180 fix: improve URL parsing in getParsedWsUrlObject (#5822) 2025-10-17 18:15:15 +05:30
Anoop M D
4197304bf9 Merge pull request #5679 from mheidinger/visual-gql-indicator
feat: add visual indicator for GQL requests
2025-10-17 15:00:37 +05:30
Max Heidinger
b75422a010 feat: add visual indicator for GQL requests 2025-10-17 10:25:54 +02:00
Pragadesh-45
e9f03c46c7 tests: add tests for URN parsing (#5819) 2025-10-17 10:58:26 +05:30
Pragadesh-45
73e828621f fix: enhance URL parameter parsing and interpolation logic (#5812)
* fix: enhance URL parameter parsing and interpolation logic
2025-10-16 17:58:53 +05:30
Siddharth Gelera (reaper)
2becf49542 fix: harden type checks for buildFormUrlEncodedPayload (#5811) 2025-10-16 13:31:13 +05:30
dependabot[bot]
e57162b79a build(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 17:10:22 +00:00
Jose Bolivar Ibz
1de9203dd5 Merge branch 'main' into fix--dot-on-proxy-options-when-unused 2025-08-29 08:45:44 -07:00
Jose Bolivar Ibz
cffa37ed50 fix: show dot on used preset setting 2025-07-22 12:17:19 -07:00
Jose Bolivar Ibz
bcf61f507a Update index.js
Variable change from let to const
2025-07-22 12:10:14 -07:00
Jose Bolivar Ibz
325b573da9 fix: dot on unused proxy settings 2025-07-22 11:51:02 -07:00
apealpha
4b5c7dcca6 fix(request): prettify JSON with variables 2025-07-16 00:00:26 +09:00
anusree-bruno
d2888daa88 add process.env variable support to GraphQL introspection 2025-06-30 13:12:35 +05:30
ZieglerZhu
ec9d63219f docs(readme): update readme_cn.md
- Added introduction of commercial version
- Added Table of Contents
- Updated installation guide via package managers
- Updated important links
- Adjusted document structure, optimized title hierarchy
2025-05-22 23:38:33 +08:00
pooja-bruno
9173ffbdee improve: use common getTreePathFromCollectionToItem function 2025-05-19 12:42:10 +05:30
anusree-bruno
5f112a318d added option to revert changes 2025-04-15 00:11:41 +05:30
661 changed files with 37561 additions and 3862 deletions

66
.coderabbit.yaml Normal file
View File

@@ -0,0 +1,66 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: 'en-US'
early_access: false
tone_instructions: 'You are an expert code reviewer in TypeScript, JavaScript, NodeJS, and ElectronJS. You work in an enterprise software developer team, providing concise and clear code review advice. You only elaborate or provide detailed explanations when requested.'
knowledge_base:
opt_out: false
code_guidelines:
enabled: true
filePatterns:
- '**/CODING_STANDARDS.md'
reviews:
profile: 'chill'
request_changes_workflow: false
high_level_summary: true
poem: true
review_status: true
collapse_walkthrough: false
auto_review:
enabled: true
drafts: false
base_branches: ['main', 'release/*']
path_instructions:
- path: 'tests/**/**.*'
instructions: |
Review the following e2e test code written using the Playwright test library. Ensure that:
- Follow best practices for Playwright code and e2e automation
- Try to reduce usage of `page.waitForTimeout();` in code unless absolutely necessary and the locator cannot be found using existing `expect()` playwright calls
- Avoid using `page.pause()` in code
- Use locator variables for locators
- Avoid using test.only
- Use multiple assertions
- Promote the use of `test.step` as much as possible so the generated reports are easier to read
- Ensure that the `fixtures` like the collections are nested inside the `fixtures` folder
**Fixture Example***: Here's an example of possible fixture and test pair
```
.
├── fixtures
│ └── collection
│ ├── base.bru
│ ├── bruno.json
│ ├── collection.bru
│ ├── ws-test-request-with-headers.bru
│ ├── ws-test-request-with-subproto.bru
│ └── ws-test-request.bru
├── connection.spec.ts # <- Depends on the collection in ./fixtures/collection
├── headers.spec.ts
├── persistence.spec.ts
├── variable-interpolation
│ ├── fixtures
│ │ └── collection
│ │ ├── environments
│ │ ├── bruno.json
│ │ └── ws-interpolation-test.bru
│ ├── init-user-data
│ └── variable-interpolation.spec.ts # <- Depends on the collection in ./variable-interpolation/fixtures/collection
└── subproto.spec.ts
```
chat:
auto_reply: true

View File

@@ -25,7 +25,7 @@ jobs:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'

View File

@@ -15,7 +15,7 @@ jobs:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
@@ -44,7 +44,7 @@ jobs:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps
@@ -73,7 +73,7 @@ jobs:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Setup Node Dependencies
uses: ./.github/actions/common/setup-node-deps

View File

@@ -13,7 +13,7 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
@@ -66,7 +66,7 @@ jobs:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
@@ -108,7 +108,7 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: v22.11.x

44
CODING_STANDARDS.md Normal file
View File

@@ -0,0 +1,44 @@
# Bruno Coding Standards
- No diffs unless an actual change is made, the code changes need to be as minimal as possible, avoid making un-necessary whitespace diffs. This is already handled by eslint but make sure you check your code changes before commiting and raising a PR.
## General Style Rules
- Use 2 spaces for indentation. No tabs, just spaces keeps everything neat and uniform.
- Stick to single quotes for strings. Double quotes are cool elsewhere, but here we go single.
- Always add semicolons at the end of statements. It's like putting a period at the end of a sentence clarity matters.
- JSX is enabled, so feel free to use it where it makes sense.
## Punctuation and Spacing
- No trailing commas. Keep it clean, no extra commas hanging around.
- Always use parentheses around parameters in arrow functions. Even for single params consistency is key.
- For multiline constructs, put opening braces on the same line, and ensure consistency. Minimum 2 elements for multiline.
- No newlines inside function parentheses. Keep 'em tight.
- Space before and after the arrow in arrow functions. `() => {}` is good.
- No space between function name and parentheses. `func()` not `func ()`.
- Semicolons go at the end of the line, not on a new line.
- No strict max length write readable code, not cramped lines.
- Multiple expressions per line in JSX are fine flexibility is nice.
Remember, these rules are here to make our codebase harmonious. If something doesn't fit perfectly, let's chat about it. Happy coding! 🚀
## Readability and Abstractions
- Avoid abstractions unless the exact same code is being used in more than 3 places.
- Names for functions need to be concise and descriptive.
- Add in JSDoc comments to add more details to the abstractions if needed.
- Follow functional programming but just enough to be readable, we don't need to go as deep as ADTs and Monads, we want to keep the code pipeline obvious and easy for everyone to read and contribute to.
- Avoid single line abstractions where all that's being done is increasing the call stack with one additional function.
- Add in meaningful comments instead of obvious ones where complex code flow is explained properly.

View File

@@ -16,6 +16,7 @@
| [日本語](docs/contributing/contributing_ja.md)
| [हिंदी](docs/contributing/contributing_hi.md)
| [Dutch](docs/contributing/contributing_nl.md)
| [فارسی](docs/contributing/contributing_fa.md)
## Let's make Bruno better, together!!
@@ -74,6 +75,7 @@ npm run build:bruno-filestore
# bundle js sandbox libraries
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
```
##### Option 2
```bash
@@ -94,18 +96,22 @@ npm run dev:electron
```
##### Option 2
```bash
# run electron and react app concurrently
npm run dev
```
#### Customize Electron `userData` path
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
e.g.
```sh
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
```
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
### Troubleshooting

View File

@@ -0,0 +1,92 @@
[English](../../contributing.md)
## با هم، Bruno را بهتر می‌کنیم!
خوشحالم که قصد دارید Bruno را بهبود ببخشید. در ادامه قوانین و راهنماها برای راه‌اندازی Bruno روی سیستم شما آورده شده است.
### فناوری‌های استفاده‌شده
به فارسی برونو Bruno با استفاده از Next.js و React ساخته شده است. همچنین از Electron برای بسته‌بندی نسخه دسکتاپ (که امکان مجموعه‌های محلی را فراهم می‌کند) استفاده می‌کنیم.
کتابخانه‌هایی که استفاده می‌کنیم:
- CSS - Tailwind استایل
- Codemirror - ویرایشگر کد
- Redux - مدیریت وضعیت
- Tabler Icons - آیکون‌ها
- formik - فرم‌ها
- Yup اعتبارسنجی اسکیمـا
- axios - کلاینت درخواست
- chokidar - پایش‌گر سیستم فایل
### پیش‌نیازها
شما به [نود v20.x یا اخرین نسخه پایدار](https://nodejs.org/en/) و npm 8.x نیاز دارید. در این پروژه از فضای کاری npm (npm workspaces) استفاده می‌کنیم.
### شروع به کدنویسی
برای راه‌اندازی محیط توسعه محلی به فایل [مستندات توسعه](docs/development_fa.md) مراجعه کنید:
### ارسال Pull Request
1 - لطفاً Pull Requestها (PR) را کوتاه و متمرکز نگه دارید و تنها یک هدف مشخص را دنبال کنند. </br>
2 - لطفاً از فرمت نام‌گذاری شاخه‌ها استفاده کنید:
- feature/[name]: این شاخه باید شامل یک قابلیت مشخص باشد.
- feature/dark-mode : مثال
- bugfix/[name]: این شاخه باید تنها شامل رفع یک باگ مشخص باشد.
- bugfix/bug-1 : مثال
## توسعه
به فارسی برونو یا Bruno به‌صورت یک اپلیکیشن «سنگین» توسعه داده می‌شود. برای اجرا باید ابتدا Next.js را در یک پنجره ترمینال اجرا کنید و سپس اپلیکیشن Electron را در پنجره ترمینال دیگری راه‌اندازی نمایید.
### نیازمندی توسعه
- NodeJS v18
### اجرای محلی
```bash
# از ورژن NodeJS 18 استفاده کنید
nvm use
# نصب وابستگی‌ها
npm i --legacy-peer-deps
# ساخت مستندات GraphQL
npm run build:graphql-docs
# ساخت bruno-query
npm run build:bruno-query
# اجرای اپ Next (ترمینال 1)
npm run dev:web
# اجرای اپ Electron (ترمینال 2)
npm run dev:electron
```
### عیب‌یابی
ممکن است هنگام اجرای `npm install` خطای `Unsupported platform` ببینید. برای رفع این مشکل، پوشه `node_modules` و فایل `package-lock.json` را حذف کرده و سپس دوباره `npm install` را اجرا کنید. این کار معمولاً همه پکیج‌های لازم را نصب می‌کند.
```shell
# حذف پوشه node_modules در زیردایرکتوری‌ها
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# حذف فایل package-lock.json در زیردایرکتوری‌ها
find . -type f -name "package-lock.json" -delete
```
### تست‌ها
```bash
# اجرای تست‌های schema مربوط به bruno
npm test --workspace=packages/bruno-schema
# اجرای تست‌ها در همه فضاهای کاری (در صورت وجود)
npm test --workspaces --if-present
```

View File

@@ -0,0 +1,8 @@
[English](../../publishing.md)
### انتشار Bruno در یک پکیج منیجر جدید
اگرچه کد ما متن‌باز است و همه می‌توانند از آن استفاده کنند، لطفاً قبل از انتشار Bruno در مدیر بسته‌های جدید با ما تماس بگیرید. به عنوان سازنده Bruno، علامت تجاری `Bruno` را برای این پروژه دارم و مایلم توزیع آن را مدیریت کنم. اگر دوست دارید Bruno را در یک مدیر بسته جدید ببینید، لطفاً یک issue در گیت‌هاب ثبت کنید.
اگرچه بیشتر قابلیت‌های ما رایگان و متن‌باز هستند (شامل REST و GraphQL Apis)،
ما تلاش می‌کنیم بین اصول متن‌باز و توسعه پایدار تعادل مناسبی برقرار کنیم - https://github.com/usebruno/bruno/discussions/269

View File

@@ -41,13 +41,6 @@
![bruno](/assets/images/landing-2.png) <br /><br />
### الطبعة الذهبية ✨
غالبية ميزاتنا مجانية ومفتوحة المصدر.
نحن نسعى لتحقيق توازن متناغم بين [مبادئ الشفافية والاستدامة](https://github.com/usebruno/bruno/discussions/269)
طلبات الشراء لـ [الطبعة الذهبية](https://www.usebruno.com/pricing) ستطلق قريبًا بسعر ~~$19~~ **$9** ! <br/>
[اشترك هنا](https://usebruno.ck.page/4c65576bd4) لتصلك إشعارات عند الإطلاق.
### التثبيت

View File

@@ -37,13 +37,37 @@ Bruno 直接在您的电脑文件夹中存储您的 API 信息。我们使用纯
Bruno 仅限离线使用。我们计划永不向 Bruno 添加云同步功能。我们重视您的数据隐私,并认为它应该留在您的设备上。阅读我们的长期愿景 [点击查看](https://github.com/usebruno/bruno/discussions/269)
[下载 Bruno](https://www.usebruno.com/downloads)
📢 观看我们在印度 FOSS 3.0 会议上的最新演讲 [点击查看](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](../../assets/images/landing-2.png) <br /><br />
### 安装
## 商业版本 ✨
Bruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) Mac、Windows 和 Linux 的可执行文件
我们的大多数功能都是免费且开源的
我们致力于在 [开源与可持续性发展](https://github.com/usebruno/bruno/discussions/269) 之间取得和谐的平衡
欢迎使用我们的 [付费版本](https://www.usebruno.com/pricing) ,看看附加的功能是否对您或团队有所帮助! <br/>
## 目录
- [安装](#安装)
- [特性](#特性)
- [跨平台使用 🖥️](#跨平台使用-)
- [通过Git协作 👩‍💻🧑‍💻](#通过git协作-)
- [重要链接 📌](#重要链接-)
- [展示 🎥](#展示-)
- [分享评价 📣](#分享评价-)
- [发布到新的包管理器](#发布到新的包管理器)
- [联系方式 🌐](#联系方式-)
- [商标](#商标)
- [贡献 👩‍💻🧑‍💻](#贡献-)
- [作者](#作者)
- [许可证 📄](#许可证-)
## 安装
Bruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) 适用于Mac、Windows 和 Linux 的可执行文件。
您也可以通过包管理器如 Homebrew、Chocolatey、Scoop、Snap 和 Apt 安装 Bruno。
@@ -58,9 +82,15 @@ choco install bruno
scoop bucket add extras
scoop install bruno
# 在 Windows 上用 winget 安装
winget install Bruno.Bruno
# 在 Linux 上用 Snap 安装
snap install bruno
# 在 Linux 上用 Flatpak 安装
flatpak install com.usebruno.Bruno
# 在 Linux 上用 Apt 安装
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
@@ -73,67 +103,50 @@ echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebr
sudo apt update && sudo apt install bruno
```
### 在 Mac 上通过 Homebrew 安装 🖥️
## 特性
### 跨平台使用 🖥️
![bruno](../../assets/images/run-anywhere.png) <br /><br />
### Collaborate 安装 👩‍💻🧑‍💻
### 通过Git协作 👩‍💻🧑‍💻
或者任何您选择的版本控制系统
![bruno](../../assets/images/version-control.png) <br /><br />
### 重要链接 📌
## 重要链接 📌
- [我们的愿景](https://github.com/usebruno/bruno/discussions/269)
- [路线图](https://github.com/usebruno/bruno/discussions/384)
- [路线图](https://www.usebruno.com/roadmap)
- [文档](https://docs.usebruno.com)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
- [网站](https://www.usebruno.com)
- [价格](https://www.usebruno.com/pricing)
- [下载](https://www.usebruno.com/downloads)
- [GitHub 赞助](https://github.com/sponsors/helloanoop).
### 展示 🎥
## 展示 🎥
- [Testimonials](https://github.com/usebruno/bruno/discussions/343)
- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
### 支持 ❤️
如果您喜欢 Bruno 并想支持我们的开源工作,请考虑通过 [GitHub Sponsors](https://github.com/sponsors/helloanoop) 来赞助我们。
### 分享评价 📣
## 分享评价 📣
如果 Bruno 在您的工作和团队中帮助了您,请不要忘记在我们的 GitHub 讨论上分享您的 [评价](https://github.com/usebruno/bruno/discussions/343)
### 发布到新的包管理器
## 发布到新的包管理器
有关更多信息,请参见 [此处](../publishing/publishing_cn.md) 。
如需了解更多信息,请参见 [此处](../publishing/publishing_cn.md) 。
### 贡献 👩‍💻🧑‍💻
我很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。
即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。
### 作者
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### 联系方式 🌐
## 联系方式 🌐
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
[Website](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno)
### 商标
## 商标
**名称**
@@ -143,6 +156,20 @@ sudo apt update && sudo apt install bruno
Logo 源自 [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### 许可证 📄
## 贡献 👩‍💻🧑‍💻
很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。
即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。
## 作者
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
## 许可证 📄
[MIT](../../license.md)

View File

@@ -43,13 +43,6 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Syn
![bruno](/assets/images/landing-2.png) <br /><br />
### Golden Edition ✨
Die meisten unserer Funktionen sind kostenlos und quelloffen.
Wir bemühen uns um ein Gleichgewicht zwischen [Open-Source-Prinzipien und Nachhaltigkeit](https://github.com/usebruno/bruno/discussions/269)
Du kannst die [Golden Edition](https://www.usebruno.com/pricing) bestellen **$19**! <br/>
### Installation
Bruno ist als Download [auf unserer Website](https://www.usebruno.com/downloads) für Mac, Windows und Linux verfügbar.

View File

@@ -43,13 +43,6 @@ Bruno funciona sin conexión a internet. No tenemos intenciones de añadir sincr
![bruno](/assets/images/landing-2.png) <br /><br />
### Golden Edition ✨
La mayoría de nuestras funcionalidades son gratis y de código abierto.
Queremos alcanzar un equilibrio en armonía entre los [principios open-source y la sostenibilidad](https://github.com/usebruno/bruno/discussions/269).
¡Puedes reservar la [Golden Edition](https://www.usebruno.com/pricing) por ~~$19~~ **$9**! <br/>
### Instalación
Bruno está disponible para su descarga [en nuestro sitio web](https://www.usebruno.com/downloads) para Mac, Windows y Linux.

143
docs/readme/readme_fa.md Normal file
View File

@@ -0,0 +1,143 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### برونو یا Bruno - محیط توسعه متن باز برای تست و توسعه API ها
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](../../readme.md)
| [Українська](./readme_ua.md)
| [Русский](./readme_ru.md)
| [Türkçe](./readme_tr.md)
| [Deutsch](./readme_de.md)
| [Français](./readme_fr.md)
| [Português (BR)](./readme_pt_br.md)
| [한국어](./readme_kr.md)
| [বাংলা](./readme_bn.md)
| [Español](./readme_es.md)
| **فارسی**
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
برونو یک کلاینت API جدید و نوآورانه است که هدفش تغییر وضعیت فعلی ابزارهایی مانند Postman و سایر ابزارهای مشابه است.
برونو مجموعه‌های شما را مستقیماً در یک پوشه روی فایل‌سیستم شما ذخیره می‌کند. ما از یک زبان نشانه‌گذاری ساده به نام Bru برای ذخیره اطلاعات درخواست‌های API استفاده می‌کنیم.
شما می‌توانید برای همکاری روی مجموعه‌های API خود، از Git یا هر سیستم کنترل نسخه دلخواهتان استفاده کنید.
برونو فقط به صورت آفلاین کار می‌کند. هیچ برنامه‌ای برای اضافه کردن همگام‌سازی ابری به برونو در آینده وجود ندارد. ما به حریم خصوصی داده‌های شما اهمیت می‌دهیم و معتقدیم که باید روی دستگاه خودتان باقی بمانند. می‌توانید چشم‌انداز بلندمدت ما را مطالعه کنید. [اینجا (به انگلیسی)](https://github.com/usebruno/bruno/discussions/269)
📢 جدیدترین ارائه ما را در کنفرانس India FOSS 3.0 تماشا کنید.
[اینجا](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](/assets/images/landing-2.png) <br /><br />
### نصب
برونو به صورت یک فایل باینری برای دانلود در دسترس است. [بر روی وبسایت ما](https://www.usebruno.com/downloads) برای مک لینکوس و ویندوز.
همچنین می‌توانید برونو را از طریق مدیر بسته‌هایی مانند Homebrew، Chocolatey، Snap و Apt نصب کنید.
```sh
# بر روی مک از طریق brew
brew install bruno
# بر روی ویندوز از طریق Chocolatey
choco install bruno
# بر روی لینوکس از طریق Snap
snap install bruno
# بر روی لینوکس از طریق Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg curl
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
| gpg --dearmor \
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
| sudo tee /etc/apt/sources.list.d/bruno.list
sudo apt update && sudo apt install bruno
```
### روی پلتفرم‌های مختلف کار می‌کند 🖥️
![bruno](/assets/images/run-anywhere.png) <br /><br />
### همکاری از طریق گیت 👩‍💻🧑‍💻
یا هر سیستم کنترل نسخه‌ای که ترجیح می‌دهید
![bruno](/assets/images/version-control.png) <br /><br />
### لینک‌های مهم 📌
- [آخرین نسخه پایدار ما](https://github.com/usebruno/bruno/discussions/269)
- [نقشه راه](https://github.com/usebruno/bruno/discussions/384)
- [مستندات](https://docs.usebruno.com)
- [وبسایت](https://www.usebruno.com)
- [اشتراک ها](https://www.usebruno.com/pricing)
- [دانلود](https://www.usebruno.com/downloads)
### ویدیوها 🎥
- [تجربه ها](https://github.com/usebruno/bruno/discussions/343)
- [مرکز دانش](https://github.com/usebruno/bruno/discussions/386)
- [اسکریپ مانیا](https://github.com/usebruno/bruno/discussions/385)
### حمایت ❤️
جوون! اگر این پروژه را دوست دارید، روی دکمه ⭐ کلیک کنید!
### تجربه‌های به اشتراک گذاشته‌شده 📣
اگر برونو به شما یا تیمتان کمک کرده است، لطفاً فراموش نکنید تجربه‌های خود را به اشتراک بگذارید. [تجربه‌های خود را در بحث گیت‌هاب ما به اشتراک بگذارید](https://github.com/usebruno/bruno/discussions/343).
### انتشار برونو در یک پکیچ منیجر جدید
لطفا چک بکنید [اینجارو](../../publishing.md) برای اطلاعات بیشتر.
### مشارکت 👩‍💻🧑‍💻
خوشحالم که می‌خواهید برونو را بهتر کنید. لطفا [راهنمای مشارکت را بررسی کنید](../contributing/contributing_fa.md).
حتی اگر نمی‌توانید از طریق کدنویسی مشارکت کنید، در گزارش باگ‌ها و درخواست قابلیت‌های جدید که به حل نیازهای شما کمک می‌کند تردید نکنید.
### نویسنده ها
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### در ارتباط باشید 🌐
[𝕏 (تویتر)](https://twitter.com/use_bruno) <br />
[وبسایت](https://www.usebruno.com) <br />
[دیسکورد](https://discord.com/invite/KgcZUncpjq) <br />
[لینکدین](https://www.linkedin.com/company/usebruno)
### برند
**نام**
به فارسی برونو - `Bruno` یک علامت تجاری ثبت‌شده متعلق به [Anoop M D](https://www.helloanoop.com/)
**لوگو**
لوگو توسط [OpenMoji](https://openmoji.org/library/emoji-1F436/) ساخته شده است. مجوز: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### مجوز 📄
[MIT](../../license.md)

View File

@@ -43,13 +43,6 @@ Bruno はオフラインのみで利用できます。Bruno にクラウド同
![bruno](/assets/images/landing-2.png) <br /><br />
### ゴールデンエディション ✨
機能のほとんどが無料で使用でき、オープンソースとなっています。
私たちは[オープンソースの原則と長期的な維持](https://github.com/usebruno/bruno/discussions/269)の間でうまくバランスを取ろうと努力しています。
[ゴールデンエディション](https://www.usebruno.com/pricing)を **19 ドル** (買い切り)で購入できます!
### インストール方法
Bruno は[私たちのウェブサイト](https://www.usebruno.com/downloads)からバイナリをダウンロードできます。Mac, Windows, Linux に対応しています。

View File

@@ -43,12 +43,6 @@
![bruno](../../assets/images/landing-2.png) <br /><br />
### ოქროს გამოცემა ✨
მთავარი ფუნქციების უმეტესობა უფასოა და ღია წყაროა. ჩვენ ვცდილობთ ჰარმონიული ბალანსის დაცვას [ღია წყაროების პრინციპებსა და მდგრადობას შორის](https://github.com/usebruno/bruno/discussions/269)
თქვენ შეგიძლიათ შეიძინოთ [ოქროს გამოცემა](https://www.usebruno.com/pricing) ერთჯერადი გადახდით **19 დოლარად**! <br/>
### ინსტალაცია
ბრუნო ხელმისაწვდომია როგორც ბინარული ჩამოტვირთვა [ჩვენ的网站上](https://www.usebruno.com/downloads) Mac-ის, Windows-ისა და Linux-ისთვის.

View File

@@ -26,13 +26,6 @@ Bruno is uitsluitend offline. Er zijn geen plannen om ooit cloud-synchronisatie
![bruno](/assets/images/landing-2.png) <br /><br />
### Golden Edition ✨
De meeste van onze functies zijn gratis en open source.
We streven naar een harmonieuze balans tussen [open-source principes en duurzaamheid](https://github.com/usebruno/bruno/discussions/269).
Je kunt de [Golden Edition](https://www.usebruno.com/pricing) kopen voor een eenmalige betaling van **$19**! <br/>
### Installatie
Bruno is beschikbaar als binaire download [op onze website](https://www.usebruno.com/downloads) voor Mac, Windows en Linux.

View File

@@ -41,13 +41,6 @@ Bruno é totalmente offline. Não há planos de adicionar sincronização em nuv
![bruno](../../assets/images/landing-2.png) <br /><br />
### Golden Edition ✨
A grande maioria dos nossos recursos são gratuitos e de código aberto.
Nós nos esforçamos para encontrar um equilíbrio harmônico entre [princípios de código aberto e sustentabilidade](https://github.com/usebruno/bruno/discussions/269)
Você pode pré encomendar o plano [Golden Edition](https://www.usebruno.com/pricing) por ~~USD $19~~ **USD $9**! <br/>
### Instalação
Bruno está disponível para download como binário [em nosso site](https://www.usebruno.com/downloads) para Mac, Windows e Linux.

View File

@@ -7,14 +7,14 @@ const eslintPluginDiff = require('eslint-plugin-diff');
let stylistic;
const runESMImports = async () => {
stylistic = await import('@stylistic/eslint-plugin').then(d => d.default);
stylistic = await import('@stylistic/eslint-plugin').then((d) => d.default);
};
module.exports = runESMImports().then(() => defineConfig([
{
plugins: {
'diff': fixupPluginRules(eslintPluginDiff),
'@stylistic': stylistic,
'@stylistic': stylistic
},
languageOptions: {
parser: require('@typescript-eslint/parser'),
@@ -26,6 +26,7 @@ module.exports = runESMImports().then(() => defineConfig([
files: [
'./eslint.config.js',
'tests/**/*.{ts,js}',
'playwright/**/*.{js,ts}',
'packages/bruno-app/**/*.{js,jsx,ts}',
'packages/bruno-app/src/test-utils/mocks/codemirror.js',
'packages/bruno-cli/**/*.js',
@@ -37,6 +38,7 @@ module.exports = runESMImports().then(() => defineConfig([
'packages/bruno-lang/**/*.js',
'packages/bruno-requests/**/*.ts',
'packages/bruno-requests/**/*.js',
'packages/bruno-tests/**/*.{js,ts}'
],
processor: 'diff/diff',
rules: {
@@ -44,7 +46,7 @@ module.exports = runESMImports().then(() => defineConfig([
indent: 2,
quotes: 'single',
semi: true,
jsx: true,
jsx: true
}).rules,
'@stylistic/comma-dangle': ['error', 'never'],
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],
@@ -52,7 +54,7 @@ module.exports = runESMImports().then(() => defineConfig([
'@stylistic/curly-newline': ['error', {
multiline: true,
minElements: 2,
consistent: true,
consistent: true
}],
'@stylistic/function-paren-newline': ['error', 'never'],
'@stylistic/array-bracket-spacing': ['error', 'never'],
@@ -63,7 +65,7 @@ module.exports = runESMImports().then(() => defineConfig([
'@stylistic/semi-style': ['error', 'last'],
'@stylistic/max-len': ['off'],
'@stylistic/jsx-one-expression-per-line': ['off']
},
}
},
{
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],

189
package-lock.json generated
View File

@@ -3906,7 +3906,7 @@
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
@@ -4469,9 +4469,9 @@
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8232,12 +8232,6 @@
"@types/node": "*"
}
},
"node_modules/@types/google-protobuf": {
"version": "3.15.12",
"resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz",
"integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==",
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -8373,9 +8367,9 @@
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz",
"integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==",
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"license": "MIT"
},
"node_modules/@types/lodash-es": {
@@ -8388,15 +8382,6 @@
"@types/lodash": "*"
}
},
"node_modules/@types/lodash.set": {
"version": "4.3.9",
"resolved": "https://registry.npmjs.org/@types/lodash.set/-/lodash.set-4.3.9.tgz",
"integrity": "sha512-KOxyNkZpbaggVmqbpr82N2tDVTx05/3/j0f50Es1prxrWB0XYf9p3QNxqcbWb7P1Q9wlvsUSlCFnwlPCIJ46PQ==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/markdown-it": {
"version": "12.2.3",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
@@ -11654,7 +11639,7 @@
"dependencies": {
"env-paths": "^2.2.1",
"import-fresh": "^3.3.0",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"parse-json": "^5.2.0"
},
"engines": {
@@ -14209,6 +14194,15 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
"node_modules/fast-fuzzy": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/fast-fuzzy/-/fast-fuzzy-1.12.0.tgz",
"integrity": "sha512-sXxGgHS+ubYpsdLnvOvJ9w5GYYZrtL9mkosG3nfuD446ahvoWEsSKBP7ieGmWIKVLnaxRDgUJkZMdxRgA2Ni+Q==",
"license": "ISC",
"dependencies": {
"graphemesplit": "^2.4.1"
}
},
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -14225,6 +14219,12 @@
"node": ">=8.6.0"
}
},
"node_modules/fast-json-format": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/fast-json-format/-/fast-json-format-0.4.0.tgz",
"integrity": "sha512-HEomBtr2fYaVX3iaRdcVLU7Qd3SQhCYvXlMMM9RNaihfIaj5bIC7ADqw/bAPSg/uyX6FIBPq69ioXq0B4Cb6eA==",
"license": "MIT"
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -15168,9 +15168,9 @@
}
},
"node_modules/google-protobuf": {
"version": "3.21.4",
"resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz",
"integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-4.0.0.tgz",
"integrity": "sha512-b8wmenhUMf2WNL+xIJ/slvD/hEE6V3nRnG86O2bzkBrMweM9gnqZE1dfXlDjibY3aXJXDNbAHepevYyQ7qWKsQ==",
"license": "(BSD-3-Clause AND Apache-2.0)"
},
"node_modules/gopd": {
@@ -15217,6 +15217,16 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/graphemesplit": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/graphemesplit/-/graphemesplit-2.6.0.tgz",
"integrity": "sha512-rG9w2wAfkpg0DILa1pjnjNfucng3usON360shisqIMUBw/87pojcBSrHmeE4UwryAuBih7g8m1oilf5/u8EWdQ==",
"license": "MIT",
"dependencies": {
"js-base64": "^3.6.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/graphiql": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/graphiql/-/graphiql-3.7.1.tgz",
@@ -15287,20 +15297,18 @@
"node": ">= 6"
}
},
"node_modules/grpc-reflection-js": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/grpc-reflection-js/-/grpc-reflection-js-0.3.0.tgz",
"integrity": "sha512-3lhTlQluPxVgbowCXA3tAZC3RJW+GSOUkguLNYl1QffYRiutUB3RDfPkQFTcrCFJgNiIIxx+iJkr8s3uSp3zWA==",
"node_modules/grpc-js-reflection-client": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/grpc-js-reflection-client/-/grpc-js-reflection-client-1.3.0.tgz",
"integrity": "sha512-eJ5/m1pXpcheSjOGExktU69WPUKnL4Su3IxGJYYYjy3/w19vE8dH7Wi46G5T92bpM0eZWftjiM5HduX8CjPq9w==",
"license": "MIT",
"dependencies": {
"@types/google-protobuf": "^3.7.2",
"@types/lodash.set": "^4.3.6",
"google-protobuf": "^3.12.2",
"lodash.set": "^4.3.2",
"protobufjs": "^7.2.2"
"@types/lodash": "^4.17.15",
"lodash": "^4.17.21",
"protobufjs": "^7.4.0"
},
"peerDependencies": {
"@grpc/grpc-js": "^1.0.0"
"@grpc/grpc-js": "^1.12.6"
}
},
"node_modules/har-schema": {
@@ -17760,6 +17768,12 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-base64": {
"version": "3.7.8",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz",
"integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==",
"license": "BSD-3-Clause"
},
"node_modules/js-md4": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz",
@@ -17773,9 +17787,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -18603,12 +18617,6 @@
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/lodash.set": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
"integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==",
"license": "MIT"
},
"node_modules/lodash.uniq": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@@ -20297,18 +20305,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pidusage": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pidusage/-/pidusage-4.0.1.tgz",
"integrity": "sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==",
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.2.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/pify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
@@ -22272,7 +22268,7 @@
"config-file-ts": "^0.2.4",
"dotenv": "^9.0.2",
"dotenv-expand": "^5.1.0",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"json5": "^2.2.0",
"lazy-val": "^1.0.4"
},
@@ -25490,6 +25486,12 @@
"node": ">=0.6.0"
}
},
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
"license": "MIT"
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@@ -25915,6 +25917,22 @@
"node": ">=4"
}
},
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"license": "MIT",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unicode-trie/node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
"license": "MIT"
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -26832,6 +26850,8 @@
"cookie": "0.7.1",
"dompurify": "^3.2.4",
"escape-html": "^1.0.3",
"fast-fuzzy": "^1.12.0",
"fast-json-format": "~0.4.0",
"file": "^0.2.2",
"file-dialog": "^0.0.8",
"file-saver": "^2.0.5",
@@ -26840,9 +26860,9 @@
"graphiql": "3.7.1",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
"hexy": "^0.3.5",
"httpsnippet": "^3.0.9",
"i18next": "24.1.2",
"iconv-lite": "^0.6.3",
"idb": "^7.0.0",
"immer": "^9.0.15",
"jsesc": "^3.0.2",
@@ -26851,6 +26871,7 @@
"jsonc-parser": "^3.2.1",
"jsonpath-plus": "^10.3.0",
"know-your-http-well": "^0.5.0",
"linkify-it": "^5.0.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
@@ -28400,6 +28421,15 @@
"node": ">=18.0.0"
}
},
"packages/bruno-app/node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"packages/bruno-app/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -28448,6 +28478,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"packages/bruno-app/node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT"
},
"packages/bruno-app/node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
@@ -28503,7 +28539,7 @@
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"lodash": "^4.17.21",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
@@ -30127,10 +30163,11 @@
"license": "MIT",
"dependencies": {
"@usebruno/schema": "^0.7.0",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"jscodeshift": "^17.3.0",
"lodash": "^4.17.21",
"nanoid": "3.3.8"
"nanoid": "3.3.8",
"xml2js": "^0.6.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
@@ -30267,11 +30304,10 @@
"https-proxy-agent": "^7.0.2",
"iconv-lite": "^0.6.3",
"is-valid-path": "^0.1.1",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
"nanoid": "3.3.8",
"pidusage": "^4.0.1",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"tough-cookie": "^6.0.0",
@@ -31508,7 +31544,7 @@
"hosted-git-info": "^4.1.0",
"is-ci": "^3.0.0",
"isbinaryfile": "^5.0.0",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"lazy-val": "^1.0.5",
"minimatch": "^5.1.1",
"read-config-file": "6.3.2",
@@ -31555,7 +31591,7 @@
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.1",
"is-ci": "^3.0.0",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"source-map-support": "^0.5.19",
"stat-mode": "^1.0.0",
"temp-file": "^3.4.0"
@@ -31686,7 +31722,7 @@
"builder-util-runtime": "9.2.4",
"fs-extra": "^10.1.0",
"iconv-lite": "^0.6.2",
"js-yaml": "^4.1.0"
"js-yaml": "^4.1.1"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
@@ -31986,6 +32022,7 @@
"cheerio": "^1.0.0",
"crypto-js": "^4.2.0",
"json-query": "^2.2.2",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nanoid": "3.3.8",
@@ -31995,6 +32032,7 @@
"quickjs-emscripten": "^0.29.2",
"tv4": "^1.3.0",
"uuid": "^9.0.0",
"xml-formatter": "^3.5.0",
"xml2js": "^0.6.2"
},
"devDependencies": {
@@ -32036,6 +32074,18 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"packages/bruno-js/node_modules/xml-formatter": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-3.5.0.tgz",
"integrity": "sha512-9ij/f2PLIPv+YDywtdztq7U82kYMDa5yPYwpn0TnXnqJRH6Su8RC/oaw91erHe3aSEbfgBaA1hDzReDFb1SVXw==",
"license": "MIT",
"dependencies": {
"xml-parser-xo": "^4.1.0"
},
"engines": {
"node": ">= 14"
}
},
"packages/bruno-lang": {
"name": "@usebruno/lang",
"version": "0.12.0",
@@ -32073,7 +32123,8 @@
"@types/qs": "^6.9.18",
"axios": "^1.9.0",
"debug": "^4.4.3",
"grpc-reflection-js": "^0.3.0",
"google-protobuf": "^4.0.0",
"grpc-js-reflection-client": "^1.3.0",
"is-ip": "^5.0.1",
"system-ca": "^2.0.1",
"tough-cookie": "^6.0.0",
@@ -32221,7 +32272,7 @@
"express-basic-auth": "^1.2.1",
"fast-xml-parser": "^5.0.8",
"http-proxy": "^1.18.1",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"multer": "^1.4.5-lts.1",

View File

@@ -27,6 +27,8 @@
"cookie": "0.7.1",
"dompurify": "^3.2.4",
"escape-html": "^1.0.3",
"fast-fuzzy": "^1.12.0",
"fast-json-format": "~0.4.0",
"file": "^0.2.2",
"file-dialog": "^0.0.8",
"file-saver": "^2.0.5",
@@ -35,9 +37,9 @@
"graphiql": "3.7.1",
"graphql": "^16.6.0",
"graphql-request": "^3.7.0",
"hexy": "^0.3.5",
"httpsnippet": "^3.0.9",
"i18next": "24.1.2",
"iconv-lite": "^0.6.3",
"idb": "^7.0.0",
"immer": "^9.0.15",
"jsesc": "^3.0.2",
@@ -46,6 +48,7 @@
"jsonc-parser": "^3.2.1",
"jsonpath-plus": "^10.3.0",
"know-your-http-well": "^0.5.0",
"linkify-it": "^5.0.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",

View File

@@ -4,7 +4,7 @@ import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrappe
const AccordionContext = createContext();
const Accordion = ({ children, defaultIndex }) => {
const Accordion = ({ children, defaultIndex, dataTestId }) => {
const [openIndex, setOpenIndex] = useState(defaultIndex);
const toggleItem = (index) => {
@@ -13,7 +13,7 @@ const Accordion = ({ children, defaultIndex }) => {
return (
<AccordionContext.Provider value={{ openIndex, toggleItem }}>
<div>{children}</div>
<div data-testid={dataTestId}>{children}</div>
</AccordionContext.Provider>
);
};

View File

@@ -0,0 +1,83 @@
import React, { useRef, forwardRef } from 'react';
import { IconCaretDown } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { humanizeRequestBodyMode } from 'utils/collections';
const DEFAULT_MODES = [
{ key: 'multipartForm', label: 'Multipart Form', category: 'Form' },
{ key: 'formUrlEncoded', label: 'Form URL Encoded', category: 'Form' },
{ key: 'json', label: 'JSON', category: 'Raw' },
{ key: 'xml', label: 'XML', category: 'Raw' },
{ key: 'text', label: 'TEXT', category: 'Raw' },
{ key: 'sparql', label: 'SPARQL', category: 'Raw' },
{ key: 'file', label: 'File / Binary', category: 'Other' },
{ key: 'none', label: 'None', category: 'Other' }
];
const BodyModeSelector = ({
currentMode,
onModeChange,
modes = DEFAULT_MODES,
disabled = false,
className = '',
wrapperClassName = '',
showCategories = true,
placement = 'bottom-end'
}) => {
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none selected-body-mode">
{humanizeRequestBodyMode(currentMode)}
{' '}
<IconCaretDown className="caret ml-2" size={14} strokeWidth={2} />
</div>
);
});
const onModeSelect = (mode) => {
dropdownTippyRef.current.hide();
onModeChange(mode);
};
// Group modes by category for rendering
const groupedModes = modes.reduce((acc, mode) => {
const category = mode.category || 'Other';
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(mode);
return acc;
}, {});
return (
<div className={`inline-flex items-center body-mode-selector ${disabled ? 'cursor-default' : 'cursor-pointer'} ${wrapperClassName}`}>
<Dropdown
onCreate={onDropdownCreate}
icon={<Icon />}
placement={placement}
disabled={disabled}
className={className}
>
{Object.entries(groupedModes).map(([category, categoryModes]) => (
<React.Fragment key={category}>
{showCategories && <div className="label-item font-medium">{category}</div>}
{categoryModes.map((mode) => (
<div
key={mode.key}
className="dropdown-item"
onClick={() => onModeSelect(mode.key)}
>
{mode.label}
</div>
))}
</React.Fragment>
))}
</Dropdown>
</div>
);
};
export default BodyModeSelector;

View File

@@ -0,0 +1,79 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.checkbox-container {
width: 1rem;
height: 1rem;
display: flex;
justify-content: center;
align-items: center;
position: relative;
cursor: pointer;
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
.checkbox-checkmark {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
visibility: ${(props) => props.checked ? 'visible' : 'hidden'};
pointer-events: none;
}
.checkbox-input {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
width: 1rem;
height: 1rem;
border: 2px solid ${(props) => {
if (props.checked && props.disabled) {
return props.theme.colors.text.muted;
}
if (props.checked && !props.disabled) {
return props.theme.colors.text.yellow;
}
return props.theme.colors.text.muted;
}};
border-radius: 4px;
background-color: ${(props) => {
if (props.checked && !props.disabled) {
return props.theme.colors.text.yellow;
}
if (props.checked && props.disabled) {
return props.theme.colors.text.muted;
}
return 'transparent';
}};
cursor: pointer;
position: relative;
transition: all 0.2s ease;
outline: none;
box-shadow: none;
&:hover:not(:disabled) {
opacity: 0.8;
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px ${(props) => props.theme.colors.text.yellow}40;
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,45 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
import IconCheckMark from 'components/Icons/IconCheckMark';
import { useTheme } from 'providers/Theme';
const Checkbox = ({
checked = false,
disabled = false,
onChange,
className = '',
id,
name,
value,
dataTestId = 'checkbox'
}) => {
const { theme } = useTheme();
const handleChange = (e) => {
if (!disabled && onChange) {
onChange(e);
}
};
return (
<StyledWrapper checked={checked} disabled={disabled} className={className}>
<div className="checkbox-container">
<input
type="checkbox"
id={id}
name={name}
value={value}
checked={checked}
disabled={disabled}
onChange={handleChange}
className="checkbox-input"
data-testid={dataTestId}
/>
<IconCheckMark className="checkbox-checkmark" color={theme.examples.checkbox.color} size={14} />
</div>
</StyledWrapper>
);
};
export default Checkbox;

View File

@@ -1,6 +1,12 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
&.read-only {
div.CodeMirror .CodeMirror-cursor {
display: none !important;
}
}
div.CodeMirror {
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};

View File

@@ -14,7 +14,8 @@ import * as jsonlint from '@prantlf/jsonlint';
import { JSHINT } from 'jshint';
import stripJsonComments from 'strip-json-comments';
import { getAllVariables } from 'utils/collections';
import CustomSearch from './CustomSearch';
import { setupLinkAware } from 'utils/codemirror/linkAware';
import CodeMirrorSearch from 'components/CodeMirrorSearch';
const CodeMirror = require('codemirror');
window.jsonlint = jsonlint;
@@ -53,9 +54,11 @@ export default class CodeEditor extends React.Component {
lineWrapping: this.props.enableLineWrapping ?? true,
tabSize: TAB_SIZE,
mode: this.props.mode || 'application/ld+json',
brunoVarInfo: {
variables
},
brunoVarInfo: this.props.enableBrunoVarInfo !== false ? {
variables,
collection: this.props.collection,
item: this.props.item
} : false,
keyMap: 'sublime',
autoCloseBrackets: true,
matchBrackets: true,
@@ -202,6 +205,8 @@ export default class CodeEditor extends React.Component {
editor,
autoCompleteOptions
);
setupLinkAware(editor);
}
}
@@ -227,6 +232,16 @@ export default class CodeEditor extends React.Component {
if (!isEqual(variables, this.variables)) {
this.addOverlay();
}
// Update collection and item when they change
if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {
if (!isEqual(this.props.collection, this.editor.options.brunoVarInfo.collection)) {
this.editor.options.brunoVarInfo.collection = this.props.collection;
}
if (!isEqual(this.props.item, this.editor.options.brunoVarInfo.item)) {
this.editor.options.brunoVarInfo.item = this.props.item;
}
}
}
if (this.props.theme !== prevProps.theme && this.editor) {
@@ -245,11 +260,16 @@ export default class CodeEditor extends React.Component {
this.editor.setOption('mode', this.props.mode);
}
if (this.props.readOnly !== prevProps.readOnly && this.editor) {
this.editor.setOption('readOnly', this.props.readOnly);
}
this.ignoreChangeEvent = false;
}
componentWillUnmount() {
if (this.editor) {
this.editor?._destroyLinkAware?.();
this.editor.off('change', this._onEdit);
this.editor.off('scroll', this.onScroll);
this.editor = null;
@@ -262,12 +282,12 @@ export default class CodeEditor extends React.Component {
}
return (
<StyledWrapper
className="h-full w-full flex flex-col relative graphiql-container"
className={`h-full w-full flex flex-col relative graphiql-container ${this.props.readOnly ? 'read-only' : ''}`}
aria-label="Code Editor"
font={this.props.font}
fontSize={this.props.fontSize}
>
<CustomSearch
<CodeMirrorSearch
visible={this.state.searchBarVisible}
editor={this.editor}
onClose={() => this.setState({ searchBarVisible: false })}
@@ -286,6 +306,11 @@ export default class CodeEditor extends React.Component {
let variables = getAllVariables(this.props.collection, this.props.item);
this.variables = variables;
// Update brunoVarInfo with latest variables
if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {
this.editor.options.brunoVarInfo.variables = variables;
}
defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting);
this.editor.setOption('mode', 'brunovariables');
};

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import debounce from 'lodash/debounce';
import { IconRegex, IconArrowUp, IconArrowDown, IconX, IconLetterCase, IconLetterW } from '@tabler/icons';
import ToolHint from 'components/ToolHint';
import StyledWrapper from './StyledWrapper';
@@ -9,7 +8,7 @@ function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');
}
const CustomSearch = ({ visible, editor, onClose }) => {
const CodeMirrorSearch = ({ visible, editor, onClose }) => {
const [searchText, setSearchText] = useState('');
const [regex, setRegex] = useState(false);
const [caseSensitive, setCaseSensitive] = useState(false);
@@ -199,4 +198,4 @@ const CustomSearch = ({ visible, editor, onClose }) => {
);
};
export default CustomSearch;
export default CodeMirrorSearch;

View File

@@ -6,7 +6,7 @@ import Dropdown from 'components/Dropdown';
import { useTheme } from 'providers/Theme';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAPIKeyPlacement } from 'utils/collections';
@@ -16,9 +16,9 @@ const ApiKeyAuth = ({ collection }) => {
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const apikeyAuth = get(collection, 'root.request.auth.apikey', {});
const apikeyAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.apikey', {}) : get(collection, 'root.request.auth.apikey', {});
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const Icon = forwardRef((props, ref) => {
return (

View File

@@ -11,7 +11,7 @@ const AuthMode = ({ collection }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const authMode = get(collection, 'root.request.auth.mode');
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
const Icon = forwardRef((props, ref) => {
return (

View File

@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const AwsV4Auth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const awsv4Auth = get(collection, 'root.request.auth.awsv4', {});
const awsv4Auth = collection.draft?.root ? get(collection, 'draft.root.request.auth.awsv4', {}) : get(collection, 'root.request.auth.awsv4', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleAccessKeyIdChange = (accessKeyId) => {
dispatch(

View File

@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const BasicAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const basicAuth = get(collection, 'root.request.auth.basic', {});
const basicAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.basic', {}) : get(collection, 'root.request.auth.basic', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(basicAuth?.password);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleUsernameChange = (username) => {
dispatch(

View File

@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const BearerAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const bearerToken = get(collection, 'root.request.auth.bearer.token', '');
const bearerToken = collection.draft?.root ? get(collection, 'draft.root.request.auth.bearer.token', '') : get(collection, 'root.request.auth.bearer.token', '');
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(bearerToken);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleTokenChange = (token) => {
dispatch(

View File

@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const DigestAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const digestAuth = get(collection, 'root.request.auth.digest', {});
const digestAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.digest', {}) : get(collection, 'root.request.auth.digest', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(digestAuth?.password);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleUsernameChange = (username) => {
dispatch(

View File

@@ -6,7 +6,7 @@ import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
@@ -19,11 +19,11 @@ const NTLMAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const ntlmAuth = get(collection, 'root.request.auth.ntlm', {});
const ntlmAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.ntlm', {}) : get(collection, 'root.request.auth.ntlm', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleUsernameChange = (username) => {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import get from 'lodash/get';
import StyledWrapper from './StyledWrapper';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
import { useDispatch } from 'react-redux';
@@ -14,10 +14,10 @@ const GrantTypeComponentMap = ({collection }) => {
const dispatch = useDispatch();
const save = () => {
dispatch(saveCollectionRoot(collection.uid));
dispatch(saveCollectionSettings(collection.uid));
};
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {});
const grantType = get(request, 'auth.oauth2.grantType', {});
switch (grantType) {
@@ -40,7 +40,7 @@ const GrantTypeComponentMap = ({collection }) => {
};
const OAuth2 = ({ collection }) => {
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {});
return (
<StyledWrapper className="mt-2 w-full">

View File

@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const WsseAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const wsseAuth = get(collection, 'root.request.auth.wsse', {});
const wsseAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.wsse', {}) : get(collection, 'root.request.auth.wsse', {});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(wsseAuth?.password);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleUserChange = (username) => {
dispatch(

View File

@@ -8,17 +8,17 @@ import BasicAuth from './BasicAuth';
import DigestAuth from './DigestAuth';
import WsseAuth from './WsseAuth';
import ApiKeyAuth from './ApiKeyAuth/';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import OAuth2 from './OAuth2';
import NTLMAuth from './NTLMAuth';
const Auth = ({ collection }) => {
const authMode = get(collection, 'root.request.auth.mode');
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
const dispatch = useDispatch();
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const getAuthView = () => {
switch (authMode) {

View File

@@ -1,19 +1,30 @@
import React from 'react';
import { IconCertificate, IconTrash, IconWorld } from '@tabler/icons';
import { useFormik } from 'formik';
import { uuid } from 'utils/common';
import * as Yup from 'yup';
import { IconEye, IconEyeOff } from '@tabler/icons';
import { useState } from 'react';
import StyledWrapper from './StyledWrapper';
import { useRef } from 'react';
import path from 'utils/common/path';
import SensitiveFieldWarning from 'components/SensitiveFieldWarning/index';
import SingleLineEditor from 'components/SingleLineEditor/index';
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField/index';
import { useTheme } from 'styled-components';
import { useDispatch } from 'react-redux';
import { updateCollectionClientCertificates } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import get from 'lodash/get';
const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
const ClientCertSettings = ({ collection }) => {
const dispatch = useDispatch();
// Get client certs from draft if exists, otherwise from brunoConfig
const clientCertConfig = collection.draft?.brunoConfig
? get(collection, 'draft.brunoConfig.clientCertificates.certs', [])
: get(collection, 'brunoConfig.clientCertificates.certs', []);
const certFilePathInputRef = useRef();
const keyFilePathInputRef = useRef();
const pfxFilePathInputRef = useRef();
const { storedTheme } = useTheme();
const formik = useFormik({
initialValues: {
@@ -62,28 +73,47 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
passphrase: values.passphrase
};
}
onUpdate(relevantValues);
// Add the new cert to the existing certs in draft
const updatedCerts = [...clientCertConfig, relevantValues];
const clientCertificates = {
enabled: true,
certs: updatedCerts
};
dispatch(updateCollectionClientCertificates({
collectionUid: collection.uid,
clientCertificates
}));
formik.resetForm();
resetFileInputFields();
}
});
const { isSensitive } = useDetectSensitiveField(collection);
const { showWarning, warningMessage } = isSensitive(formik.values.passphrase);
const getFile = (e) => {
const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]);
if (filePath) {
let relativePath = path.relative(root, filePath);
let relativePath = path.relative(collection.pathname, filePath);
formik.setFieldValue(e.name, relativePath);
}
};
const resetFileInputFields = () => {
certFilePathInputRef.current.value = '';
keyFilePathInputRef.current.value = '';
pfxFilePathInputRef.current.value = '';
if (certFilePathInputRef.current) {
certFilePathInputRef.current.value = '';
}
if (keyFilePathInputRef.current) {
keyFilePathInputRef.current.value = '';
}
if (pfxFilePathInputRef.current) {
pfxFilePathInputRef.current.value = '';
}
};
const [passwordVisible, setPasswordVisible] = useState(false);
const handleTypeChange = (e) => {
formik.setFieldValue('type', e.target.value);
if (e.target.value === 'cert') {
@@ -97,6 +127,21 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
}
};
const handleRemove = (indexToRemove) => {
const updatedCerts = clientCertConfig.filter((cert, index) => index !== indexToRemove);
const clientCertificates = {
enabled: true,
certs: updatedCerts
};
dispatch(updateCollectionClientCertificates({
collectionUid: collection.uid,
clientCertificates
}));
};
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
return (
<StyledWrapper className="w-full h-full">
<div className="text-xs mb-4 text-muted">Add client certificates to be used for specific domains.</div>
@@ -116,9 +161,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
</div>
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
<button onClick={() => handleRemove(index)} className="remove-certificate ml-2">
<IconTrash size={18} strokeWidth={1.5} />
</button>
</button>
</div>
</li>
))}
@@ -314,30 +359,27 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
Passphrase
</label>
<div className="textbox flex flex-row items-center w-[300px] h-[1.70rem] relative">
<input
id="passphrase"
type={passwordVisible ? 'text' : 'password'}
name="passphrase"
className="outline-none w-64 bg-transparent"
onChange={formik.handleChange}
<SingleLineEditor
value={formik.values.passphrase || ''}
theme={storedTheme}
onChange={(val) => formik.setFieldValue('passphrase', val)}
collection={collection}
isSecret={true}
/>
<button
type="button"
className="btn btn-sm absolute right-0 l"
onClick={() => setPasswordVisible(!passwordVisible)}
>
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
</button>
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
</div>
{formik.touched.passphrase && formik.errors.passphrase ? (
<div className="ml-1 text-red-500">{formik.errors.passphrase}</div>
) : null}
</div>
<div className="mt-6">
<div className="mt-6 flex flex-row gap-2 items-center">
<button type="submit" className="submit btn btn-sm btn-secondary">
Add
</button>
<div className="h-4 border-l border-gray-600"></div>
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</form>
</StyledWrapper>

View File

@@ -1,10 +1,10 @@
import 'github-markdown-css/github-markdown.css';
import get from 'lodash/get';
import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections';
import { updateCollectionDocs, deleteCollectionDraft } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import Markdown from 'components/MarkDown';
import CodeEditor from 'components/CodeEditor';
import StyledWrapper from './StyledWrapper';
@@ -14,7 +14,7 @@ const Docs = ({ collection }) => {
const dispatch = useDispatch();
const { displayedTheme } = useTheme();
const [isEditing, setIsEditing] = useState(false);
const docs = get(collection, 'root.docs', '');
const docs = collection.draft?.root ? get(collection, 'draft.root.docs', '') : get(collection, 'root.docs', '');
const preferences = useSelector((state) => state.app.preferences);
const toggleViewMode = () => {
@@ -31,17 +31,17 @@ const Docs = ({ collection }) => {
};
const handleDiscardChanges = () => {
dispatch(
dispatch((
updateCollectionDocs({
collectionUid: collection.uid,
docs: docs
})
}))
);
toggleViewMode();
}
const onSave = () => {
dispatch(saveCollectionRoot(collection.uid));
dispatch(saveCollectionSettings(collection.uid));
toggleViewMode();
}

View File

@@ -10,7 +10,7 @@ import {
deleteCollectionHeader,
setCollectionHeaders
} from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
@@ -21,7 +21,7 @@ const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const Headers = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const headers = get(collection, 'root.request.headers', []);
const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
const toggleBulkEditMode = () => {
@@ -40,12 +40,13 @@ const Headers = ({ collection }) => {
);
};
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleHeaderValueChange = (e, _header, type) => {
const header = cloneDeep(_header);
switch (type) {
case 'name': {
header.name = e.target.value;
// Strip newlines from header keys
header.name = e.target.value.replace(/[\r\n]/g, '');
break;
}
case 'value': {

View File

@@ -1,37 +1,46 @@
import React from 'react';
import { useFormik } from 'formik';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
import cloneDeep from 'lodash/cloneDeep';
import { updateCollectionPresets } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { get } from 'lodash';
import DeprecationWarning from 'components/DeprecationWarning';
const PresetsSettings = ({ collection }) => {
const dispatch = useDispatch();
const {
brunoConfig: { presets: presets = {} }
} = collection;
const initialPresets = { requestType: 'http', requestUrl: '' };
const formik = useFormik({
enableReinitialize: true,
initialValues: {
requestType: presets.requestType || 'http',
requestUrl: presets.requestUrl || ''
},
onSubmit: (newPresets) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.presets = newPresets;
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
toast.success('Collection presets updated');
}
});
// Get presets from draft.brunoConfig if it exists, otherwise from brunoConfig
const currentPresets = collection.draft?.brunoConfig
? get(collection, 'draft.brunoConfig.presets', initialPresets)
: get(collection, 'brunoConfig.presets', initialPresets);
// Helper to update presets config
const updatePresets = (updates) => {
const updatedPresets = { ...currentPresets, ...updates };
dispatch(updateCollectionPresets({
collectionUid: collection.uid,
presets: updatedPresets
}));
};
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleRequestTypeChange = (e) => {
updatePresets({ requestType: e.target.value });
};
const handleRequestUrlChange = (e) => {
updatePresets({ requestUrl: e.target.value });
};
return (
<StyledWrapper className="h-full w-full">
<div className="text-xs mb-4 text-muted">
<DeprecationWarning featureName="Presets" learnMoreUrl="https://github.com/usebruno/bruno/discussions/6234" />
<div className="text-xs mb-4 mt-4 text-muted">
These presets will be used as the default values for new requests in this collection.
</div>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="bruno-form">
<div className="mb-3 flex items-center">
<label className="settings-label flex items-center" htmlFor="enabled">
Request Type
@@ -42,9 +51,9 @@ const PresetsSettings = ({ collection }) => {
className="cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
onChange={handleRequestTypeChange}
value="http"
checked={formik.values.requestType === 'http'}
checked={(currentPresets.requestType || 'http') === 'http'}
/>
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
HTTP
@@ -55,9 +64,9 @@ const PresetsSettings = ({ collection }) => {
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
onChange={handleRequestTypeChange}
value="graphql"
checked={formik.values.requestType === 'graphql'}
checked={(currentPresets.requestType || 'http') === 'graphql'}
/>
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
GraphQL
@@ -68,9 +77,9 @@ const PresetsSettings = ({ collection }) => {
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
onChange={handleRequestTypeChange}
value="grpc"
checked={formik.values.requestType === 'grpc'}
checked={(currentPresets.requestType || 'http') === 'grpc'}
/>
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
gRPC
@@ -93,8 +102,8 @@ const PresetsSettings = ({ collection }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.requestUrl || ''}
onChange={handleRequestUrlChange}
value={currentPresets.requestUrl || ''}
style={{ width: '100%' }}
/>
</div>
@@ -102,11 +111,11 @@ const PresetsSettings = ({ collection }) => {
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</form>
</div>
</StyledWrapper>
);
};

View File

@@ -1,4 +1,5 @@
import React, { useRef } from 'react';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import {
IconTrash,
@@ -10,8 +11,10 @@ import {
import { getBasename } from 'utils/common/path';
import { Tooltip } from 'react-tooltip';
import useProtoFileManagement from '../../../hooks/useProtoFileManagement';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
const ProtobufSettings = ({ collection }) => {
const dispatch = useDispatch();
const {
protoFiles,
importPaths,
@@ -27,6 +30,8 @@ const ProtobufSettings = ({ collection }) => {
} = useProtoFileManagement(collection);
const fileInputRef = useRef(null);
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
// Get file path using the ipcRenderer
const getProtoFile = async (event) => {
const files = event?.files;
@@ -164,7 +169,7 @@ const ProtobufSettings = ({ collection }) => {
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
<div className="flex items-center">
<IconFile size={16} className="text-gray-500 dark:text-gray-400 mr-2" />
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100" data-testid="protobuf-proto-file-name">
{getBasename(collection.pathname, file.path)}
</span>
{!isValid && <IconAlertCircle size={12} className="text-red-600 dark:text-red-400 ml-2" />}
@@ -329,6 +334,12 @@ const ProtobufSettings = ({ collection }) => {
</div>
</div>
<div className="mt-6">
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</StyledWrapper>
);
};

View File

@@ -1,106 +1,155 @@
import React, { useEffect } from 'react';
import { useFormik } from 'formik';
import React from 'react';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
import { IconEye, IconEyeOff } from '@tabler/icons';
import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { updateCollectionProxy } from 'providers/ReduxStore/slices/collections';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { get } from 'lodash';
import toast from 'react-hot-toast';
const ProxySettings = ({ proxyConfig, onUpdate }) => {
const proxySchema = Yup.object({
enabled: Yup.string().oneOf(['global', 'true', 'false']),
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string()
.when('enabled', {
is: 'true',
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
otherwise: (hostname) => hostname.nullable()
})
.max(1024),
port: Yup.number()
.min(1)
.max(65535)
.typeError('Specify port between 1 and 65535')
.nullable()
.transform((_, val) => (val ? Number(val) : null)),
auth: Yup.object()
.when('enabled', {
is: 'true',
then: Yup.object({
enabled: Yup.boolean(),
username: Yup.string()
.when('enabled', {
is: true,
then: (username) => username.required('Specify username for proxy authentication.')
})
.max(1024),
password: Yup.string()
.when('enabled', {
is: true,
then: (password) => password.required('Specify password for proxy authentication.')
})
.max(1024)
})
})
.optional(),
bypassProxy: Yup.string().optional().max(1024)
});
const ProxySettings = ({ collection }) => {
const dispatch = useDispatch();
const initialProxyConfig = { enabled: 'global', protocol: 'http', hostname: '', port: '', auth: { enabled: false, username: '', password: '' }, bypassProxy: '' };
const formik = useFormik({
initialValues: {
enabled: proxyConfig.enabled || 'global',
protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '',
auth: {
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
},
bypassProxy: proxyConfig.bypassProxy || ''
},
validationSchema: proxySchema,
onSubmit: (values) => {
proxySchema
.validate(values, { abortEarly: true })
.then((validatedProxy) => {
// serialize 'enabled' to boolean
if (validatedProxy.enabled === 'true') {
validatedProxy.enabled = true;
} else if (validatedProxy.enabled === 'false') {
validatedProxy.enabled = false;
}
// Get proxy from draft.brunoConfig if it exists, otherwise from brunoConfig
const currentProxyConfig = collection.draft?.brunoConfig
? get(collection, 'draft.brunoConfig.proxy', initialProxyConfig)
: get(collection, 'brunoConfig.proxy', initialProxyConfig);
onUpdate(validatedProxy);
})
.catch((error) => {
let errMsg = error.message || 'Preferences validation error';
toast.error(errMsg);
});
}
});
const [passwordVisible, setPasswordVisible] = useState(false);
useEffect(() => {
formik.setValues({
enabled: proxyConfig.enabled === true ? 'true' : proxyConfig.enabled === false ? 'false' : 'global',
protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '',
const validateHostnameOnChange = (hostname) => {
if (hostname && hostname.length > 1024) {
toast.error('Hostname must be less than 1024 characters');
return false;
}
return true;
};
const validatePortOnChange = (port) => {
if (!port || port === '') {
return true; // Allow empty port during typing
}
const portNum = Number(port);
if (isNaN(portNum)) {
toast.error('Port must be a valid number');
return false;
}
if (portNum < 1 || portNum > 65535) {
toast.error('Port must be between 1 and 65535');
return false;
}
return true;
};
const validateAuthUsernameOnChange = (username) => {
if (username && username.length > 1024) {
toast.error('Username must be less than 1024 characters');
return false;
}
return true;
};
const validateAuthPasswordOnChange = (password) => {
if (password && password.length > 1024) {
toast.error('Password must be less than 1024 characters');
return false;
}
return true;
};
const validateBypassProxyOnChange = (bypassProxy) => {
if (bypassProxy && bypassProxy.length > 1024) {
toast.error('Bypass proxy must be less than 1024 characters');
return false;
}
return true;
};
// Helper to update proxy config
const updateProxy = (updates) => {
const updatedProxy = { ...currentProxyConfig, ...updates };
dispatch(updateCollectionProxy({
collectionUid: collection.uid,
proxy: updatedProxy
}));
};
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleEnabledChange = (e) => {
const value = e.target.value;
// Convert string to boolean or keep as 'global'
const enabled = value === 'true' ? true : value === 'false' ? false : 'global';
updateProxy({ enabled });
};
const handleProtocolChange = (e) => {
updateProxy({ protocol: e.target.value });
};
const handleHostnameChange = (e) => {
const hostname = e.target.value;
if (validateHostnameOnChange(hostname)) {
updateProxy({ hostname });
}
};
const handlePortChange = (e) => {
const port = e.target.value ? Number(e.target.value) : '';
if (validatePortOnChange(port)) {
updateProxy({ port });
}
};
const handleAuthEnabledChange = (e) => {
updateProxy({
auth: {
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
},
bypassProxy: proxyConfig.bypassProxy || ''
...currentProxyConfig.auth,
enabled: e.target.checked
}
});
}, [proxyConfig]);
};
const handleAuthUsernameChange = (e) => {
const username = e.target.value;
if (validateAuthUsernameOnChange(username)) {
updateProxy({
auth: {
...currentProxyConfig.auth,
username
}
});
}
};
const handleAuthPasswordChange = (e) => {
const password = e.target.value;
if (validateAuthPasswordOnChange(password)) {
updateProxy({
auth: {
...currentProxyConfig.auth,
password
}
});
}
};
const handleBypassProxyChange = (e) => {
const bypassProxy = e.target.value;
if (validateBypassProxyOnChange(bypassProxy)) {
updateProxy({ bypassProxy });
}
};
const enabledValue = currentProxyConfig.enabled === true ? 'true' : currentProxyConfig.enabled === false ? 'false' : 'global';
return (
<StyledWrapper className="h-full w-full">
<div className="text-xs mb-4 text-muted">Configure proxy settings for this collection.</div>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="bruno-form">
<div className="mb-3 flex items-center">
<label className="settings-label flex items-center" htmlFor="enabled">
Config
@@ -120,8 +169,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
type="radio"
name="enabled"
value="global"
checked={formik.values.enabled === 'global'}
onChange={formik.handleChange}
checked={enabledValue === 'global'}
onChange={handleEnabledChange}
className="mr-1"
/>
global
@@ -130,9 +179,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<input
type="radio"
name="enabled"
value={'true'}
checked={formik.values.enabled === 'true'}
onChange={formik.handleChange}
value="true"
checked={enabledValue === 'true'}
onChange={handleEnabledChange}
className="mr-1"
/>
enabled
@@ -141,9 +190,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<input
type="radio"
name="enabled"
value={'false'}
checked={formik.values.enabled === 'false'}
onChange={formik.handleChange}
value="false"
checked={enabledValue === 'false'}
onChange={handleEnabledChange}
className="mr-1"
/>
disabled
@@ -160,8 +209,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
type="radio"
name="protocol"
value="http"
checked={formik.values.protocol === 'http'}
onChange={formik.handleChange}
checked={(currentProxyConfig.protocol || 'http') === 'http'}
onChange={handleProtocolChange}
className="mr-1"
/>
HTTP
@@ -171,8 +220,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
type="radio"
name="protocol"
value="https"
checked={formik.values.protocol === 'https'}
onChange={formik.handleChange}
checked={(currentProxyConfig.protocol || 'http') === 'https'}
onChange={handleProtocolChange}
className="mr-1"
/>
HTTPS
@@ -182,8 +231,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
type="radio"
name="protocol"
value="socks4"
checked={formik.values.protocol === 'socks4'}
onChange={formik.handleChange}
checked={(currentProxyConfig.protocol || 'http') === 'socks4'}
onChange={handleProtocolChange}
className="mr-1"
/>
SOCKS4
@@ -193,8 +242,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
type="radio"
name="protocol"
value="socks5"
checked={formik.values.protocol === 'socks5'}
onChange={formik.handleChange}
checked={(currentProxyConfig.protocol || 'http') === 'socks5'}
onChange={handleProtocolChange}
className="mr-1"
/>
SOCKS5
@@ -214,12 +263,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.hostname || ''}
onChange={handleHostnameChange}
value={currentProxyConfig.hostname || ''}
/>
{formik.touched.hostname && formik.errors.hostname ? (
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="port">
@@ -234,12 +280,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.port}
onChange={handlePortChange}
value={currentProxyConfig.port || ''}
/>
{formik.touched.port && formik.errors.port ? (
<div className="ml-3 text-red-500">{formik.errors.port}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.enabled">
@@ -248,8 +291,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<input
type="checkbox"
name="auth.enabled"
checked={formik.values.auth.enabled}
onChange={formik.handleChange}
checked={currentProxyConfig.auth?.enabled || false}
onChange={handleAuthEnabledChange}
/>
</div>
<div>
@@ -266,12 +309,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.username}
onChange={formik.handleChange}
value={currentProxyConfig.auth?.username || ''}
onChange={handleAuthUsernameChange}
/>
{formik.touched.auth?.username && formik.errors.auth?.username ? (
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
) : null}
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="auth.password">
@@ -287,8 +327,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={formik.values.auth.password}
onChange={formik.handleChange}
value={currentProxyConfig.auth?.password || ''}
onChange={handleAuthPasswordChange}
/>
<button
type="button"
@@ -298,9 +338,6 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
</button>
</div>
{formik.touched.auth?.password && formik.errors.auth?.password ? (
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
) : null}
</div>
</div>
<div className="mb-3 flex items-center">
@@ -316,19 +353,16 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.bypassProxy || ''}
onChange={handleBypassProxyChange}
value={currentProxyConfig.bypassProxy || ''}
/>
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
) : null}
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save
</button>
</div>
</form>
</div>
</StyledWrapper>
);
};

View File

@@ -1,20 +1,37 @@
import React from 'react';
import React, { useState, useEffect, useRef } from 'react';
import get from 'lodash/get';
import { useDispatch, useSelector } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateCollectionRequestScript, updateCollectionResponseScript } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
import StyledWrapper from './StyledWrapper';
const Script = ({ collection }) => {
const dispatch = useDispatch();
const requestScript = get(collection, 'root.request.script.req', '');
const responseScript = get(collection, 'root.request.script.res', '');
const [activeTab, setActiveTab] = useState('pre-request');
const preRequestEditorRef = useRef(null);
const postResponseEditorRef = useRef(null);
const requestScript = collection.draft?.root ? get(collection, 'draft.root.request.script.req', '') : get(collection, 'root.request.script.req', '');
const responseScript = collection.draft?.root ? get(collection, 'draft.root.request.script.res', '') : get(collection, 'root.request.script.res', '');
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
// Refresh CodeMirror when tab becomes visible
useEffect(() => {
const timer = setTimeout(() => {
if (activeTab === 'pre-request' && preRequestEditorRef.current?.editor) {
preRequestEditorRef.current.editor.refresh();
} else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) {
postResponseEditorRef.current.editor.refresh();
}
}, 0);
return () => clearTimeout(timer);
}, [activeTab]);
const onRequestScriptEdit = (value) => {
dispatch(
updateCollectionRequestScript({
@@ -34,42 +51,51 @@ const Script = ({ collection }) => {
};
const handleSave = () => {
dispatch(saveCollectionRoot(collection.uid));
dispatch(saveCollectionSettings(collection.uid));
};
return (
<StyledWrapper className="w-full flex flex-col h-full">
<StyledWrapper className="w-full flex flex-col h-full pt-4">
<div className="text-xs mb-4 text-muted">
Write pre and post-request scripts that will run before and after any request in this collection is sent.
</div>
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Pre Request</div>
<CodeEditor
collection={collection}
value={requestScript || ''}
theme={displayedTheme}
onEdit={onRequestScriptEdit}
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'bru']}
/>
</div>
<div className="flex-1 mt-6">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<CodeEditor
collection={collection}
value={responseScript || ''}
theme={displayedTheme}
onEdit={onResponseScriptEdit}
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'res', 'bru']}
/>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="pre-request">Pre Request</TabsTrigger>
<TabsTrigger value="post-response">Post Response</TabsTrigger>
</TabsList>
<TabsContent value="pre-request" className="mt-2">
<CodeEditor
ref={preRequestEditorRef}
collection={collection}
value={requestScript || ''}
theme={displayedTheme}
onEdit={onRequestScriptEdit}
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'bru']}
/>
</TabsContent>
<TabsContent value="post-response" className="mt-2">
<CodeEditor
ref={postResponseEditorRef}
collection={collection}
value={responseScript || ''}
theme={displayedTheme}
onEdit={onResponseScriptEdit}
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'res', 'bru']}
/>
</TabsContent>
</Tabs>
<div className="mt-12">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>

View File

@@ -3,13 +3,13 @@ import get from 'lodash/get';
import { useDispatch, useSelector } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateCollectionTests } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
const Tests = ({ collection }) => {
const dispatch = useDispatch();
const tests = get(collection, 'root.request.tests', '');
const tests = collection.draft?.root ? get(collection, 'draft.root.request.tests', '') : get(collection, 'root.request.tests', '');
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
@@ -23,7 +23,7 @@ const Tests = ({ collection }) => {
);
};
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
return (
<StyledWrapper className="w-full flex flex-col h-full">

View File

@@ -3,8 +3,8 @@ import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import MultiLineEditor from 'components/MultiLineEditor';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
@@ -28,7 +28,7 @@ const VarsTable = ({ collection, vars, varType }) => {
);
};
const onSave = () => dispatch(saveCollectionRoot(collection.uid));
const onSave = () => dispatch(saveCollectionSettings(collection.uid));
const handleVarChange = (e, v, type) => {
const _var = cloneDeep(v);
switch (type) {
@@ -114,7 +114,7 @@ const VarsTable = ({ collection, vars, varType }) => {
/>
</td>
<td>
<SingleLineEditor
<MultiLineEditor
value={_var.value}
theme={storedTheme}
onSave={onSave}

View File

@@ -2,14 +2,16 @@ import React from 'react';
import get from 'lodash/get';
import VarsTable from './VarsTable';
import StyledWrapper from './StyledWrapper';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import DeprecationWarning from 'components/DeprecationWarning';
const Vars = ({ collection }) => {
const dispatch = useDispatch();
const requestVars = get(collection, 'root.request.vars.req', []);
const responseVars = get(collection, 'root.request.vars.res', []);
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []);
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
@@ -18,6 +20,7 @@ const Vars = ({ collection }) => {
</div>
<div className="flex-1">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<DeprecationWarning featureName="Post Response Variables" learnMoreUrl="https://github.com/usebruno/bruno/discussions/6231" />
<VarsTable collection={collection} vars={responseVars} varType="response" />
</div>
<div className="mt-6">

View File

@@ -1,9 +1,6 @@
import React from 'react';
import classnames from 'classnames';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import toast from 'react-hot-toast';
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
import { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
import { useDispatch } from 'react-redux';
import ProxySettings from './ProxySettings';
@@ -31,61 +28,26 @@ const CollectionSettings = ({ collection }) => {
);
};
const root = collection?.root;
const root = collection?.draft?.root || collection?.root;
const hasScripts = root?.request?.script?.res || root?.request?.script?.req;
const hasTests = root?.request?.tests;
const hasDocs = root?.docs;
const headers = get(collection, 'root.request.headers', []);
const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
const activeHeadersCount = headers.filter((header) => header.enabled).length;
const requestVars = get(collection, 'root.request.vars.req', []);
const responseVars = get(collection, 'root.request.vars.res', []);
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []);
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
const authMode = (collection.draft?.root ? get(collection, 'draft.root.request.auth', {}) : get(collection, 'root.request.auth', {})).mode || 'none';
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
const protobufConfig = get(collection, 'brunoConfig.protobuf', {});
const presets = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.presets', []) : get(collection, 'brunoConfig.presets', []);
const hasPresets = presets && presets.requestUrl !== '';
const onProxySettingsUpdate = (config) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.proxy = config;
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
.then(() => {
toast.success('Collection settings updated successfully.');
})
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
};
const onClientCertSettingsUpdate = (config) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
if (!brunoConfig.clientCertificates) {
brunoConfig.clientCertificates = {
enabled: true,
certs: [config]
};
} else {
brunoConfig.clientCertificates.certs.push(config);
}
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
.then(() => {
toast.success('Collection settings updated successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
};
const onClientCertSettingsRemove = (config) => {
const brunoConfig = cloneDeep(collection.brunoConfig);
brunoConfig.clientCertificates.certs = brunoConfig.clientCertificates.certs.filter(
(item) => item.domain != config.domain
);
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
.then(() => {
toast.success('Collection settings updated successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
};
const proxyConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.proxy', {}) : get(collection, 'brunoConfig.proxy', {});
const proxyEnabled = proxyConfig.hostname ? true : false;
const clientCertConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.clientCertificates.certs', []) : get(collection, 'brunoConfig.clientCertificates.certs', []);
const protobufConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.protobuf', {}) : get(collection, 'brunoConfig.protobuf', {});
const getTabPanel = (tab) => {
switch (tab) {
@@ -111,15 +73,12 @@ const CollectionSettings = ({ collection }) => {
return <Presets collection={collection} />;
}
case 'proxy': {
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
return <ProxySettings collection={collection} />;
}
case 'clientCert': {
return (
<ClientCertSettings
root={collection.pathname}
clientCertConfig={clientCertConfig}
onUpdate={onClientCertSettingsUpdate}
onRemove={onClientCertSettingsRemove}
collection={collection}
/>
);
}
@@ -163,10 +122,11 @@ const CollectionSettings = ({ collection }) => {
</div>
<div className={getTabClassname('presets')} role="tab" onClick={() => setTab('presets')}>
Presets
{hasPresets && <StatusDot />}
</div>
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
Proxy
{Object.keys(proxyConfig).length > 0 && <StatusDot />}
{Object.keys(proxyConfig).length > 0 && proxyEnabled && <StatusDot />}
</div>
<div className={getTabClassname('clientCert')} role="tab" onClick={() => setTab('clientCert')}>
Client Certificates

View File

@@ -0,0 +1,42 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.deprecation-warning {
box-sizing: border-box;
display: flex;
flex-direction: row;
align-items: center;
padding: 8px;
gap: 4px;
margin-bottom: 8px;
background: ${(props) => props.theme.deprecationWarning.bg};
border: 1px solid ${(props) => props.theme.deprecationWarning.border};
border-radius: 6px;
.warning-icon {
color: ${(props) => props.theme.deprecationWarning.icon};
flex-shrink: 0;
width: 16px;
height: 16px;
}
.warning-text {
font-family: 'Inter', sans-serif;
font-style: normal;
font-size: 14px;
line-height: 17px;
color: ${(props) => props.theme.deprecationWarning.text};
a {
color: ${(props) => props.theme.textLink};
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import IconAlertTriangleFilled from '../Icons/IconAlertTriangleFilled';
import StyledWrapper from './StyledWrapper';
const DeprecationWarning = ({ featureName, learnMoreUrl }) => {
return (
<StyledWrapper>
<div className="deprecation-warning">
<IconAlertTriangleFilled className="warning-icon" size={16} />
<span className="warning-text">
{featureName} will be removed in <strong>v3.0.0</strong>. They are deprecated and will no longer be supported. Learn more in{' '}
<a href={learnMoreUrl} target="_blank" rel="noreferrer">this post</a> or contact us at{' '}
<a href="mailto:support@usebruno.com">support@usebruno.com</a> with questions.
</span>
</div>
</StyledWrapper>
);
};
export default DeprecationWarning;

View File

@@ -13,7 +13,7 @@ import {
IconChevronDown,
IconTerminal2,
IconNetwork,
IconDashboard,
IconDashboard
} from '@tabler/icons';
import {
closeConsole,

View File

@@ -5,7 +5,7 @@ const StyledWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
background: ${props => props.theme.console.bg};
background: ${(props) => props.theme.console.bg};
}
.tab-content-area {
@@ -30,19 +30,19 @@ const StyledWrapper = styled.div`
.section-header {
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid ${props => props.theme.console.border};
border-bottom: 1px solid ${(props) => props.theme.console.border};
h3 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: ${props => props.theme.console.titleColor};
color: ${(props) => props.theme.console.titleColor};
}
p {
margin: 0;
font-size: 13px;
color: ${props => props.theme.console.textMuted};
color: ${(props) => props.theme.console.textMuted};
}
}
@@ -53,7 +53,7 @@ const StyledWrapper = styled.div`
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
color: ${props => props.theme.console.titleColor};
color: ${(props) => props.theme.console.titleColor};
}
}
@@ -65,8 +65,8 @@ const StyledWrapper = styled.div`
}
.resource-card {
background: ${props => props.theme.console.headerBg};
border: 1px solid ${props => props.theme.console.border};
background: ${(props) => props.theme.console.headerBg};
border: 1px solid ${(props) => props.theme.console.border};
border-radius: 4px;
padding: 8px;
}
@@ -76,7 +76,7 @@ const StyledWrapper = styled.div`
align-items: center;
gap: 6px;
margin-bottom: 6px;
color: ${props => props.theme.console.titleColor};
color: ${(props) => props.theme.console.titleColor};
}
.resource-title {
@@ -87,13 +87,13 @@ const StyledWrapper = styled.div`
.resource-value {
font-size: 18px;
font-weight: 600;
color: ${props => props.theme.console.titleColor};
color: ${(props) => props.theme.console.titleColor};
margin-bottom: 2px;
}
.resource-subtitle {
font-size: 11px;
color: ${props => props.theme.console.buttonColor};
color: ${(props) => props.theme.console.buttonColor};
}
.resource-trend {
@@ -112,7 +112,7 @@ const StyledWrapper = styled.div`
}
&.stable {
color: ${props => props.theme.console.buttonColor};
color: ${(props) => props.theme.console.buttonColor};
}
}
`;

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import {
@@ -6,13 +6,44 @@ import {
IconDatabase,
IconClock,
IconServer,
IconChartLine,
IconChartLine
} from '@tabler/icons';
const Performance = () => {
const { systemResources } = useSelector(state => state.performance);
const { systemResources } = useSelector((state) => state.performance);
const formatBytes = bytes => {
useEffect(() => {
const { ipcRenderer } = window;
if (!ipcRenderer) {
console.warn('IPC Renderer not available');
return;
}
const startMonitoring = async () => {
try {
await ipcRenderer.invoke('renderer:start-system-monitoring', 2000);
} catch (error) {
console.error('Failed to start system monitoring:', error);
}
};
const stopMonitoring = async () => {
try {
await ipcRenderer.invoke('renderer:stop-system-monitoring');
} catch (error) {
console.error('Failed to stop system monitoring:', error);
}
};
startMonitoring();
return () => {
stopMonitoring();
};
}, []);
const formatBytes = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
@@ -20,7 +51,7 @@ const Performance = () => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const formatUptime = seconds => {
const formatUptime = (seconds) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);

View File

@@ -0,0 +1,24 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
/* Environment item styling */
.environment-item {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
padding: 0.375rem 0.5rem;
border-radius: 0.25rem;
transition: background-color 0.15s ease;
.environment-name {
color: ${(props) => props.theme.text};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,269 @@
import React, { useState, useEffect, useMemo } from 'react';
import Portal from 'components/Portal/index';
import Modal from 'components/Modal';
import { exportBrunoEnvironment } from 'utils/exporters/bruno-environment';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
const ExportEnvironmentModal = ({ onClose, environments = [], environmentType }) => {
const dispatch = useDispatch();
// Helper function to truncate environment names
const truncateEnvName = (name) => {
if (name.length > 40) {
return name.substring(0, 40) + '...';
}
return name;
};
const [isExporting, setIsExporting] = useState(false);
const [filePath, setFilePath] = useState('');
const [selectedEnvironments, setSelectedEnvironments] = useState({});
const [exportFormat, setExportFormat] = useState(environments.length > 1 ? 'single-file' : 'single-object');
// Initialize selected environments
useEffect(() => {
const initialSelection = {};
// Add all environments and select them by default
environments.forEach((env) => {
initialSelection[env.uid] = true;
});
setSelectedEnvironments(initialSelection);
}, [environments]);
useEffect(() => {
const selectedCount = Object.values(selectedEnvironments).filter(Boolean).length;
if (selectedCount <= 1) {
setExportFormat('single-object');
}
if (exportFormat === 'single-object' && selectedCount > 1) {
setExportFormat('single-file');
}
}, [selectedEnvironments]);
const browse = () => {
dispatch(browseDirectory())
.then((dirPath) => {
if (typeof dirPath === 'string') {
setFilePath(dirPath);
}
})
.catch((error) => {
setFilePath('');
console.error(error);
});
};
const handleEnvironmentToggle = (envUid) => {
setSelectedEnvironments((prev) => {
const newSelection = {
...prev,
[envUid]: !prev[envUid]
};
return newSelection;
});
};
const handleSelectAll = () => {
const allSelected = environments.every((env) => selectedEnvironments[env.uid]) || false;
const newSelection = environments.reduce((acc, env) => ({
...acc,
[env.uid]: !allSelected
}), {}) || {};
setSelectedEnvironments(newSelection);
};
// Memoized selected environments and count
const selectedEnvs = useMemo(() => {
return environments.filter((env) => selectedEnvironments[env.uid]) || [];
}, [environments, selectedEnvironments]);
const selectedCount = selectedEnvs.length;
const exportFormatOptions = useMemo(() => {
const isMultiple = selectedCount > 1;
if (isMultiple) {
return [
{ value: 'single-file', label: 'Single JSON file', description: 'All environments in one JSON array' },
{ value: 'folder', label: 'Separate files in folder', description: 'Each environment as a separate JSON file', disabled: false }
];
}
return [
{ value: 'single-object', label: 'Single JSON file', description: 'Export as a single environment JSON object' },
{ value: 'folder', label: 'Separate files in folder', description: 'Each environment as a separate JSON file', disabled: true }
];
}, [selectedCount, exportFormat]);
const handleExport = async () => {
try {
setIsExporting(true);
if (!filePath) {
toast.error('Please select a location to save the files');
return;
}
if (selectedCount === 0) {
toast.error('Please select at least one environment to export');
return;
}
await exportBrunoEnvironment({ environments: selectedEnvs, environmentType, filePath, exportFormat });
const successMessage = exportFormat === 'folder'
? `Environments exported successfully to bruno-${environmentType}-environments folder`
: 'Environment(s) exported successfully';
toast.success(successMessage);
onClose();
} catch (error) {
console.error('Export error:', error);
toast.error(error.message || 'Failed to export environments');
} finally {
setIsExporting(false);
}
};
return (
<Portal>
<StyledWrapper>
<Modal
size="md"
title="Export Environments"
hideFooter={true}
handleCancel={onClose}
>
<div className="py-2">
{/* Environments Section */}
<div className="mb-4">
{environments && environments.length > 0 ? (
<div className="flex flex-col h-full">
<div className="flex justify-between items-center mb-2 pb-1">
<h3 className="font-semibold text-sm text-theme">
{environmentType === 'global' ? 'Global Environments' : 'Collection Environments'}
</h3>
<button
type="button"
onClick={handleSelectAll}
className="text-xs text-link px-1 py-0.5 rounded transition-colors"
>
{environments.every((env) => selectedEnvironments[env.uid]) ? 'Deselect All' : 'Select All'}
</button>
</div>
<div className="flex flex-col gap-1 flex-1 overflow-y-auto">
{environments.map((env) => (
<label key={env.uid} className="environment-item">
<input
type="checkbox"
checked={selectedEnvironments[env.uid] || false}
onChange={() => handleEnvironmentToggle(env.uid)}
disabled={isExporting}
className="w-3.5 h-3.5 flex-shrink-0"
/>
<span className="environment-name">{truncateEnvName(env.name)}</span>
</label>
))}
</div>
</div>
) : (
<div className="flex flex-col h-full">
<div className="flex justify-between items-center mb-2 pb-1">
<h3 className="font-semibold text-sm text-theme">
{environmentType === 'global' ? 'Global Environments' : 'Collection Environments'}
</h3>
</div>
<div className="flex items-center justify-center flex-1 p-4 text-center">
<span className="text-xs text-muted">
No {environmentType === 'global' ? 'global' : 'collection'} environments
</span>
</div>
</div>
)}
</div>
{/* Export Format Section */}
{selectedCount > 0 && (
<div className="mb-4">
<label className="block text-sm font-medium mb-2 text-theme">
Export Format
</label>
<div className="space-y-2">
{exportFormatOptions.map((option) => (
<label key={option.value} className={`flex items-start p-2 rounded transition-colors ${option.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}>
<input
type="radio"
name="exportFormat"
value={option.value}
checked={exportFormat === option.value}
onChange={(e) => setExportFormat(e.target.value)}
disabled={isExporting || option.disabled}
className={`mt-0.5 mr-3 w-4 h-4 ${option.disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`}
/>
<div>
<div className={`text-sm font-medium ${option.disabled ? 'text-muted' : 'text-theme'}`}>{option.label}</div>
<div className="text-xs text-muted">{option.description}</div>
</div>
</label>
))}
</div>
</div>
)}
{/* Location Input Section */}
<div className="mb-4">
<label htmlFor="export-location" className="block text-sm font-medium mb-2 text-theme">
Location
</label>
<div className="flex flex-col relative items-center">
<input
id="export-location"
type="text"
className={`flex-1 textbox w-full ${isExporting || selectedCount <= 0 ? '' : 'cursor-pointer'}`}
title={filePath}
value={filePath}
onClick={browse}
onChange={(e) => setFilePath(e.target.value)}
disabled={isExporting || selectedCount <= 0}
placeholder="Select a target location"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
/>
</div>
</div>
{/* Export Actions */}
<div className="flex justify-end gap-2 mt-4 pt-3 border-t border-gray-200 dark:border-gray-700">
<button
type="button"
className="btn btn-sm btn-cancel mt-2 flex items-center"
onClick={onClose}
disabled={isExporting}
>
Cancel
</button>
<button
type="button"
className="btn btn-sm btn-secondary mt-2 flex items-center"
onClick={handleExport}
disabled={isExporting || selectedCount === 0}
>
{isExporting ? 'Exporting...' : `Export ${selectedCount || ''} Environment${selectedCount !== 1 ? 's' : ''}`}
</button>
</div>
</div>
</Modal>
</StyledWrapper>
</Portal>
);
};
export default ExportEnvironmentModal;

View File

@@ -0,0 +1,166 @@
import React, { useState } from 'react';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import importPostmanEnvironment from 'utils/importers/postman-environment';
import importBrunoEnvironment from 'utils/importers/bruno-environment';
import { readMultipleFiles } from 'utils/importers/file-reader';
import { importEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import { toastError } from 'utils/common/error';
import { IconFileImport } from '@tabler/icons';
const ImportEnvironmentModal = ({ type = 'collection', collection, onClose, onEnvironmentCreated }) => {
const dispatch = useDispatch();
const [isDragOver, setIsDragOver] = useState(false);
const isGlobal = type === 'global';
// Validate required props
if (!isGlobal && !collection) {
console.error('ImportEnvironmentModal: collection prop is required when type is "collection"');
return null;
}
const modalTitle = isGlobal ? 'Import Global Environment' : 'Import Environment';
const modalTestId = isGlobal ? 'import-global-environment-modal' : 'import-environment-modal';
const importTestId = isGlobal ? 'import-global-environment' : 'import-environment';
const processEnvironments = async (environments, successMessage) => {
const validEnvironments = environments.filter((env) => {
if (env.name && env.name !== 'undefined') {
return true;
} else {
toast.error('Failed to import environment: env has no name');
return false;
}
});
if (validEnvironments.length === 0) {
toast.error('No valid environments found to import');
return;
}
try {
// Process environments sequentially to ensure unique name checking considers previously imported environments
let importedCount = 0;
for (const environment of validEnvironments) {
const action = isGlobal
? addGlobalEnvironment({ name: environment.name, variables: environment.variables })
: importEnvironment({ name: environment.name, variables: environment.variables, collectionUid: collection?.uid });
await dispatch(action);
importedCount++;
}
toast.success(`${importedCount > 1 ? `${importedCount} environments` : 'Environment'} imported successfully`);
} catch (error) {
toast.error('An error occurred while importing the environment(s)');
console.error(error);
throw error;
}
};
const detectEnvironmentFormat = (data) => {
// bruno environment `single-object` export type
if (data.info && data.info.type === 'bruno-environment') {
return 'bruno';
} else if (Array.isArray(data)) {
// bruno environment`single-file` export type
return data.some((env) => env.info && env.info.type === 'bruno-environment') ? 'bruno' : 'postman';
} else if (data.id && data.values) {
// postman environment
return 'postman';
}
return 'bruno';
};
const handleImportEnvironment = async (files) => {
try {
// Read and parse all files
const parsedFiles = await readMultipleFiles(Array.from(files));
// Detect format from first file's content
const format = detectEnvironmentFormat(parsedFiles[0].content);
let environments;
if (format === 'postman') {
environments = await importPostmanEnvironment(parsedFiles);
} else {
environments = await importBrunoEnvironment(parsedFiles);
}
await processEnvironments(environments);
onClose();
if (onEnvironmentCreated) {
onEnvironmentCreated();
}
} catch (err) {
toastError(err, 'Import environment failed');
}
};
const handleFileSelect = () => {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.accept = '.json';
input.onchange = (e) => {
if (e.target.files && e.target.files.length > 0) {
handleImportEnvironment(e.target.files);
}
};
input.click();
};
const handleDragOver = (e) => {
e.preventDefault();
setIsDragOver(true);
};
const handleDragLeave = (e) => {
e.preventDefault();
setIsDragOver(false);
};
const handleDrop = (e) => {
e.preventDefault();
setIsDragOver(false);
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
handleImportEnvironment(files);
}
};
return (
<Portal>
<Modal size="md" title={modalTitle} hideFooter={true} handleConfirm={onClose} handleCancel={onClose} dataTestId={modalTestId}>
<div className="py-2">
<div
className={`flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed p-12 text-center cursor-pointer transition-colors focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2 ${
isDragOver
? 'border-amber-400 bg-amber-50 dark:bg-amber-900/20'
: 'border-zinc-300 dark:border-zinc-400 hover:border-zinc-400'
}`}
onClick={handleFileSelect}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
data-testid={importTestId}
>
<IconFileImport size={64} />
<span className="mt-2 block text-sm font-semibold">
{isDragOver ? 'Drop your environment files here' : 'Import your environments'}
</span>
<span className="mt-1 block text-xs text-muted">
Drag & drop JSON files/folders or click to browse. Supports both Bruno and Postman formats.
</span>
</div>
</div>
</Modal>
</Portal>
);
};
export default ImportEnvironmentModal;

View File

@@ -11,9 +11,8 @@ import EnvironmentListContent from './EnvironmentListContent/index';
import EnvironmentSettings from '../EnvironmentSettings';
import GlobalEnvironmentSettings from 'components/GlobalEnvironments/EnvironmentSettings';
import CreateEnvironment from '../EnvironmentSettings/CreateEnvironment';
import ImportEnvironment from '../EnvironmentSettings/ImportEnvironment';
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
import CreateGlobalEnvironment from 'components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment';
import ImportGlobalEnvironment from 'components/GlobalEnvironments/EnvironmentSettings/ImportEnvironment';
import ToolHint from 'components/ToolHint';
import StyledWrapper from './StyledWrapper';
@@ -242,7 +241,8 @@ const EnvironmentSelector = ({ collection }) => {
)}
{showImportGlobalModal && (
<ImportGlobalEnvironment
<ImportEnvironmentModal
type="global"
onClose={() => setShowImportGlobalModal(false)}
onEnvironmentCreated={() => {
setShowGlobalSettings(true);
@@ -261,7 +261,8 @@ const EnvironmentSelector = ({ collection }) => {
)}
{showImportCollectionModal && (
<ImportEnvironment
<ImportEnvironmentModal
type="collection"
collection={collection}
onClose={() => setShowImportCollectionModal(false)}
onEnvironmentCreated={() => {

View File

@@ -185,7 +185,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
</thead>
<tbody>
{formik.values.map((variable, index) => (
<tr key={variable.uid}>
<tr key={variable.uid} data-testid={`env-var-row-${variable.name}`}>
<td className="text-center">
<input
type="checkbox"
@@ -221,6 +221,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
value={variable.value}
isSecret={variable.secret}
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
enableBrunoVarInfo={false}
/>
</div>
{!variable.secret && hasSensitiveUsage(variable.name) && (

View File

@@ -4,6 +4,7 @@ import CopyEnvironment from '../../CopyEnvironment';
import DeleteEnvironment from '../../DeleteEnvironment';
import RenameEnvironment from '../../RenameEnvironment';
import EnvironmentVariables from './EnvironmentVariables';
import ToolHint from 'components/ToolHint/index';
const EnvironmentDetails = ({ environment, collection, setIsModified, onClose }) => {
const [openEditModal, setOpenEditModal] = useState(false);
@@ -30,10 +31,22 @@ const EnvironmentDetails = ({ environment, collection, setIsModified, onClose })
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
<span className="ml-1 font-semibold break-all">{environment.name}</span>
</div>
<div className="flex gap-x-4 pl-4">
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} />
<div className="flex gap-x-2 pl-2">
<ToolHint text="Edit Environment" toolhintId={`edit-${environment.uid}`}>
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
</ToolHint>
<ToolHint text="Copy Environment" toolhintId={`copy-${environment.uid}`}>
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
</ToolHint>
<ToolHint text="Delete Environment" toolhintId={`delete-${environment.uid}`}>
<IconTrash
className="cursor-pointer"
size={20}
strokeWidth={1.5}
onClick={() => setOpenDeleteModal(true)}
data-testid="delete-environment-button"
/>
</ToolHint>
</div>
</div>

View File

@@ -3,15 +3,15 @@ import { findEnvironmentInCollection } from 'utils/collections';
import usePrevious from 'hooks/usePrevious';
import EnvironmentDetails from './EnvironmentDetails';
import CreateEnvironment from '../CreateEnvironment';
import { IconDownload, IconShieldLock } from '@tabler/icons';
import ImportEnvironment from '../ImportEnvironment';
import { IconDownload, IconShieldLock, IconUpload } from '@tabler/icons';
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
import ManageSecrets from '../ManageSecrets';
import StyledWrapper from './StyledWrapper';
import ConfirmSwitchEnv from './ConfirmSwitchEnv';
import ToolHint from 'components/ToolHint';
import { isEqual } from 'lodash';
const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified, onClose }) => {
const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified, onClose, setShowExportModal }) => {
const { environments } = collection;
const [openCreateModal, setOpenCreateModal] = useState(false);
const [openImportModal, setOpenImportModal] = useState(false);
@@ -96,7 +96,7 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
return (
<StyledWrapper>
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
{openImportModal && <ImportEnvironment collection={collection} onClose={() => setOpenImportModal(false)} />}
{openImportModal && <ImportEnvironmentModal type="collection" collection={collection} onClose={() => setOpenImportModal(false)} />}
{openManageSecretsModal && <ManageSecrets onClose={() => setOpenManageSecretsModal(false)} />}
<div className="flex">
@@ -129,6 +129,10 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
<IconDownload size={12} strokeWidth={2} />
<span className="label ml-1 text-xs">Import</span>
</div>
<div className="flex items-center mt-2" onClick={() => setShowExportModal(true)}>
<IconUpload size={12} strokeWidth={2} />
<span className="label ml-1 text-xs">Export</span>
</div>
<div className="flex items-center mt-2" onClick={() => handleSecretsClick()}>
<IconShieldLock size={12} strokeWidth={2} />
<span className="label ml-1 text-xs">Managing Secrets</span>

View File

@@ -1,64 +0,0 @@
import React from 'react';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import importPostmanEnvironment from 'utils/importers/postman-environment';
import { importEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import { toastError } from 'utils/common/error';
import { IconDatabaseImport } from '@tabler/icons';
const ImportEnvironment = ({ collection, onClose, onEnvironmentCreated }) => {
const dispatch = useDispatch();
const handleImportPostmanEnvironment = () => {
importPostmanEnvironment()
.then((environments) => {
environments
.filter((env) =>
env.name && env.name !== 'undefined'
? true
: () => {
toast.error('Failed to import environment: env has no name');
return false;
}
)
.map((environment) => {
dispatch(importEnvironment(environment.name, environment.variables, collection.uid))
.then(() => {
toast.success('Environment imported successfully');
})
.catch((error) => {
toast.error('An error occurred while importing the environment');
console.error(error);
});
});
})
.then(() => {
onClose();
// Call the callback if provided
if (onEnvironmentCreated) {
onEnvironmentCreated();
}
})
.catch((err) => toastError(err, 'Postman Import environment failed'));
};
return (
<Portal>
<Modal size="sm" title="Import Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose} dataTestId="import-environment-modal">
<button
type="button"
onClick={handleImportPostmanEnvironment}
className="flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed border-zinc-300 dark:border-zinc-400 p-12 text-center hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2"
data-testid="import-postman-environment"
>
<IconDatabaseImport size={64} />
<span className="mt-2 block text-sm font-semibold">Import your Postman environments</span>
</button>
</Modal>
</Portal>
);
};
export default ImportEnvironment;

View File

@@ -3,8 +3,9 @@ import React, { useState } from 'react';
import CreateEnvironment from './CreateEnvironment';
import EnvironmentList from './EnvironmentList';
import StyledWrapper from './StyledWrapper';
import ImportEnvironment from './ImportEnvironment';
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
import { IconFileAlert } from '@tabler/icons';
import ExportEnvironmentModal from 'components/Environments/Common/ExportEnvironmentModal';
export const SharedButton = ({ children, className, onClick }) => {
return (
@@ -47,6 +48,7 @@ const EnvironmentSettings = ({ collection, onClose }) => {
const { environments } = collection;
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
const [tab, setTab] = useState('default');
const [showExportModal, setShowExportModal] = useState(false);
if (!environments || !environments.length) {
return (
<StyledWrapper>
@@ -54,7 +56,7 @@ const EnvironmentSettings = ({ collection, onClose }) => {
{tab === 'create' ? (
<CreateEnvironment collection={collection} onClose={() => setTab('default')} />
) : tab === 'import' ? (
<ImportEnvironment collection={collection} onClose={() => setTab('default')} />
<ImportEnvironmentModal type="collection" collection={collection} onClose={() => setTab('default')} />
) : (
<DefaultTab setTab={setTab} />
)}
@@ -64,16 +66,26 @@ const EnvironmentSettings = ({ collection, onClose }) => {
}
return (
<Modal size="lg" title="Environments" handleCancel={onClose} hideFooter={true}>
<EnvironmentList
selectedEnvironment={selectedEnvironment}
setSelectedEnvironment={setSelectedEnvironment}
collection={collection}
isModified={isModified}
setIsModified={setIsModified}
onClose={onClose}
/>
</Modal>
<StyledWrapper>
<Modal size="lg" title="Environments" handleCancel={onClose} hideFooter={true}>
<EnvironmentList
selectedEnvironment={selectedEnvironment}
setSelectedEnvironment={setSelectedEnvironment}
collection={collection}
isModified={isModified}
setIsModified={setIsModified}
onClose={onClose}
setShowExportModal={setShowExportModal}
/>
</Modal>
{showExportModal && (
<ExportEnvironmentModal
onClose={() => setShowExportModal(false)}
environments={collection.environments}
environmentType="collection"
/>
)}
</StyledWrapper>
);
};

View File

@@ -5,7 +5,7 @@ import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
import { IconX } from '@tabler/icons';
import { isWindowsOS } from 'utils/common/platform';
const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false }) => {
const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false, readOnly = false }) => {
const dispatch = useDispatch();
const filenames = (isSingleFilePicker ? [value] : value || [])
.filter((v) => v != null && v != '')
@@ -50,20 +50,24 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
return filenames.length + ' file(s) selected';
};
const buttonClass = `btn btn-secondary px-1 ${readOnly ? 'view-mode' : 'edit-mode'}`;
return filenames.length > 0 ? (
<div
className="btn btn-secondary px-1"
className={buttonClass}
style={{ fontWeight: 400, width: '100%', textOverflow: 'ellipsis', overflowX: 'hidden' }}
title={title}
>
<button className="align-middle" onClick={clear}>
<IconX size={18} />
</button>
&nbsp;
{!readOnly && (
<button className="align-middle" onClick={clear}>
<IconX size={18} />
</button>
)}
{!readOnly && <>&nbsp;</>}
{renderButtonText(filenames)}
</div>
) : (
<button className="btn btn-secondary px-1" style={{ width: '100%' }} onClick={browse}>
<button className={buttonClass} style={{ width: '100%' }} onClick={!readOnly ? browse : undefined} disabled={readOnly}>
{isSingleFilePicker ? 'Select File' : 'Select Files'}
</button>
);

View File

@@ -17,7 +17,7 @@ import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth';
import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
import { findItemInCollection, findParentItemInCollection, humanizeRequestAuthMode } from 'utils/collections/index';
import { humanizeRequestAuthMode, getTreePathFromCollectionToItem } from 'utils/collections/index';
const GrantTypeComponentMap = ({ collection, folder }) => {
const dispatch = useDispatch();
@@ -26,7 +26,8 @@ const GrantTypeComponentMap = ({ collection, folder }) => {
dispatch(saveFolderRoot(collection.uid, folder.uid));
};
let request = get(folder, 'root.request', {});
const folderRoot = folder?.draft || folder?.root;
let request = get(folderRoot, 'request', {});
const grantType = get(request, 'auth.oauth2.grantType', 'authorization_code');
switch (grantType) {
@@ -45,23 +46,15 @@ const GrantTypeComponentMap = ({ collection, folder }) => {
const Auth = ({ collection, folder }) => {
const dispatch = useDispatch();
let request = get(folder, 'root.request', {});
const authMode = get(folder, 'root.request.auth.mode');
const getTreePathFromCollectionToFolder = (collection, _folder) => {
let path = [];
let item = findItemInCollection(collection, _folder?.uid);
while (item) {
path.unshift(item);
item = findParentItemInCollection(collection, item?.uid);
}
return path;
};
const folderRoot = folder?.draft || folder?.root;
let request = get(folderRoot, 'request', {});
const authMode = get(folderRoot, 'request.auth.mode');
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionAuth = get(collection, 'root.request.auth');
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
@@ -69,14 +62,15 @@ const Auth = ({ collection, folder }) => {
};
// Get path from collection to current folder
const folderTreePath = getTreePathFromCollectionToFolder(collection, folder);
const folderTreePath = getTreePathFromCollectionToItem(collection, folder);
// Check parent folders to find closest auth configuration
// Skip the last item which is the current folder
for (let i = 0; i < folderTreePath.length - 1; i++) {
const parentFolder = folderTreePath[i];
if (parentFolder.type === 'folder') {
const folderAuth = get(parentFolder, 'root.request.auth');
const parentFolderRoot = parentFolder?.draft || parentFolder?.root;
const folderAuth = get(parentFolderRoot, 'request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {
effectiveSource = {
type: 'folder',

View File

@@ -11,7 +11,7 @@ const AuthMode = ({ collection, folder }) => {
const dispatch = useDispatch();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const authMode = get(folder, 'root.request.auth.mode');
const authMode = folder.draft ? get(folder, 'draft.request.auth.mode') : get(folder, 'root.request.auth.mode');
const Icon = forwardRef((props, ref) => {
return (

View File

@@ -14,7 +14,7 @@ const Documentation = ({ collection, folder }) => {
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const [isEditing, setIsEditing] = useState(false);
const docs = get(folder, 'root.docs', '');
const docs = folder.draft ? get(folder, 'draft.docs', '') : get(folder, 'root.docs', '');
const toggleViewMode = () => {
setIsEditing((prev) => !prev);

View File

@@ -16,7 +16,7 @@ const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const Headers = ({ collection, folder }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const headers = get(folder, 'root.request.headers', []);
const headers = folder.draft ? get(folder, 'draft.request.headers', []) : get(folder, 'root.request.headers', []);
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
const toggleBulkEditMode = () => {
@@ -41,7 +41,8 @@ const Headers = ({ collection, folder }) => {
const header = cloneDeep(_header);
switch (type) {
case 'name': {
header.name = e.target.value;
// Strip newlines from header keys
header.name = e.target.value.replace(/[\r\n]/g, '');
break;
}
case 'value': {

View File

@@ -1,20 +1,37 @@
import React from 'react';
import React, { useState, useEffect, useRef } from 'react';
import get from 'lodash/get';
import { useDispatch, useSelector } from 'react-redux';
import CodeEditor from 'components/CodeEditor';
import { updateFolderRequestScript, updateFolderResponseScript } from 'providers/ReduxStore/slices/collections';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
import StyledWrapper from './StyledWrapper';
const Script = ({ collection, folder }) => {
const dispatch = useDispatch();
const requestScript = get(folder, 'root.request.script.req', '');
const responseScript = get(folder, 'root.request.script.res', '');
const [activeTab, setActiveTab] = useState('pre-request');
const preRequestEditorRef = useRef(null);
const postResponseEditorRef = useRef(null);
const requestScript = folder.draft ? get(folder, 'draft.request.script.req', '') : get(folder, 'root.request.script.req', '');
const responseScript = folder.draft ? get(folder, 'draft.request.script.res', '') : get(folder, 'root.request.script.res', '');
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
// Refresh CodeMirror when tab becomes visible
useEffect(() => {
const timer = setTimeout(() => {
if (activeTab === 'pre-request' && preRequestEditorRef.current?.editor) {
preRequestEditorRef.current.editor.refresh();
} else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) {
postResponseEditorRef.current.editor.refresh();
}
}, 0);
return () => clearTimeout(timer);
}, [activeTab]);
const onRequestScriptEdit = (value) => {
dispatch(
updateFolderRequestScript({
@@ -40,38 +57,47 @@ const Script = ({ collection, folder }) => {
};
return (
<StyledWrapper className="w-full flex flex-col h-full">
<StyledWrapper className="w-full flex flex-col h-full pt-4">
<div className="text-xs mb-4 text-muted">
Pre and post-request scripts that will run before and after any request inside this folder is sent.
</div>
<div className="flex flex-col flex-1 mt-2 gap-y-2">
<div className="title text-xs">Pre Request</div>
<CodeEditor
collection={collection}
value={requestScript || ''}
theme={displayedTheme}
onEdit={onRequestScriptEdit}
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'bru']}
/>
</div>
<div className="flex flex-col flex-1 mt-2 gap-y-2">
<div className="title text-xs">Post Response</div>
<CodeEditor
collection={collection}
value={responseScript || ''}
theme={displayedTheme}
onEdit={onResponseScriptEdit}
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'res', 'bru']}
/>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="pre-request">Pre Request</TabsTrigger>
<TabsTrigger value="post-response">Post Response</TabsTrigger>
</TabsList>
<TabsContent value="pre-request" className="mt-2">
<CodeEditor
ref={preRequestEditorRef}
collection={collection}
value={requestScript || ''}
theme={displayedTheme}
onEdit={onRequestScriptEdit}
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'bru']}
/>
</TabsContent>
<TabsContent value="post-response" className="mt-2">
<CodeEditor
ref={postResponseEditorRef}
collection={collection}
value={responseScript || ''}
theme={displayedTheme}
onEdit={onResponseScriptEdit}
mode="javascript"
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'res', 'bru']}
/>
</TabsContent>
</Tabs>
<div className="mt-12">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>

View File

@@ -9,7 +9,7 @@ import StyledWrapper from './StyledWrapper';
const Tests = ({ collection, folder }) => {
const dispatch = useDispatch();
const tests = get(folder, 'root.request.tests', '');
const tests = folder.draft ? get(folder, 'draft.request.tests', '') : get(folder, 'root.request.tests', '');
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);

View File

@@ -4,7 +4,7 @@ import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import MultiLineEditor from 'components/MultiLineEditor';
import InfoTip from 'components/InfoTip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
@@ -113,7 +113,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
/>
</td>
<td>
<SingleLineEditor
<MultiLineEditor
value={_var.value}
theme={storedTheme}
onSave={onSave}

View File

@@ -4,12 +4,14 @@ import VarsTable from './VarsTable';
import StyledWrapper from './StyledWrapper';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import DeprecationWarning from 'components/DeprecationWarning';
const Vars = ({ collection, folder }) => {
const dispatch = useDispatch();
const requestVars = get(folder, 'root.request.vars.req', []);
const responseVars = get(folder, 'root.request.vars.res', []);
const requestVars = folder.draft ? get(folder, 'draft.request.vars.req', []) : get(folder, 'root.request.vars.req', []);
const responseVars = folder.draft ? get(folder, 'draft.request.vars.res', []) : get(folder, 'root.request.vars.res', []);
const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
@@ -18,6 +20,7 @@ const Vars = ({ collection, folder }) => {
</div>
<div className="flex-1">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<DeprecationWarning featureName="Post Response Variables" learnMoreUrl="https://github.com/usebruno/bruno/discussions/6231" />
<VarsTable folder={folder} collection={collection} vars={responseVars} varType="response" />
</div>
<div className="mt-6">

View File

@@ -20,7 +20,7 @@ const FolderSettings = ({ collection, folder }) => {
tab = folderLevelSettingsSelectedTab[folder?.uid];
}
const folderRoot = folder?.root;
const folderRoot = folder?.draft || folder?.root;
const hasScripts = folderRoot?.request?.script?.res || folderRoot?.request?.script?.req;
const hasTests = folderRoot?.request?.tests;

View File

@@ -18,7 +18,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const addButtonRef = useRef(null);
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector(state => state.globalEnvironments);
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
let _collection = cloneDeep(collection);
@@ -125,7 +125,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
</thead>
<tbody>
{formik.values.map((variable, index) => (
<tr key={variable.uid}>
<tr key={variable.uid} data-testid={`env-var-row-${variable.name}`}>
<td className="text-center">
<input
type="checkbox"
@@ -136,7 +136,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
/>
</td>
<td>
<div className="flex items-center">
<div className="flex items-center" data-testid={`env-var-name-${index}`}>
<input
type="text"
autoComplete="off"
@@ -153,7 +153,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
</div>
</td>
<td className="flex flex-row flex-nowrap items-center">
<div className="overflow-hidden grow w-full relative">
<div className="overflow-hidden grow w-full relative" data-testid={`env-var-value-${index}`}>
<MultiLineEditor
theme={storedTheme}
collection={_collection}
@@ -162,6 +162,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
isSecret={variable.secret}
readOnly={typeof variable.value !== 'string'}
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
enableBrunoVarInfo={false}
/>
</div>
{typeof variable.value !== 'string' && (

View File

@@ -4,8 +4,9 @@ import CopyEnvironment from '../../CopyEnvironment';
import DeleteEnvironment from '../../DeleteEnvironment';
import RenameEnvironment from '../../RenameEnvironment';
import EnvironmentVariables from './EnvironmentVariables';
import ToolHint from 'components/ToolHint/index';
const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
const EnvironmentDetails = ({ environment, setIsModified, collection, allEnvironments }) => {
const [openEditModal, setOpenEditModal] = useState(false);
const [openDeleteModal, setOpenDeleteModal] = useState(false);
const [openCopyModal, setOpenCopyModal] = useState(false);
@@ -29,15 +30,26 @@ const EnvironmentDetails = ({ environment, setIsModified, collection }) => {
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
<span className="ml-1 font-semibold break-all">{environment.name}</span>
</div>
<div className="flex gap-x-4 pl-4">
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} />
<div className="flex gap-x-2 pl-2">
<ToolHint text="Edit Environment" toolhintId={`edit-${environment.uid}`}>
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
</ToolHint>
<ToolHint text="Copy Environment" toolhintId={`copy-${environment.uid}`}>
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
</ToolHint>
<ToolHint text="Delete Environment" toolhintId={`delete-${environment.uid}`}>
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} data-testid="delete-environment-button" />
</ToolHint>
</div>
</div>
<div>
<EnvironmentVariables environment={environment} setIsModified={setIsModified} collection={collection} />
<EnvironmentVariables
environment={environment}
setIsModified={setIsModified}
collection={collection}
allEnvironments={allEnvironments}
/>
</div>
</div>
);

View File

@@ -2,15 +2,15 @@ import React, { useEffect, useState } from 'react';
import usePrevious from 'hooks/usePrevious';
import EnvironmentDetails from './EnvironmentDetails';
import CreateEnvironment from '../CreateEnvironment';
import { IconDownload, IconShieldLock } from '@tabler/icons';
import { IconDownload, IconShieldLock, IconUpload } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import ConfirmSwitchEnv from './ConfirmSwitchEnv';
import ManageSecrets from 'components/Environments/EnvironmentSettings/ManageSecrets/index';
import ImportEnvironment from '../ImportEnvironment';
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
import { isEqual } from 'lodash';
import ToolHint from 'components/ToolHint/index';
const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironment, setSelectedEnvironment, isModified, setIsModified, collection }) => {
const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironment, setSelectedEnvironment, isModified, setIsModified, collection, setShowExportModal }) => {
const [openCreateModal, setOpenCreateModal] = useState(false);
const [openImportModal, setOpenImportModal] = useState(false);
const [openManageSecretsModal, setOpenManageSecretsModal] = useState(false);
@@ -38,7 +38,7 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
return;
}
const environment = environments?.find(env => env.uid === activeEnvironmentUid) || environments?.[0];
const environment = environments?.find((env) => env.uid === activeEnvironmentUid) || environments?.[0] || null;
setSelectedEnvironment(environment);
setOriginalEnvironmentVariables(environment?.variables || []);
@@ -90,6 +90,12 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
setOpenManageSecretsModal(true);
};
const handleExportClick = () => {
if (setShowExportModal) {
setShowExportModal(true);
}
};
const handleConfirmSwitch = (saveChanges) => {
if (!saveChanges) {
setSwitchEnvConfirmClose(false);
@@ -99,7 +105,7 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
return (
<StyledWrapper>
{openCreateModal && <CreateEnvironment onClose={() => setOpenCreateModal(false)} />}
{openImportModal && <ImportEnvironment onClose={() => setOpenImportModal(false)} />}
{openImportModal && <ImportEnvironmentModal type="global" onClose={() => setOpenImportModal(false)} />}
{openManageSecretsModal && <ManageSecrets onClose={() => setOpenManageSecretsModal(false)} />}
<div className="flex">
@@ -132,6 +138,10 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
<IconDownload size={12} strokeWidth={2} />
<span className="label ml-1 text-xs">Import</span>
</div>
<div className="flex items-center mt-2" onClick={() => handleExportClick()}>
<IconUpload size={12} strokeWidth={2} />
<span className="label ml-1 text-xs">Export</span>
</div>
<div className="flex items-center mt-2" onClick={() => handleSecretsClick()}>
<IconShieldLock size={12} strokeWidth={2} />
<span className="label ml-1 text-xs">Managing Secrets</span>
@@ -144,6 +154,7 @@ const EnvironmentList = ({ environments, activeEnvironmentUid, selectedEnvironme
setIsModified={setIsModified}
originalEnvironmentVariables={originalEnvironmentVariables}
collection={collection}
allEnvironments={environments}
/>
</div>
</StyledWrapper>

View File

@@ -1,65 +0,0 @@
import React from 'react';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux';
import importPostmanEnvironment from 'utils/importers/postman-environment';
import { toastError } from 'utils/common/error';
import { IconDatabaseImport } from '@tabler/icons';
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import { uuid } from 'utils/common/index';
const ImportEnvironment = ({ onClose, onEnvironmentCreated }) => {
const dispatch = useDispatch();
const handleImportPostmanEnvironment = () => {
importPostmanEnvironment()
.then((environments) => {
environments
.filter((env) =>
env.name && env.name !== 'undefined'
? true
: () => {
toast.error('Failed to import environment: env has no name');
return false;
}
)
.map((environment) => {
dispatch(addGlobalEnvironment({ name: environment.name, variables: environment.variables }))
.then(() => {
toast.success('Global Environment imported successfully');
})
.catch((error) => {
toast.error('An error occurred while importing the environment');
console.error(error);
});
});
})
.then(() => {
onClose();
// Call the callback if provided
if (onEnvironmentCreated) {
onEnvironmentCreated();
}
})
.catch((err) => toastError(err, 'Postman Import environment failed'));
};
return (
<Portal>
<Modal size="sm" title="Import Global Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose} dataTestId="import-global-environment-modal">
<button
type="button"
onClick={handleImportPostmanEnvironment}
className="flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed border-zinc-300 dark:border-zinc-400 p-12 text-center hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2"
data-testid="import-postman-global-environment"
>
<IconDatabaseImport size={64} />
<span className="mt-2 block text-sm font-semibold">Import your Postman environments</span>
</button>
</Modal>
</Portal>
);
};
export default ImportEnvironment;

View File

@@ -4,7 +4,8 @@ import CreateEnvironment from './CreateEnvironment';
import EnvironmentList from './EnvironmentList';
import StyledWrapper from './StyledWrapper';
import { IconFileAlert } from '@tabler/icons';
import ImportEnvironment from './ImportEnvironment/index';
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
import ExportEnvironmentModal from 'components/Environments/Common/ExportEnvironmentModal';
export const SharedButton = ({ children, className, onClick }) => {
return (
@@ -44,6 +45,7 @@ const EnvironmentSettings = ({ globalEnvironments, collection, activeGlobalEnvir
const environments = globalEnvironments;
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
const [tab, setTab] = useState('default');
const [showExportModal, setShowExportModal] = useState(false);
if (!environments || !environments.length) {
return (
<StyledWrapper>
@@ -51,7 +53,7 @@ const EnvironmentSettings = ({ globalEnvironments, collection, activeGlobalEnvir
{tab === 'create' ? (
<CreateEnvironment onClose={() => setTab('default')} />
) : tab === 'import' ? (
<ImportEnvironment onClose={() => setTab('default')} />
<ImportEnvironmentModal type="global" onClose={() => setTab('default')} />
) : (
<DefaultTab setTab={setTab} />
)}
@@ -61,17 +63,27 @@ const EnvironmentSettings = ({ globalEnvironments, collection, activeGlobalEnvir
}
return (
<Modal size="lg" title="Global Environments" handleCancel={onClose} hideFooter={true}>
<EnvironmentList
environments={globalEnvironments}
activeEnvironmentUid={activeGlobalEnvironmentUid}
selectedEnvironment={selectedEnvironment}
setSelectedEnvironment={setSelectedEnvironment}
isModified={isModified}
setIsModified={setIsModified}
collection={collection}
/>
</Modal>
<StyledWrapper>
<Modal size="lg" title="Global Environments" handleCancel={onClose} hideFooter={true}>
<EnvironmentList
environments={globalEnvironments}
activeEnvironmentUid={activeGlobalEnvironmentUid}
selectedEnvironment={selectedEnvironment}
setSelectedEnvironment={setSelectedEnvironment}
isModified={isModified}
setIsModified={setIsModified}
collection={collection}
setShowExportModal={setShowExportModal}
/>
</Modal>
{showExportModal && (
<ExportEnvironmentModal
onClose={() => setShowExportModal(false)}
environments={globalEnvironments}
environmentType="global"
/>
)}
</StyledWrapper>
);
};

View File

@@ -74,7 +74,7 @@ const GlobalSearchModal = ({ isOpen, onClose }) => {
if (isItemARequest(item)) {
// add an optional check for the item name to prevent a crash if it doesnt exist.
const nameMatch = searchTerms.every(term => (item.name || '').toLowerCase().includes(term));
const nameMatch = searchTerms.every((term) => (item.name || '').toLowerCase().includes(term));
const urlMatch = searchTerms.every(term => (item.request?.url || '').toLowerCase().includes(term));
const pathMatch = enablePathMatch && searchTerms.every(term => itemPathLower.includes(term));

View File

@@ -0,0 +1,24 @@
import React from 'react';
const CloseAllIcon = ({ size = 18, strokeWidth = 1.5, className = '', ...props }) => {
return (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M7 7L7 5C7 4.46957 7.21072 3.96086 7.58579 3.58579C7.96086 3.21071 8.46957 3 9 3L19 3C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5L21 15C21 15.5304 20.7893 16.0391 20.4142 16.4142C20.0391 16.7893 19.5304 17 19 17L17 17M17 19C17 19.5304 16.7893 20.0391 16.4142 20.4142C16.0391 20.7893 15.5304 21 15 21L5 21C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19L3 9C3 8.46957 3.21072 7.96086 3.58579 7.58579C3.96086 7.21071 4.46957 7 5 7L15 7C15.5304 7 16.0391 7.21071 16.4142 7.58579C16.7893 7.96086 17 8.46957 17 9L17 19Z"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13 11L7 17M7 11L13 17"
stroke="currentColor"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export default CloseAllIcon;

View File

@@ -0,0 +1,21 @@
import React from 'react';
const ExampleIcon = ({ color = 'white', size = 16, ...props }) => {
return (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<g clipPath="url(#clip0_486_1191)">
<path d="M2.66699 3.33329C2.66699 3.15648 2.73723 2.98691 2.86225 2.86189C2.98728 2.73686 3.15685 2.66663 3.33366 2.66663H12.667C12.8438 2.66663 13.0134 2.73686 13.1384 2.86189C13.2634 2.98691 13.3337 3.15648 13.3337 3.33329V12.6666C13.3337 12.8434 13.2634 13.013 13.1384 13.138C13.0134 13.2631 12.8438 13.3333 12.667 13.3333H3.33366C3.15685 13.3333 2.98728 13.2631 2.86225 13.138C2.73723 13.013 2.66699 12.8434 2.66699 12.6666V3.33329Z" stroke={color} stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path d="M9.33366 5.33337H6.66699V10.6667H9.33366" stroke={color} stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
<path d="M9.33366 8H6.66699" stroke={color} stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</g>
<defs>
<clipPath id="clip0_486_1191">
<rect width={size} height={size} fill="white" />
</clipPath>
</defs>
</svg>
);
};
export default ExampleIcon;

View File

@@ -0,0 +1,14 @@
import React from 'react';
const IconAlertTriangleFilled = ({ size = 16, ...props }) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
<path
d="M12 1.67c.955 0 1.845 .467 2.39 1.247l.105 .16l8.114 13.548a2.914 2.914 0 0 1 -2.307 4.363l-.195 .008h-16.225a2.914 2.914 0 0 1 -2.582 -4.2l.099 -.185l8.11 -13.538a2.914 2.914 0 0 1 2.491 -1.403zm.01 13.33l-.127 .007a1 1 0 0 0 0 1.986l.117 .007l.127 -.007a1 1 0 0 0 0 -1.986l-.117 -.007zm-.01 -7a1 1 0 0 0 -.993 .883l-.007 .117v4l.007 .117a1 1 0 0 0 1.986 0l.007 -.117v-4l-.007 -.117a1 1 0 0 0 -.993 -.883z"
fill="currentColor"
/>
</svg>
);
};
export default IconAlertTriangleFilled;

View File

@@ -0,0 +1,18 @@
import React from 'react';
const IconCaretDown = ({ color = '#8C8C8C', ...props }) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<g clipPath="url(#clip0_464_9256)">
<path d="M10.5444 5.75H4.46004C4.26888 5.7509 4.08142 5.78521 3.91637 5.84952C3.75132 5.91383 3.61447 6.00587 3.51947 6.11647C3.42448 6.22706 3.37466 6.35234 3.375 6.47978C3.37534 6.60723 3.42583 6.73238 3.52142 6.84275L6.56492 10.23C6.66228 10.3372 6.79942 10.4258 6.96311 10.4874C7.1268 10.5489 7.31151 10.5813 7.49945 10.5814C7.68739 10.5816 7.8722 10.5494 8.03608 10.4881C8.19995 10.4267 8.33735 10.3383 8.43504 10.2312L11.4763 6.8465C11.573 6.73635 11.6246 6.61118 11.626 6.48355C11.6273 6.35591 11.5783 6.23028 11.4839 6.11924C11.3895 6.0082 11.253 5.91564 11.088 5.85084C10.9231 5.78603 10.7359 5.75126 10.5444 5.75Z" fill="#8C8C8C" />
</g>
<defs>
<clipPath id="clip0_464_9256">
<rect width="9" height="6" fill="white" transform="translate(3 5)" />
</clipPath>
</defs>
</svg>
);
};
export default IconCaretDown;

View File

@@ -0,0 +1,11 @@
import React from 'react';
const IconCheckMark = ({ color = '#cccccc', size = 16, ...props }) => {
return (
<svg width={size} height={size} viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M3.3335 8.49996L6.66683 11.8333L13.3335 5.16663" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
};
export default IconCheckMark;

View File

@@ -0,0 +1,19 @@
import React from 'react';
const IconEdit = ({ color = '#F39D0E', size = 16, ...props }) => {
return (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<g clipPath="url(#clip0_464_9527)">
<path d="M12.6665 13.3332H5.66654L2.85988 10.4665C2.73571 10.3416 2.66602 10.1727 2.66602 9.99654C2.66602 9.82042 2.73571 9.65145 2.85988 9.52654L9.52654 2.85988C9.65145 2.73571 9.82042 2.66602 9.99654 2.66602C10.1727 2.66602 10.3416 2.73571 10.4665 2.85988L13.7999 6.19321C13.924 6.31812 13.9937 6.48709 13.9937 6.66321C13.9937 6.83933 13.924 7.0083 13.7999 7.13321L7.66654 13.3332" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round" />
<path d="M11.9998 8.86663L7.7998 4.66663" stroke={color} strokeWidth="1.33333" strokeLinecap="round" strokeLinejoin="round" />
</g>
<defs>
<clipPath id="clip0_464_9527">
<rect width={size} height={size} fill="white" />
</clipPath>
</defs>
</svg>
);
};
export default IconEdit;

View File

@@ -9,7 +9,8 @@ const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
<div className="bruno-modal-header">
{customHeader ? customHeader : <>{title ? <div className="bruno-modal-header-title">{title}</div> : null}</>}
{handleCancel && !hideClose ? (
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null} data-test-id="modal-close-button">
// TODO: Remove data-test-id and use data-testid instead across the codebase.
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null} data-test-id="modal-close-button" data-testid="modal-close-button">
×
</div>
) : null}
@@ -25,7 +26,8 @@ const ModalFooter = ({
handleCancel,
confirmDisabled,
hideCancel,
hideFooter
hideFooter,
confirmButtonClass = 'btn-secondary'
}) => {
confirmText = confirmText || 'Save';
cancelText = cancelText || 'Cancel';
@@ -44,7 +46,7 @@ const ModalFooter = ({
<span>
<button
type="submit"
className="submit btn btn-md btn-secondary"
className={`submit btn btn-md ${confirmButtonClass}`}
disabled={confirmDisabled}
onClick={handleSubmit}
>
@@ -72,7 +74,8 @@ const Modal = ({
disableEscapeKey,
onClick,
closeModalFadeTimeout = 500,
dataTestId
dataTestId,
confirmButtonClass
}) => {
const modalRef = useRef(null);
const [isClosing, setIsClosing] = useState(false);
@@ -138,6 +141,7 @@ const Modal = ({
confirmDisabled={confirmDisabled}
hideCancel={hideCancel}
hideFooter={hideFooter}
confirmButtonClass={confirmButtonClass}
/>
</div>

View File

@@ -9,14 +9,15 @@ const StyledWrapper = styled.div`
&.read-only {
.CodeMirror .CodeMirror-lines {
cursor: not-allowed !important;
user-select: none !important;
-webkit-user-select: none !important;
-ms-user-select: none !important;
}
.CodeMirror-line {
color: ${(props) => props.theme.colors.text.muted} !important;
}
.CodeMirror-cursor {
display: none !important;
}
}
.CodeMirror {

View File

@@ -5,6 +5,7 @@ import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
import { MaskedEditor } from 'utils/common/masked-editor';
import StyledWrapper from './StyledWrapper';
import { setupLinkAware } from 'utils/codemirror/linkAware';
import { IconEye, IconEyeOff } from '@tabler/icons';
const CodeMirror = require('codemirror');
@@ -18,6 +19,7 @@ class MultiLineEditor extends Component {
this.cachedValue = props.value || '';
this.editorRef = React.createRef();
this.variables = {};
this.readOnly = props.readOnly || false;
this.state = {
maskInput: props.isSecret || false // Always mask the input by default (if it's a secret)
@@ -29,13 +31,17 @@ class MultiLineEditor extends Component {
const variables = getAllVariables(this.props.collection, this.props.item);
this.editor = CodeMirror(this.editorRef.current, {
lineWrapping: false,
lineNumbers: false,
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
placeholder: this.props.placeholder,
mode: 'brunovariables',
brunoVarInfo: {
variables
},
readOnly: this.props.readOnly ? 'nocursor' : false,
brunoVarInfo: this.props.enableBrunoVarInfo !== false ? {
variables,
collection: this.props.collection,
item: this.props.item
} : false,
readOnly: this.props.readOnly,
tabindex: 0,
extraKeys: {
'Ctrl-Enter': () => {
@@ -81,6 +87,8 @@ class MultiLineEditor extends Component {
this.editor,
autoCompleteOptions
);
setupLinkAware(this.editor);
this.editor.setValue(String(this.props.value) || '');
this.editor.on('change', this._onEdit);
@@ -108,8 +116,11 @@ class MultiLineEditor extends Component {
if (!this.maskedEditor) this.maskedEditor = new MaskedEditor(this.editor, '*');
this.maskedEditor.enable();
} else {
this.maskedEditor?.disable();
this.maskedEditor = null;
if (this.maskedEditor) {
this.maskedEditor.disable();
this.maskedEditor.destroy();
this.maskedEditor = null;
}
}
};
@@ -121,14 +132,26 @@ class MultiLineEditor extends Component {
let variables = getAllVariables(this.props.collection, this.props.item);
if (!isEqual(variables, this.variables)) {
this.editor.options.brunoVarInfo.variables = variables;
if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {
this.editor.options.brunoVarInfo.variables = variables;
}
this.addOverlay(variables);
}
// Update collection and item when they change
if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {
if (!isEqual(this.props.collection, this.editor.options.brunoVarInfo.collection)) {
this.editor.options.brunoVarInfo.collection = this.props.collection;
}
if (!isEqual(this.props.item, this.editor.options.brunoVarInfo.item)) {
this.editor.options.brunoVarInfo.item = this.props.item;
}
}
if (this.props.theme !== prevProps.theme && this.editor) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
}
if (this.props.readOnly !== prevProps.readOnly && this.editor) {
this.editor.setOption('readOnly', this.props.readOnly ? 'nocursor' : false);
this.editor.setOption('readOnly', this.props.readOnly);
}
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
this.cachedValue = String(this.props.value);
@@ -140,6 +163,9 @@ class MultiLineEditor extends Component {
// also set the maskInput flag to the new value
this.setState({ maskInput: this.props.isSecret });
}
if (this.props.readOnly !== prevProps.readOnly && this.editor) {
this.editor.setOption('readOnly', this.props.readOnly || false);
}
this.ignoreChangeEvent = false;
}
@@ -147,6 +173,9 @@ class MultiLineEditor extends Component {
if (this.brunoAutoCompleteCleanup) {
this.brunoAutoCompleteCleanup();
}
if (this.editor?._destroyLinkAware) {
this.editor._destroyLinkAware();
}
if (this.maskedEditor) {
this.maskedEditor.destroy();
this.maskedEditor = null;

View File

@@ -0,0 +1,74 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.radio-container {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.radio-input {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
width: 16px;
height: 16px;
border: 2px solid ${(props) => props.theme.colors.text.muted};
border-radius: 50%;
background-color: transparent;
cursor: pointer;
position: relative;
outline: none;
box-shadow: none;
margin: 0;
&:checked {
border-color: ${(props) => props.theme.colors.text.yellow};
background-color: transparent;
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background-color: ${(props) => props.theme.colors.text.yellow};
}
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
border-color: ${(props) => props.theme.colors.text.muted};
background-color: transparent;
&:checked {
border-color: ${(props) => props.theme.colors.text.muted};
&::after {
background-color: ${(props) => props.theme.colors.text.muted};
}
}
}
&:hover:not(:disabled) {
opacity: 0.8;
}
}
.radio-label {
position: absolute;
top: 0;
left: 0;
width: 16px;
height: 16px;
cursor: pointer;
pointer-events: none;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,40 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
const RadioButton = ({
checked,
disabled = false,
onChange,
name,
value,
id,
className = '',
dataTestId = 'radio-button'
}) => {
const handleChange = (e) => {
if (!disabled && onChange) {
onChange(e);
}
};
return (
<StyledWrapper>
<div className={`radio-container ${className}`}>
<input
type="radio"
id={id}
name={name}
value={value}
checked={checked}
disabled={disabled}
onChange={handleChange}
className="radio-input"
data-testid={dataTestId}
/>
<label htmlFor={id} className="radio-label" />
</div>
</StyledWrapper>
);
};
export default RadioButton;

View File

@@ -79,7 +79,7 @@ const ReorderTable = ({ children, updateReorderedItem }) => {
<>
<div
draggable
className="group drag-handle absolute z-10 left-[-17px] p-3.5 py-3.5 px-2.5 top-[3px] cursor-grab"
className="group drag-handle absolute z-10 left-[-17px] top-1/2 -translate-y-[80%] p-2.5 cursor-grab"
>
{hoveredRow === index && (
<>

View File

@@ -169,7 +169,6 @@ const AssertionRow = ({
<SingleLineEditor
value={value}
theme={storedTheme}
readOnly={true}
onSave={onSave}
onChange={(newValue) => {
handleAssertionChange(

View File

@@ -4,7 +4,8 @@ import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { IconPlus, IconTrash, IconAdjustmentsHorizontal } from '@tabler/icons';
import { cloneDeep } from "lodash";
import SingleLineEditor from "components/SingleLineEditor/index";
import SingleLineEditor from 'components/SingleLineEditor/index';
import MultiLineEditor from 'components/MultiLineEditor/index';
import StyledWrapper from "./StyledWrapper";
import Table from "components/Table/index";
@@ -205,7 +206,7 @@ const AdditionalParams = ({ item = {}, request, updateAuth, collection, handleS
/>
</td>
<td>
<SingleLineEditor
<MultiLineEditor
value={param?.value || ''}
theme={storedTheme}
onChange={(value) => handleUpdateAdditionalParam({

View File

@@ -9,6 +9,7 @@ import WsseAuth from './WsseAuth';
import NTLMAuth from './NTLMAuth';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import ApiKeyAuth from './ApiKeyAuth';
import StyledWrapper from './StyledWrapper';
@@ -27,6 +28,7 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
};
const Auth = ({ item, collection }) => {
const dispatch = useDispatch();
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
@@ -37,13 +39,14 @@ const Auth = ({ item, collection }) => {
// Save function for request level
const save = () => {
return saveRequest(item.uid, collection.uid);
return dispatch(saveRequest(item.uid, collection.uid));
};
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionAuth = get(collection, 'root.request.auth');
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',

View File

@@ -6,9 +6,9 @@ import { updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/colle
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import StyledWrapper from './StyledWrapper';
import { format, applyEdits } from 'jsonc-parser';
import { IconWand } from '@tabler/icons';
import toast from 'react-hot-toast';
import { prettifyJsonString } from 'utils/common/index';
const GraphQLVariables = ({ variables, item, collection }) => {
const dispatch = useDispatch();
@@ -19,8 +19,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
const onPrettify = () => {
if (!variables) return;
try {
const edits = format(variables, undefined, { tabSize: 2, insertSpaces: true });
const prettyVariables = applyEdits(variables, edits);
const prettyVariables = prettifyJsonString(variables);
dispatch(
updateRequestGraphqlVariables({
variables: prettyVariables,

View File

@@ -12,9 +12,9 @@ import StyledWrapper from './StyledWrapper';
import { IconSend, IconRefresh, IconWand, IconPlus, IconTrash, IconChevronDown, IconChevronUp } from '@tabler/icons';
import ToolHint from 'components/ToolHint/index';
import { toastError } from 'utils/common/error';
import { format, applyEdits } from 'jsonc-parser';
import toast from 'react-hot-toast'
import { getAbsoluteFilePath } from 'utils/common/path';
import { prettifyJsonString } from 'utils/common/index';
const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCollapsed, onToggleCollapse, handleRun, canClientSendMultipleMessages }) => {
const dispatch = useDispatch();
@@ -41,8 +41,8 @@ const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCol
dispatch(updateRequestBody({
content: currentMessages,
itemUid: item.uid,
collectionUid: collection.uid
itemUid: item.uid,
collectionUid: collection.uid
}));
};
@@ -130,8 +130,7 @@ const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCol
const onPrettify = () => {
try {
const edits = format(content, undefined, { tabSize: 2, insertSpaces: true });
const prettyBodyJson = applyEdits(content, edits);
const prettyBodyJson = prettifyJsonString(content);
const currentMessages = [...(body.grpc || [])];
currentMessages[index] = {
@@ -187,6 +186,7 @@ const SingleGrpcMessage = ({ message, item, collection, index, methodType, isCol
onClick={onSend}
disabled={!isConnectionActive}
className={`p-1 rounded ${isConnectionActive ? 'hover:bg-zinc-200 dark:hover:bg-zinc-600' : 'opacity-50 cursor-not-allowed'} transition-colors`}
data-testid={`grpc-send-message-${index}`}
>
<IconSend
size={16}
@@ -300,6 +300,7 @@ const GrpcBody = ({ item, collection, handleRun }) => {
<div
ref={messagesContainerRef}
id="grpc-messages-container"
data-testid="grpc-messages-container"
className={`flex-1 ${body.grpc.length === 1 || !canClientSendMultipleMessages ? 'h-full' : 'overflow-y-auto'} ${canClientSendMultipleMessages && 'pb-16'}`}
>
{body.grpc
@@ -326,6 +327,7 @@ const GrpcBody = ({ item, collection, handleRun }) => {
<button
onClick={addNewMessage}
className="add-message-btn flex items-center justify-center gap-2 py-2 px-4 rounded-md border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-700 hover:bg-neutral-200 dark:hover:bg-neutral-600 transition-colors shadow-md"
data-testid="grpc-add-message-button"
>
<IconPlus size={16} strokeWidth={1.5} className="text-neutral-700 dark:text-neutral-300" />
<span className="font-medium text-sm text-neutral-700 dark:text-neutral-300">Add Message</span>

View File

@@ -1,12 +1,14 @@
import React, { forwardRef } from 'react';
import { IconChevronDown } from '@tabler/icons';
import Dropdown from 'components/Dropdown/index';
import {
IconGrpcUnary,
IconGrpcBidiStreaming,
IconGrpcClientStreaming,
IconGrpcServerStreaming,
IconGrpcBidiStreaming
IconGrpcUnary
} from 'components/Icons/Grpc';
import SearchInput from 'components/SearchInput/index';
import { search } from 'fast-fuzzy';
import React, { forwardRef, useEffect, useRef, useState } from 'react';
const MethodDropdown = ({
grpcMethods,
@@ -14,6 +16,19 @@ const MethodDropdown = ({
onMethodSelect,
onMethodDropdownCreate
}) => {
const [searchText, setSearchText] = useState('');
const [focusedIndex, setFocusedIndex] = useState(-1);
const searchInputRef = useRef();
const listRef = useRef();
useEffect(() => {
const activeItem = listRef.current?.querySelector(`[data-index="${focusedIndex}"]`);
if (activeItem) {
activeItem.scrollIntoView({ block: 'nearest' });
}
}, [focusedIndex]);
const groupMethodsByService = (methods) => {
if (!methods || !methods.length) return {};
@@ -58,11 +73,11 @@ const MethodDropdown = ({
const MethodsDropdownIcon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-center ml-2 cursor-pointer select-none">
<div ref={ref} className="flex items-center justify-center ml-2 cursor-pointer select-none" data-testid="grpc-method-dropdown-trigger">
{selectedGrpcMethod && <div className="mr-2">{getIconForMethodType(selectedGrpcMethod.type)}</div>}
<span className="text-xs">
{selectedGrpcMethod ? (
<span className="dark:text-neutral-300 text-neutral-700 text-nowrap">
<span className="dark:text-neutral-300 text-neutral-700 text-nowrap" data-testid="selected-grpc-method-name">
{selectedGrpcMethod.path.split('.').at(-1) || selectedGrpcMethod.path}
</span>
) : (
@@ -79,49 +94,125 @@ const MethodDropdown = ({
onMethodSelect({ path: method.path, type: methodType });
};
const filteredMethods = searchText ? search(String(searchText), grpcMethods, { keySelector: (obj) => obj.path }) : grpcMethods;
const groupedMethods = groupMethodsByService(filteredMethods);
// Flatten grouped methods for keyboard navigation
const flatMethodList = Object.values(groupedMethods).flat();
const handleKeyDown = (e) => {
if (!flatMethodList.length) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
setFocusedIndex((prev) =>
prev < flatMethodList.length - 1 ? prev + 1 : flatMethodList.length - 1);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setFocusedIndex((prev) =>
prev >= 0 ? prev - 1 : -1);
} else if (e.key === 'Enter' && focusedIndex >= 0) {
e.preventDefault();
handleGrpcMethodSelect(flatMethodList[focusedIndex]);
}
};
const focusSearchInput = () => {
setTimeout(() => {
if (searchInputRef.current) {
searchInputRef.current.focus();
}
}, 0); // 0ms to ensure the dropdown is fully rendered and focused
};
const handleDropdownShow = () => {
focusSearchInput();
setSearchText('');
setFocusedIndex(-1);
};
const handleSearchChange = (e) => {
// auto focus the first method when the search input is not empty
if (e.target.value.trim().length > 0) {
setFocusedIndex(0);
} else {
setFocusedIndex(-1);
}
};
if (!grpcMethods || grpcMethods.length === 0) {
return null;
}
return (
<div className="flex items-center h-full mr-2" data-testid="grpc-methods-dropdown">
<Dropdown onCreate={onMethodDropdownCreate} icon={<MethodsDropdownIcon />} placement="bottom-end" style={{ maxWidth: 'unset' }}>
<div className="max-h-96 overflow-y-auto max-w-96 min-w-60" data-testid="grpc-methods-list">
{Object.entries(groupMethodsByService(grpcMethods)).map(([serviceName, methods], serviceIndex) => (
<div key={serviceIndex} className="service-group mb-2">
<Dropdown onCreate={onMethodDropdownCreate} icon={<MethodsDropdownIcon />} placement="bottom-end" style={{ maxWidth: 'unset' }} onShow={handleDropdownShow}>
<SearchInput
searchText={searchText}
setSearchText={setSearchText}
placeholder="Search"
ref={searchInputRef}
onKeyDown={handleKeyDown}
onBlur={focusSearchInput}
onChange={handleSearchChange}
className="mt-2 mb-3 "
data-testid="grpc-methods-search-input"
/>
<div ref={listRef} className="max-h-96 overflow-y-auto w-96 min-w-60" data-testid="grpc-methods-list">
{Object.entries(groupedMethods).map(([serviceName, methods], serviceIndex) => (
<div key={serviceIndex} className="service-group mb-2" onKeyDown={handleKeyDown} tabIndex={0}>
<div className="service-header px-3 py-1 bg-neutral-100 dark:bg-neutral-800 text-sm font-medium truncate sticky top-0 z-10">
{serviceName || 'Default Service'}
</div>
<div className="service-methods">
{methods.map((method, methodIndex) => (
<div
key={`${serviceIndex}-${methodIndex}`}
className={`py-2 px-3 w-full border-l-2 transition-all duration-200 relative group ${
selectedGrpcMethod && selectedGrpcMethod.path === method.path
? 'border-yellow-500 bg-yellow-500/20 dark:bg-yellow-900/20'
: 'border-transparent hover:bg-black/5 dark:hover:bg-white/5'
}`}
onClick={() => handleGrpcMethodSelect(method)}
data-testid="grpc-method-item"
>
<div className="flex items-center">
<div className="text-xs mr-3 text-gray-500">
{getIconForMethodType(method.type)}
</div>
<div className="flex flex-col flex-1">
<div className="font-medium text-gray-900 dark:text-gray-100">
{method.methodName}
{methods.map((method, methodIndex) => {
const globalMethodIndex
= Object.values(groupedMethods)
.slice(0, serviceIndex)
.reduce((acc, group) => acc + group.length, 0) + methodIndex;
return (
<div
key={`${serviceIndex}-${methodIndex}`}
className={`py-2 px-3 w-full border-l-2 transition-all duration-200 relative group ${
selectedGrpcMethod && selectedGrpcMethod.path === method.path
? 'border-yellow-500 bg-yellow-500/20 dark:bg-yellow-900/20'
: 'border-transparent hover:bg-black/5 dark:hover:bg-white/5'
} ${focusedIndex === globalMethodIndex
? 'bg-black/5 dark:bg-white/5' : ''}`}
onClick={() => handleGrpcMethodSelect(method)}
data-index={globalMethodIndex}
data-testid="grpc-method-item"
>
<div className="flex items-center">
<div className="text-xs mr-3 text-gray-500">
{getIconForMethodType(method.type)}
</div>
<div className="text-xs text-gray-500">
{method.type}
<div className="flex flex-col flex-1">
<div className="font-medium text-gray-900 dark:text-gray-100">
{method.methodName}
</div>
<div className="text-xs text-gray-500">
{method.type}
</div>
</div>
</div>
</div>
</div>
))}
);
})}
</div>
</div>
))}
{filteredMethods.length === 0 && (
<div className="py-2 px-3 w-full transition-all duration-200 relative group">
<div className="flex items-center">
<div className="text-xs mr-3 text-gray-500">
No methods found for the search term
</div>
</div>
</div>
)}
</div>
</Dropdown>
</div>

View File

@@ -39,7 +39,7 @@ const ProtoFileDropdown = ({
return;
}
const { success: addSuccess, relativePath, alreadyExists, error: addError } = await protoFileManagement.addProtoFileToCollection(filePath);
const { success: addSuccess, relativePath, alreadyExists, error: addError } = await protoFileManagement.addProtoFileFromRequest(filePath);
if (!addSuccess) {
if (addError) {
toast.error(`Failed to add proto file: ${addError.message}`);
@@ -91,7 +91,7 @@ const ProtoFileDropdown = ({
return;
}
const { success: addSuccess, error: addError } = await protoFileManagement.addImportPathToCollection(directoryPath);
const { success: addSuccess, error: addError } = await protoFileManagement.addImportPathFromRequest(directoryPath);
if (!addSuccess) {
if (addError) {
toast.error(`Failed to add import path: ${addError.message}`);
@@ -103,7 +103,7 @@ const ProtoFileDropdown = ({
};
const handleToggleImportPath = async (index) => {
const { success, enabled, error } = await protoFileManagement.toggleImportPath(index);
const { success, enabled, error } = await protoFileManagement.toggleImportPathFromRequest(index);
if (!success) {
if (error) {
toast.error(`Failed to toggle import path: ${error.message}`);

View File

@@ -129,11 +129,15 @@ const GrpcQueryUrl = ({ item, collection, handleRun }) => {
setProtoFilePath('');
setIsReflectionMode(true);
dispatch(updateRequestProtoPath({
protoPath: '',
itemUid: item.uid,
collectionUid: collection.uid
}));
// Only update protoPath if it was previously set (to avoid creating unnecessary draft state)
const currentProtoPath = getPropertyFromDraftOrRequest(item, 'request.protoPath', '');
if (currentProtoPath) {
dispatch(updateRequestProtoPath({
protoPath: '',
itemUid: item.uid,
collectionUid: collection.uid
}));
}
if (methods && methods.length > 0) {
toast.success(`Loaded ${methods.length} gRPC methods from reflection`);
@@ -346,6 +350,7 @@ const GrpcQueryUrl = ({ item, collection, handleRun }) => {
strokeWidth={1.5}
size={22}
className={`${(isReflectionMode ? reflectionManagement.isLoadingMethods : protoFileManagement.isLoadingMethods) ? 'animate-spin' : 'cursor-pointer'}`}
data-testid="refresh-methods-icon"
/>
<span className="infotip-text text-xs">
{isReflectionMode ? 'Refresh server reflection' : 'Refresh proto file methods'}
@@ -388,25 +393,28 @@ const GrpcQueryUrl = ({ item, collection, handleRun }) => {
{isConnectionActive && isStreamingMethod && (
<div className="connection-controls relative flex items-center h-full gap-3">
<div className="infotip" onClick={handleCancelConnection}>
<div className="infotip" onClick={handleCancelConnection} data-testid="grpc-cancel-connection-button">
<IconX color={theme.requestTabs.icon.color} strokeWidth={1.5} size={22} className="cursor-pointer" />
<span className="infotip-text text-xs">Cancel</span>
</div>
{isClientStreamingMethod && <div onClick={handleEndConnection}>
<IconCheck
color={theme.colors.text.green}
strokeWidth={2}
size={22}
className="cursor-pointer"
/>
</div>}
{isClientStreamingMethod && (
<div onClick={handleEndConnection} data-testid="grpc-end-connection-button">
<IconCheck
color={theme.colors.text.green}
strokeWidth={2}
size={22}
className="cursor-pointer"
/>
</div>
)}
</div>
)}
{(!isConnectionActive || !isStreamingMethod) && (
<div
className="cursor-pointer"
data-testid="grpc-send-request-button"
onClick={(e) => {
e.stopPropagation();
handleRun(e);

View File

@@ -48,7 +48,8 @@ const GrpcAuth = ({ item, collection }) => {
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionAuth = get(collection, 'root.request.auth');
const collectionRoot = collection?.draft?.root || collection?.root || {};
const collectionAuth = get(collectionRoot, 'request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',

Some files were not shown because too many files have changed in this diff Show More