Compare commits

...

143 Commits

Author SHA1 Message Date
Bijin A B
ae8851385f Merge branch 'main' of usebruno/bruno into workspaces 2025-12-04 04:17:45 +05:30
Bijin A B
6786f19d04 Merge pull request #6300 from usebruno/chore/oss-lint-fixes
chore: repo-wide lint fixes
2025-12-04 03:40:29 +05:30
Bijin A B
05fe8b1b27 chore: repo-wide lint fixes 2025-12-04 01:37:20 +05:30
sanish chirayath
b5722bf11c fix: wrap script in async IIFE to create isolated scope (#6229)
* fix: lexical scope for running scripts

* review fixes
2025-12-04 00:23:06 +05:30
Siddharth Gelera (reaper)
2b8da39bcf fix: update content security policy to allow inline scripts (#6139) 2025-12-03 19:57:10 +05:30
Pragadesh-45
38ba53be9f fix: stringify rawValue in brunoVarInfo (#6281) 2025-12-03 18:27:12 +05:30
Chirag Chandrashekhar
9159f523d9 Inbuilt Terminal (#6066)
* add: terminal

* added support for multiple terminal sessions and opening terminal from collection's working directory

* Use PowerShell as default shell on Windows

Replace cmd.exe with powershell.exe for terminal sessions on Windows.
Falls back to PWSH environment variable if set (for PowerShell Core).

* refactor(ui): improved session list by moving path display to hover for cleaner view

* chore: format

* refactor: improve terminal code quality and UI consistency

- Add terminal icon to 'Open in Terminal' dropdown item in CollectionItem
- Remove unused imports and functions (IconPlus, callIpc, canWriteToTerminal)
- Fix React key prop placement in SessionList component
- Replace deprecated substr with substring in terminal session ID generation
- Improve error handling for terminal cleanup on app quit
- Simplify terminal cleanup logic in window close handler

---------

Co-authored-by: naman-bruno <naman@usebruno.com>
Co-authored-by: Sid <siddharth@usebruno.com>
2025-12-03 17:48:28 +05:30
Abhishek S Lal
a3d2d35d2e feat: introduce REQUEST_TYPES constant and update item deletion logic (#6244)
- Added REQUEST_TYPES constant to centralize request type definitions.
- Updated deleteItem action to filter items based on REQUEST_TYPES and folders.
- Modified collection.js to handle REQUEST_TYPES during file resequence operations.
2025-12-03 16:07:38 +05:30
Chirag Chandrashekhar
9caef9e573 Fix: WS variable interpolation (#6184)
* 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

* feat: add automatic WebSocket reconnection on URL variable changes

- Detect changes to interpolated URL (including variable changes)
- Automatically disconnect and reconnect when interpolated URL changes
- Add debouncing (400ms) to prevent excessive reconnections
- Track previous interpolated URL to avoid unnecessary reconnects
- Store interpolated URL when connection becomes active
- Improve error handling and cleanup

* chore: removing diff

* Add WebSocket connection status IPC method

- Add connectionStatus() method to WsClient that returns detailed status
  ('disconnected', 'connecting', 'connected') instead of boolean
- Add renderer:ws:connection-status IPC handler in electron layer
- Add getWsConnectionStatus() utility function in network utils
- Provides more granular connection state information for UI components

* refactor: improve WebSocket connection status tracking in WsQueryUrl

- Replace boolean isConnectionActive with connectionStatus state ('disconnected', 'connecting', 'connected')
- Add useWsConnectionStatus hook to poll connection status every 2 seconds
- Refactor connection handlers: handleConnect, handleDisconnect, handleReconnect
- Update to use getWsConnectionStatus instead of isWsConnectionActive for more granular status
- Improve reconnect logic to handle URL variable interpolation changes
- Add proper connection status indicators in UI (connecting state with pulse animation)

* fix: improve WebSocket URL handling and request initialization

- Fix WebSocket URL state management by reading directly from item instead of local state
- Add handleUrlChange function to properly dispatch URL changes
- Fix interpolated URL change detection logic in useEffect
- Initialize params array for new WebSocket requests to prevent undefined errors
- Ensure params array is initialized when URL changes in draft/request
- Remove console.log statements and unused imports
- Update persistence test replacement URL to avoid port conflicts

These changes ensure WebSocket requests properly handle URL changes and
maintain consistent state between draft and saved requests.

* feat: refactor WebSocket connection status handling

---------

Co-authored-by: Sid <siddharth@usebruno.com>
2025-12-03 15:52:52 +05:30
naman-bruno
77d2fecfe6 fixes 2025-12-03 15:02:45 +05:30
naman-bruno
663b06d60f fix blank line 2025-12-03 14:11:25 +05:30
naman-bruno
dc3b074520 fixes: coderabbit 2025-12-03 14:04:58 +05:30
Siddharth Gelera (reaper)
893058067d chore: improve coderabbit review instructions (#6282)
* chore: add more review items

* Update CODING_STANDARDS.md

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-12-03 13:39:24 +05:30
naman-bruno
c7be4775b3 fixes: comments 2025-12-03 13:36:02 +05:30
Anoop M D
4a38f2d49f Update PR template to consent for AI usage 2025-12-03 03:49:02 +05:30
naman-bruno
72d5411df8 fixes 2025-12-03 00:48:19 +05:30
naman-bruno
d167be658f fix: close tests 2025-12-03 00:25:55 +05:30
naman-bruno
08c183b4ec fix: tests 2025-12-03 00:06:40 +05:30
Tarunkumar
d56e4f625b fixed min width issue so two windows can be used in parallel (#5863)
* fixed min width issue so two windows can be used in parallel

* updated the min height as well, so that Bruno can be viewed in quarter window on screen

---------

Co-authored-by: Tarunkumar N Bagmar <tarunkumar@Tarunkumars-MacBook-Air.local>
Co-authored-by: Anoop M D <anoop@usebruno.com>
2025-12-02 21:39:03 +05:30
Sid
9bbcf7ecbe fix:match to full string (#6272) 2025-12-02 17:54:53 +05:30
Chirag Chandrashekhar
ee4c923bc5 Merge pull request #6117 from chirag-bruno/bugfix/postman-export-omit-collection-vars
fix: Exporting Bruno collection as Postman collection omits collection variables
2025-12-02 16:47:15 +05:30
naman-bruno
bc82536a82 fix: ws and grpc request not saving (#6267) 2025-12-02 15:54:54 +05:30
Sid
b95ef99ef2 Merge pull request #6266 from usebruno/feat/copy-response-to-clipboard-5409
added copy button to copy response (#5409)
2025-12-02 15:47:18 +05:30
Pooja
2a251b1a62 added copy button to copy response (#6131)
* added copy button to copy response

* add: response action component

* fix: lint error

* add: playwright test

---------

Co-authored-by: Shashank Shekhar <48152748+sha5-git@users.noreply.github.com>
Co-authored-by: Sid <siddharth@usebruno.com>
2025-12-02 15:33:01 +05:30
Pooja
06a024a1d9 feat: add copy paste feature for folder (#6097)
* feat: add copy paste feature for folder

* add: playwright test

* fix

* fix

* add: keyboardFocusBg in light mode

* fix: copy paste in yaml collection

* improvement
2025-12-02 15:29:32 +05:30
Shashank Shekhar
8cee7bad39 added copy button to copy response (#5409)
Co-authored-by: Shashank Shekhar <48152748+sha5-git@users.noreply.github.com>
2025-12-02 14:53:51 +05:30
naman-bruno
c8d13f16c3 fixes 2025-12-02 14:42:00 +05:30
naman-bruno
399201bbc9 fixes 2025-12-02 14:41:51 +05:30
naman-bruno
93eae99302 init: workspaces 2025-12-02 14:41:39 +05:30
Chirag Chandrashekhar
bc4062b950 Bugfix/inaccurate process metrics (#6257) 2025-12-02 14:40:20 +05:30
Sid
b3ffc904ad feature/autoSave (#582) (#6211) 2025-12-02 14:32:34 +05:30
Sid
af707de684 fix(autoSave): minor bug fixes 2025-12-02 14:09:19 +05:30
Sid
be94224cfd Merge branch 'main' into feat/autosave-internal-582 2025-12-02 13:53:46 +05:30
Pooja
786a3414b8 remove: presets and response var (#6195) 2025-12-02 13:53:04 +05:30
Sid
7de56bd85c Merge branch 'main' into feat/autosave-internal-582 2025-12-02 13:52:12 +05:30
Pooja
4ce5debc4c feature/autoSave (#582) (#6225) 2025-12-02 13:46:06 +05:30
Pooja
8716e2b2a6 change: default cli sandbox to safe (#6198)
* change: default cli sandbox to safe
2025-12-02 13:09:16 +05:30
Sanjai Kumar
9d6486ba3e fix: update OAuth2 authorization header encoding to remove unnecessary URI encoding (#6263) 2025-12-02 12:56:08 +05:30
tlaloc911
dd72ee5d77 Add package directories to .gitignore (fix #6249) (#6250) 2025-12-01 09:23:44 +05:30
dependabot[bot]
7f204a8769 chore(deps): bump actions/upload-artifact from 4 to 5 (#5912)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-29 00:21:24 +05:30
dependabot[bot]
32990db3fb chore(deps): bump actions/checkout from 5 to 6 (#6192)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [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/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-29 00:18:20 +05:30
lohit
4f8d2c0c67 feat: default developer mode to nodevm and remove vm2 (#6187) 2025-11-29 00:04:55 +05:30
Anoop M D
8c06a229e9 Merge pull request #6238 from usebruno/chore/add-codemirror-placeholder
chore: added codemirror placeholder
2025-11-28 06:07:18 +05:30
Anoop M D
f367ea5a89 chore: added codemirror placeholder 2025-11-28 06:06:22 +05:30
Anoop M D
55a6af1ce3 Merge pull request #6236 from naman-bruno/bugfix/dropdown
rm: api spec from collection dropdown
2025-11-28 05:59:23 +05:30
Anoop M D
e9efcb48ac Merge pull request #6237 from usebruno/feat/design-revamp
feat: design revamp
2025-11-28 05:39:14 +05:30
Anoop M D
fa94efaa24 feat: design revamp 2025-11-28 05:10:29 +05:30
naman-bruno
1b2df9fba4 fix: dropdown 2025-11-28 02:09:48 +05:30
naman-bruno
7ee366eb81 Redesign dropdowns (#6235)
* redesign dropdowns

* fix: colors

---------

Co-authored-by: Anoop M D <anoop@usebruno.com>
2025-11-27 22:20:51 +05:30
Abhishek S Lal
59514127d5 Merge pull request #6171 from abhishek-bruno/style/update-font-size
style: updated font size to 13px using theme props.
2025-11-27 22:19:35 +05:30
Sanjai Kumar
9d98eb86c4 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-27 21:57:39 +05:30
Sanjai Kumar
bb0096eb38 feat: added multipart data formatting in timeline (#6185)
refactor: remove escapeHeaderValue function and enhance formatMultipartData utility
2025-11-27 18:12:20 +05:30
Sanjai Kumar
6e88671788 feat: add support for legacy request object translations in Postman converter (#6174) 2025-11-27 10:42:06 +05:30
naman-bruno
d17048f80c feat(opencollection): add YAML-based collection support (#6155)
* add: opencollection

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2025-11-27 00:55:08 +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
srikary12
086d0d98ef feature/autoSave (#582) 2025-11-26 12:17:52 +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
sajadoncode
b15c421270 docs: add farsi translation 2025-11-07 09:36:09 +01:00
adarshajit
1656e951fb feat: add stop request button in api url bar 2025-11-05 17:08:19 +05:30
944 changed files with 35796 additions and 12798 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

@@ -1,9 +1,10 @@
# Description
### Description
<!-- Explain here the changes your PR introduces and text to help us understand the context of this change. -->
### Contribution Checklist:
#### Contribution Checklist:
- [ ] **I've used AI significantly to create this pull request**
- [ ] **The pull request only addresses one issue or adds one feature.**
- [ ] **The pull request does not introduce any breaking changes**
- [ ] **I have added screenshots or gifs to help explain the change if applicable.**
@@ -12,6 +13,6 @@
Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.
### Publishing to New Package Managers
#### Publishing to New Package Managers
Please see [here](../publishing.md) for more information.

View File

@@ -23,4 +23,5 @@ runs:
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build:bruno-converters
npm run build:bruno-requests
npm run build:schema-types
npm run build:bruno-filestore

View File

@@ -25,7 +25,7 @@ jobs:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
@@ -40,7 +40,7 @@ jobs:
run: |
cd packages/bruno-tests/collection
npm install
bru run --env Prod --output junit.xml --format junit
bru run --env Prod --output junit.xml --format junit --sandbox developer
- name: Publish Test Report
uses: dorny/test-reporter@v2

View File

@@ -15,7 +15,7 @@ jobs:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- 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@v5
- uses: actions/checkout@v6
- 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@v5
- uses: actions/checkout@v6
- 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@v5
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
@@ -30,6 +30,7 @@ jobs:
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build --workspace=packages/bruno-converters
npm run build --workspace=packages/bruno-requests
npm run build --workspace=packages/bruno-schema-types
npm run build --workspace=packages/bruno-filestore
- name: Lint Check
@@ -66,7 +67,7 @@ jobs:
pull-requests: write
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
@@ -83,6 +84,7 @@ jobs:
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build --workspace=packages/bruno-converters
npm run build --workspace=packages/bruno-requests
npm run build --workspace=packages/bruno-schema-types
npm run build --workspace=packages/bruno-filestore
- name: Run Local Testbench
@@ -94,7 +96,7 @@ jobs:
run: |
cd packages/bruno-tests/collection
npm install
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit --sandbox developer
- name: Publish Test Report
uses: EnricoMi/publish-unit-test-result-action@v2
@@ -108,7 +110,7 @@ jobs:
timeout-minutes: 60
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
with:
node-version: v22.11.x
@@ -134,12 +136,13 @@ jobs:
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build:bruno-converters
npm run build:bruno-requests
npm run build:schema-types
npm run build:bruno-filestore
- name: Run Playwright tests
run: |
xvfb-run npm run test:e2e
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
if: ${{ !cancelled() }}
with:
name: playwright-report

9
.gitignore vendored
View File

@@ -51,3 +51,12 @@ bruno.iml
# Playwright
/blob-report/
# Development plan files
*.plan.md
# packages dist
packages/bruno-filestore/dist
packages/bruno-requests/dist
packages/bruno-schema-types/dist
packages/bruno-converters/dist

78
CODING_STANDARDS.md Normal file
View File

@@ -0,0 +1,78 @@
# 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! 🚀
## Tests
- Add tests for any new functionality or meaningful changes. If code is added, removed, or significantly modified, corresponding tests should be updated or created.
- Prioritise high-value tests over maximum coverage. Focus on testing behaviour that is critical, complex, or likely to break—dont chase coverage numbers for their own sake.
- Write behaviour-driven tests, not implementation-driven ones. Tests should validate real expected output and observable behaviour, not internal details or mocked-out logic unless absolutely necessary.
- Minimise mocking unless it meaningfully increases clarity or isolates external dependencies. Prefer real flows where practical; only mock external services, slow systems, or non-deterministic behaviour.
- Keep tests readable and maintainable. Optimise for clarity over cleverness. Name tests descriptively, keep setup minimal, and avoid unnecessary abstraction.
- Aim for tests that fail usefully. When a test fails, it should clearly indicate what behaviour broke and why.
- Cover both the “happy path” and the realistically problematic paths. Validate expected success behaviour, but also validate error handling, edge cases, and degraded-mode behaviour when appropriate.
- Ensure tests are deterministic and reproducible. No randomness, timing dependencies, or environment-specific assumptions without explicit control.
- Avoid overfitting tests to current behaviour if future flexibility matters. Only assert what needs to be true, not incidental details.
- Use consistent patterns and helper utilities where they improve clarity. Prefer shared test utilities over copy-pasted setup code, but only when it actually reduces complexity.
- Tests should be fast enough to run continuously. Avoid long-running operations unless absolutely necessary; prefer lightweight fixtures and isolated units.
## UI Specific instructions
### React
- Use styled component's theme prop to manage CSS colors and not CSS variables when in the context of a styled component or any react component using the styled component
- Styled Components are used as wrappers to define both self and children components style, tailwind classes are used specifically for layout based styles.
- Styled Component CSS might also change layout but tailwind classes shouldn't define colors.
## 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!!
@@ -69,11 +70,13 @@ npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
npm run build:schema-types
npm run build:bruno-filestore
# bundle js sandbox libraries
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
```
##### Option 2
```bash
@@ -94,18 +97,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

@@ -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

@@ -1,6 +1,6 @@
// eslint.config.js
const { defineConfig } = require("eslint/config");
const globals = require("globals");
const { defineConfig } = require('eslint/config');
const globals = require('globals');
const { fixupPluginRules } = require('@eslint/compat');
const eslintPluginDiff = require('eslint-plugin-diff');
@@ -11,6 +11,16 @@ const runESMImports = async () => {
};
module.exports = runESMImports().then(() => defineConfig([
// Global ignores - must be a standalone object with ONLY ignores
{
ignores: [
'**/node_modules/**/*',
'**/dist/**/*',
'**/*.bru',
'packages/bruno-js/src/sandbox/bundle-browser-rollup.js',
'packages/bruno-app/public/static/**/*'
]
},
{
plugins: {
'diff': fixupPluginRules(eslintPluginDiff),
@@ -34,13 +44,13 @@ module.exports = runESMImports().then(() => defineConfig([
'packages/bruno-converters/**/*.js',
'packages/bruno-electron/**/*.js',
'packages/bruno-filestore/**/*.ts',
'packages/bruno-schema-types/**/*.ts',
'packages/bruno-js/**/*.js',
'packages/bruno-lang/**/*.js',
'packages/bruno-requests/**/*.ts',
'packages/bruno-requests/**/*.js',
'packages/bruno-tests/**/*.{js,ts}'
],
processor: 'diff/diff',
rules: {
...stylistic.configs.customize({
indent: 2,
@@ -56,7 +66,7 @@ module.exports = runESMImports().then(() => defineConfig([
minElements: 2,
consistent: true
}],
'@stylistic/function-paren-newline': ['error', 'never'],
'@stylistic/function-paren-newline': ['off'],
'@stylistic/array-bracket-spacing': ['error', 'never'],
'@stylistic/arrow-spacing': ['error', { before: true, after: true }],
'@stylistic/function-call-spacing': ['error', 'never'],
@@ -64,12 +74,14 @@ module.exports = runESMImports().then(() => defineConfig([
'@stylistic/padding-line-between-statements': ['off'],
'@stylistic/semi-style': ['error', 'last'],
'@stylistic/max-len': ['off'],
'@stylistic/jsx-one-expression-per-line': ['off']
'@stylistic/jsx-one-expression-per-line': ['off'],
'@stylistic/max-statements-per-line': ['off'],
'@stylistic/no-mixed-operators': ['off']
}
},
{
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
ignores: ["**/*.config.js", "**/public/**/*"],
files: ['packages/bruno-app/**/*.{js,jsx,ts}'],
ignores: ['**/*.config.js', '**/public/**/*'],
languageOptions: {
globals: {
...globals.browser,
@@ -82,114 +94,114 @@ module.exports = runESMImports().then(() => defineConfig([
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
jsx: true
}
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
// It prevents lint errors when using CommonJS exports (module.exports) in Jest mocks.
files: ["packages/bruno-app/src/test-utils/mocks/codemirror.js"],
files: ['packages/bruno-app/src/test-utils/mocks/codemirror.js'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
...globals.jest
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-cli/**/*.js"],
ignores: ["**/*.config.js"],
files: ['packages/bruno-cli/**/*.js'],
ignores: ['**/*.config.js'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.jest
},
parserOptions: {
ecmaVersion: "latest"
},
ecmaVersion: 'latest'
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-common/**/*.ts"],
ignores: ["**/*.config.js", "**/dist/**/*"],
files: ['packages/bruno-common/**/*.ts'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.jest
},
parser: require("@typescript-eslint/parser"),
parser: require('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: "./packages/bruno-common/tsconfig.json",
},
ecmaVersion: 'latest',
sourceType: 'module',
project: './packages/bruno-common/tsconfig.json'
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-converters/**/*.js"],
ignores: ["**/*.config.js", "**/dist/**/*"],
files: ['packages/bruno-converters/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.jest
},
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
ecmaVersion: 'latest',
sourceType: 'module'
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-electron/**/*.js"],
ignores: ["**/*.config.js", "**/web/**/*"],
files: ['packages/bruno-electron/**/*.js'],
ignores: ['**/*.config.js', '**/web/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
...globals.jest
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-filestore/**/*.ts"],
ignores: ["**/*.config.js", "**/dist/**/*"],
files: ['packages/bruno-filestore/**/*.ts'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.jest
},
parser: require("@typescript-eslint/parser"),
parser: require('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: "./packages/bruno-filestore/tsconfig.json",
},
ecmaVersion: 'latest',
sourceType: 'module',
project: './packages/bruno-filestore/tsconfig.json'
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-js/**/*.js"],
ignores: ["**/*.config.js", "**/dist/**/*"],
files: ['packages/bruno-js/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
@@ -200,65 +212,65 @@ module.exports = runESMImports().then(() => defineConfig([
typeDetectGlobalObject: false
},
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
ecmaVersion: 'latest',
sourceType: 'module'
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-lang/**/*.js"],
ignores: ["**/*.config.js", "**/dist/**/*"],
files: ['packages/bruno-lang/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.jest
},
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
ecmaVersion: 'latest',
sourceType: 'module'
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-requests/**/*.ts"],
ignores: ["**/*.config.js", "**/dist/**/*"],
files: ['packages/bruno-requests/**/*.ts'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.jest
},
parser: require("@typescript-eslint/parser"),
parser: require('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
project: "./packages/bruno-requests/tsconfig.json",
},
ecmaVersion: 'latest',
sourceType: 'module',
project: './packages/bruno-requests/tsconfig.json'
}
},
rules: {
"no-undef": "error",
},
'no-undef': 'error'
}
},
{
files: ["packages/bruno-requests/**/*.js"],
ignores: ["**/*.config.js", "**/dist/**/*"],
files: ['packages/bruno-requests/**/*.js'],
ignores: ['**/*.config.js', '**/dist/**/*'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
...globals.jest
},
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
ecmaVersion: 'latest',
sourceType: 'module'
}
},
rules: {
"no-undef": "error",
},
},
'no-undef': 'error'
}
}
]));

502
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"packages/bruno-common",
"packages/bruno-converters",
"packages/bruno-schema",
"packages/bruno-schema-types",
"packages/bruno-query",
"packages/bruno-js",
"packages/bruno-lang",
@@ -3554,6 +3555,16 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@develar/schema-utils/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/@develar/schema-utils/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -3906,7 +3917,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 +4480,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": {
@@ -5067,6 +5078,98 @@
"jsep": "^0.4.0||^1.0.0"
}
},
"node_modules/@lydell/node-pty": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty/-/node-pty-1.1.0.tgz",
"integrity": "sha512-VDD8LtlMTOrPKWMXUAcB9+LTktzuunqrMwkYR1DMRBkS6LQrCt+0/Ws1o2rMml/n3guePpS7cxhHF7Nm5K4iMw==",
"license": "MIT",
"optionalDependencies": {
"@lydell/node-pty-darwin-arm64": "1.1.0",
"@lydell/node-pty-darwin-x64": "1.1.0",
"@lydell/node-pty-linux-arm64": "1.1.0",
"@lydell/node-pty-linux-x64": "1.1.0",
"@lydell/node-pty-win32-arm64": "1.1.0",
"@lydell/node-pty-win32-x64": "1.1.0"
}
},
"node_modules/@lydell/node-pty-darwin-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-arm64/-/node-pty-darwin-arm64-1.1.0.tgz",
"integrity": "sha512-7kFD+owAA61qmhJCtoMbqj3Uvff3YHDiU+4on5F2vQdcMI3MuwGi7dM6MkFG/yuzpw8LF2xULpL71tOPUfxs0w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@lydell/node-pty-darwin-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-x64/-/node-pty-darwin-x64-1.1.0.tgz",
"integrity": "sha512-XZdvqj5FjAMjH8bdp0YfaZjur5DrCIDD1VYiE9EkkYVMDQqRUPHYV3U8BVEQVT9hYfjmpr7dNaELF2KyISWSNA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@lydell/node-pty-linux-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-arm64/-/node-pty-linux-arm64-1.1.0.tgz",
"integrity": "sha512-yyDBmalCfHpLiQMT2zyLcqL2Fay4Xy7rIs8GH4dqKLnEviMvPGOK7LADVkKAsbsyXBSISL3Lt1m1MtxhPH6ckg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@lydell/node-pty-linux-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-x64/-/node-pty-linux-x64-1.1.0.tgz",
"integrity": "sha512-NcNqRTD14QT+vXcEuqSSvmWY+0+WUBn2uRE8EN0zKtDpIEr9d+YiFj16Uqds6QfcLCHfZmC+Ls7YzwTaqDnanA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@lydell/node-pty-win32-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-arm64/-/node-pty-win32-arm64-1.1.0.tgz",
"integrity": "sha512-JOMbCou+0fA7d/m97faIIfIU0jOv8sn2OR7tI45u3AmldKoKoLP8zHY6SAvDDnI3fccO1R2HeR1doVjpS7HM0w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@lydell/node-pty-win32-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-x64/-/node-pty-win32-x64-1.1.0.tgz",
"integrity": "sha512-3N56BZ+WDFnUMYRtsrr7Ky2mhWGl9xXcyqR6cexfuCqcz9RNWL+KoXRv/nZylY5dYaXkft4JaR1uVu+roiZDAw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@malept/flatpak-bundler": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz",
@@ -5566,6 +5669,13 @@
"node": ">= 8"
}
},
"node_modules/@opencollection/types": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.1.0.tgz",
"integrity": "sha512-/v64ShE+KyDUAfAlO6Qd5wBwPArd603VC44eife/CdmrtPUSIiFBYcZ9gxAD7LlW99J36wb5IkMpKFDvViINiA==",
"dev": true,
"license": "MIT"
},
"node_modules/@parcel/watcher": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz",
@@ -6964,25 +7074,6 @@
"@rsbuild/core": "1.x"
}
},
"node_modules/@rsbuild/plugin-sass/node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/@rsbuild/plugin-sass/node_modules/postcss": {
"version": "8.4.49",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
@@ -8232,12 +8323,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 +8458,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 +8473,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",
@@ -8422,6 +8498,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/nanoid": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/nanoid/-/nanoid-2.1.0.tgz",
"integrity": "sha512-xdkn/oRTA0GSNPLIKZgHWqDTWZsVrieKomxJBOQUK9YDD+zfSgmwD5t4WJYra5S7XyhTw7tfvwznW+pFexaepQ==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "22.15.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz",
@@ -8858,6 +8943,10 @@
"resolved": "packages/bruno-schema",
"link": true
},
"node_modules/@usebruno/schema-types": {
"resolved": "packages/bruno-schema-types",
"link": true
},
"node_modules/@usebruno/tests": {
"resolved": "packages/bruno-tests",
"link": true
@@ -8866,22 +8955,6 @@
"resolved": "packages/bruno-toml",
"link": true
},
"node_modules/@usebruno/vm2": {
"version": "3.9.19",
"resolved": "https://registry.npmjs.org/@usebruno/vm2/-/vm2-3.9.19.tgz",
"integrity": "sha512-WIrR9ODN2xkwUEoJb3awhCZO2dTgq8NWoObofAGuzFQOQ27rw96d2GJU/T8OKcygjfJiNei9nuqidyMh81kiug==",
"license": "MIT",
"dependencies": {
"acorn": "^8.7.0",
"acorn-walk": "^8.2.0"
},
"bin": {
"vm2": "bin/vm2"
},
"engines": {
"node": ">=6.0"
}
},
"node_modules/@web/rollup-plugin-copy": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@web/rollup-plugin-copy/-/rollup-plugin-copy-0.5.1.tgz",
@@ -9152,6 +9225,21 @@
"node": ">=10.0.0"
}
},
"node_modules/@xterm/addon-fit": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
"integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
},
"node_modules/@xterm/xterm": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
"license": "MIT"
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -9224,6 +9312,7 @@
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -9257,6 +9346,7 @@
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.11.0"
@@ -9321,16 +9411,6 @@
}
}
},
"node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/amdefine": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-0.0.8.tgz",
@@ -11654,7 +11734,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": {
@@ -14235,9 +14315,9 @@
}
},
"node_modules/fast-json-format": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/fast-json-format/-/fast-json-format-0.2.0.tgz",
"integrity": "sha512-HdcxHsca6fqk7vt7Ak4a8JTWZQt3yEPwXk8hBPKCg8PLgX7DmXEbwgv7vLLiinAQxTcSFNyz5OiWr8m4r/0tKA==",
"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": {
@@ -15183,9 +15263,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": {
@@ -15312,20 +15392,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": {
@@ -17804,9 +17882,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"
@@ -18634,12 +18712,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",
@@ -19436,6 +19508,24 @@
"integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==",
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/native-reg": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/native-reg/-/native-reg-1.1.1.tgz",
@@ -21264,25 +21354,6 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/posthog-node": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.2.1.tgz",
@@ -22291,7 +22362,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"
},
@@ -23758,6 +23829,16 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/schema-utils/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/schema-utils/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -26867,6 +26948,8 @@
"@usebruno/common": "0.1.0",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.7.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"classnames": "^2.3.1",
"codemirror": "5.65.2",
"codemirror-graphql": "2.1.1",
@@ -26874,7 +26957,7 @@
"dompurify": "^3.2.4",
"escape-html": "^1.0.3",
"fast-fuzzy": "^1.12.0",
"fast-json-format": "~0.2.0",
"fast-json-format": "~0.4.0",
"file": "^0.2.2",
"file-dialog": "^0.0.8",
"file-saver": "^2.0.5",
@@ -26883,6 +26966,7 @@
"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",
"idb": "^7.0.0",
@@ -26893,6 +26977,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",
@@ -28442,6 +28527,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",
@@ -28449,24 +28543,6 @@
"dev": true,
"license": "MIT"
},
"packages/bruno-app/node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"packages/bruno-app/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
@@ -28490,6 +28566,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",
@@ -28533,7 +28615,6 @@
"@usebruno/js": "0.12.0",
"@usebruno/lang": "0.12.0",
"@usebruno/requests": "^0.1.0",
"@usebruno/vm2": "^3.9.13",
"aws4-axios": "^3.3.0",
"axios": "^1.8.3",
"axios-ntlm": "^1.4.2",
@@ -28545,7 +28626,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",
@@ -30169,7 +30250,7 @@
"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",
@@ -30239,24 +30320,6 @@
"node": ">=16 || 14 >=14.17"
}
},
"packages/bruno-converters/node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"packages/bruno-converters/node_modules/rimraf": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
@@ -30280,6 +30343,7 @@
"@aws-sdk/credential-providers": "3.750.0",
"@grpc/grpc-js": "^1.13.2",
"@grpc/proto-loader": "^0.7.13",
"@lydell/node-pty": "^1.1.0",
"@usebruno/common": "0.1.0",
"@usebruno/converters": "^0.1.0",
"@usebruno/filestore": "^0.1.0",
@@ -30288,7 +30352,6 @@
"@usebruno/node-machine-id": "^2.0.0",
"@usebruno/requests": "^0.1.0",
"@usebruno/schema": "0.7.0",
"@usebruno/vm2": "^3.9.13",
"about-window": "^1.15.2",
"aws4-axios": "^3.3.0",
"axios": "^1.8.3",
@@ -30310,7 +30373,7 @@
"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",
@@ -31550,7 +31613,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",
@@ -31597,7 +31660,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"
@@ -31728,7 +31791,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"
@@ -31811,24 +31874,6 @@
"dev": true,
"license": "MIT"
},
"packages/bruno-electron/node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"packages/bruno-electron/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
@@ -31858,20 +31903,27 @@
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@types/nanoid": "^2.1.0",
"@usebruno/lang": "0.12.0",
"lodash": "^4.17.21"
"ajv": "^8.17.1",
"lodash": "^4.17.21",
"yaml": "^2.3.4"
},
"devDependencies": {
"@babel/preset-env": "^7.22.0",
"@babel/preset-typescript": "^7.22.0",
"@opencollection/types": "0.1.0",
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
"@rollup/plugin-typescript": "^12.1.2",
"@types/jest": "^29.5.11",
"@types/lodash": "^4.14.191",
"@types/node": "^24.1.0",
"@usebruno/schema-types": "0.0.1",
"babel-jest": "^29.7.0",
"jest": "^29.2.0",
"nanoid": "3.3.8",
"rimraf": "^3.0.2",
"rollup": "3.29.5",
"rollup-plugin-dts": "^5.0.0",
@@ -31880,6 +31932,33 @@
"typescript": "^4.8.4"
}
},
"packages/bruno-filestore/node_modules/@rollup/plugin-typescript": {
"version": "12.3.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz",
"integrity": "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/pluginutils": "^5.1.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.14.0||^3.0.0||^4.0.0",
"tslib": "*",
"typescript": ">=3.7.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
},
"tslib": {
"optional": true
}
}
},
"packages/bruno-filestore/node_modules/@types/node": {
"version": "24.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
@@ -31960,6 +32039,18 @@
"dev": true,
"license": "MIT"
},
"packages/bruno-filestore/node_modules/yaml": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"packages/bruno-graphql-docs": {
"name": "@usebruno/graphql-docs",
"version": "0.1.0",
@@ -32046,9 +32137,6 @@
"@rollup/plugin-node-resolve": "^15.0.1",
"rollup": "3.29.5",
"rollup-plugin-terser": "^7.0.2"
},
"peerDependencies": {
"@usebruno/vm2": "^3.9.13"
}
},
"packages/bruno-js/node_modules/axios": {
@@ -32062,24 +32150,6 @@
"proxy-from-env": "^1.1.0"
}
},
"packages/bruno-js/node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"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",
@@ -32129,7 +32199,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",
@@ -32247,21 +32318,26 @@
"yup": "^0.32.11"
}
},
"packages/bruno-schema/node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"packages/bruno-schema-types": {
"name": "@usebruno/schema-types",
"version": "0.0.1",
"license": "MIT",
"devDependencies": {
"typescript": "^5.0.0"
}
},
"packages/bruno-schema-types/node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"nanoid": "bin/nanoid.cjs"
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
"node": ">=14.17"
}
},
"packages/bruno-tests": {
@@ -32277,7 +32353,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

@@ -8,6 +8,7 @@
"packages/bruno-common",
"packages/bruno-converters",
"packages/bruno-schema",
"packages/bruno-schema-types",
"packages/bruno-query",
"packages/bruno-js",
"packages/bruno-lang",
@@ -61,6 +62,7 @@
"build:bruno-converters": "npm run build --workspace=packages/bruno-converters",
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
"build:schema-types": "npm run build --workspace=packages/bruno-schema-types",
"build:electron": "node ./scripts/build-electron.js",
"build:electron:mac": "./scripts/build-electron.sh mac",
"build:electron:win": "./scripts/build-electron.sh win",

View File

@@ -6,4 +6,4 @@ module.exports = {
}]
],
plugins: ['babel-plugin-styled-components']
};
};

View File

@@ -1,10 +1,10 @@
module.exports = {
rootDir: '.',
transform: {
'^.+\\.[jt]sx?$': 'babel-jest',
'^.+\\.[jt]sx?$': 'babel-jest'
},
transformIgnorePatterns: [
"/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/",
'/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/'
],
moduleNameMapper: {
'^assets/(.*)$': '<rootDir>/src/assets/$1',
@@ -22,9 +22,9 @@ module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@testing-library/jest-dom'],
setupFiles: [
'<rootDir>/jest.setup.js',
'<rootDir>/jest.setup.js'
],
testMatch: [
'<rootDir>/src/**/*.spec.[jt]s?(x)'
]
};
};

View File

@@ -21,6 +21,8 @@
"@usebruno/common": "0.1.0",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.7.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"classnames": "^2.3.1",
"codemirror": "5.65.2",
"codemirror-graphql": "2.1.1",
@@ -28,7 +30,7 @@
"dompurify": "^3.2.4",
"escape-html": "^1.0.3",
"fast-fuzzy": "^1.12.0",
"fast-json-format": "~0.2.0",
"fast-json-format": "~0.4.0",
"file": "^0.2.2",
"file-dialog": "^0.0.8",
"file-saver": "^2.0.5",
@@ -37,6 +39,7 @@
"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",
"idb": "^7.0.0",
@@ -47,6 +50,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

@@ -1,6 +1,6 @@
const darkTheme = {
brand: '#546de5',
text: 'rgb(52 52 52)',
'brand': '#546de5',
'text': 'rgb(52 52 52)',
'primary-text': '#ffffff',
'primary-theme': '#1e1e1e',
'secondary-text': '#929292',

View File

@@ -1,6 +1,6 @@
const lightTheme = {
brand: '#546de5',
text: 'rgb(52 52 52)',
'brand': '#546de5',
'text': 'rgb(52 52 52)',
'primary-text': 'rgb(52 52 52)',
'primary-theme': '#ffffff',
'secondary-text': '#929292',

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

@@ -6,7 +6,7 @@ import StyledWrapper from './StyledWrapper';
const BrunoSupport = ({ onClose }) => {
return (
<StyledWrapper>
<Modal size="sm" title={'Support'} handleCancel={onClose} hideFooter={true}>
<Modal size="sm" title="Support" handleCancel={onClose} hideFooter={true}>
<div className="collection-options">
<div className="mt-2">
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end">

View File

@@ -18,6 +18,33 @@ const StyledWrapper = styled.div`
flex-direction: column-reverse;
}
.CodeMirror-placeholder {
color: ${(props) => props.theme.text} !important;
opacity: 0.5 !important;
}
.CodeMirror-linenumber {
text-align: left !important;
padding-left: 3px !important;
}
/* Override default lint highlight background when emphasizing the gutter */
.CodeMirror-lint-line-error,
.CodeMirror-lint-line-warning {
background: none !important;
}
/* Style line numbers when there's a lint issue */
.CodeMirror-lint-line-error .CodeMirror-linenumber {
color: #d32f2f !important;
text-decoration: underline;
}
.CodeMirror-lint-line-warning .CodeMirror-linenumber {
color: #f57c00 !important;
text-decoration: underline;
}
/* Removes the glow outline around the folded json */
.CodeMirror-foldmarker {
text-shadow: none;
@@ -73,41 +100,48 @@ const StyledWrapper = styled.div`
}
}
.cm-s-monokai span.cm-property,
.cm-s-monokai span.cm-attribute {
color: #9cdcfe !important;
}
.cm-s-monokai span.cm-string {
color: #ce9178 !important;
}
.cm-s-monokai span.cm-number {
color: #b5cea8 !important;
}
.cm-s-monokai span.cm-atom {
color: #569cd6 !important;
.cm-s-default, .cm-s-monokai {
span.cm-def {
color: ${(props) => props.theme.codemirror.tokens.definition} !important;
}
span.cm-property {
color: ${(props) => props.theme.codemirror.tokens.property} !important;
}
span.cm-string {
color: ${(props) => props.theme.codemirror.tokens.string} !important;
}
span.cm-number {
color: ${(props) => props.theme.codemirror.tokens.number} !important;
}
span.cm-atom {
color: ${(props) => props.theme.codemirror.tokens.atom} !important;
}
span.cm-variable {
color: ${(props) => props.theme.codemirror.tokens.variable} !important;
}
span.cm-keyword {
color: ${(props) => props.theme.codemirror.tokens.keyword} !important;
}
span.cm-comment {
color: ${(props) => props.theme.codemirror.tokens.comment} !important;
}
span.cm-operator {
color: ${(props) => props.theme.codemirror.tokens.operator} !important;
}
}
/* Variable validation colors */
.cm-variable-valid {
color: green;
color: #5fad89 !important; /* Soft sage */
}
.cm-variable-invalid {
color: red;
color: #d17b7b !important; /* Soft coral */
}
.CodeMirror-search-hint {
display: inline;
}
.cm-s-default span.cm-property {
color: #1f61a0 !important;
}
.cm-s-default span.cm-variable {
color: #397d13 !important;
}
//matching bracket fix
.CodeMirror-matchingbracket {
@@ -126,6 +160,31 @@ const StyledWrapper = styled.div`
.cm-search-current {
background: rgba(255, 193, 7, 0.4);
}
.lint-error-tooltip {
position: fixed;
z-index: 10000;
background: ${(props) => props.theme.codemirror.bg};
border-radius: ${(props) => props.theme.border.radius.base};
padding: 8px 12px;
max-width: 400px;
box-shadow: ${(props) => props.theme.shadow.sm};
font-size: ${(props) => props.theme.font.size.xs};
line-height: 1.5;
pointer-events: none;
.lint-tooltip-message {
padding: 2px 0;
}
.lint-tooltip-message.error {
color: ${(props) => props.theme.colors.text.danger};
}
.lint-tooltip-message.warning {
color: ${(props) => props.theme.colors.text.warning};
}
}
`;
export default StyledWrapper;

View File

@@ -14,6 +14,8 @@ import * as jsonlint from '@prantlf/jsonlint';
import { JSHINT } from 'jshint';
import stripJsonComments from 'strip-json-comments';
import { getAllVariables } from 'utils/collections';
import { setupLinkAware } from 'utils/codemirror/linkAware';
import { setupLintErrorTooltip } from 'utils/codemirror/lint-errors';
import CodeMirrorSearch from 'components/CodeMirrorSearch';
const CodeMirror = require('codemirror');
@@ -36,7 +38,8 @@ export default class CodeEditor extends React.Component {
this.lintOptions = {
esversion: 11,
expr: true,
asi: true
asi: true,
highlightLines: true
};
this.state = {
@@ -49,19 +52,22 @@ export default class CodeEditor extends React.Component {
const editor = (this.editor = CodeMirror(this._node, {
value: this.props.value || '',
placeholder: '...',
lineNumbers: true,
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,
showCursorWhenSelecting: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
lint: this.lintOptions,
readOnly: this.props.readOnly,
scrollbarStyle: 'overlay',
@@ -99,7 +105,7 @@ export default class CodeEditor extends React.Component {
},
'Cmd-H': 'replace',
'Ctrl-H': 'replace',
Tab: function (cm) {
'Tab': function (cm) {
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
? cm.execCommand('indentMore')
: cm.replaceSelection(' ', 'end');
@@ -145,7 +151,7 @@ export default class CodeEditor extends React.Component {
} else if (this.props.mode == 'application/xml') {
var doc = new DOMParser();
try {
//add header element and remove prefix namespaces for DOMParser
// add header element and remove prefix namespaces for DOMParser
var dcm = doc.parseFromString(
'<a> ' + internal.replace(/(?<=\<|<\/)\w+:/g, '') + '</a>',
'application/xml'
@@ -182,7 +188,7 @@ export default class CodeEditor extends React.Component {
}
return found;
});
if (editor) {
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
editor.on('change', this._onEdit);
@@ -191,7 +197,7 @@ export default class CodeEditor extends React.Component {
this.addOverlay();
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
// Setup AutoComplete Helper for all modes
const autoCompleteOptions = {
showHintsFor: this.props.showHintsFor,
@@ -202,6 +208,11 @@ export default class CodeEditor extends React.Component {
editor,
autoCompleteOptions
);
setupLinkAware(editor);
// Setup lint error tooltip on line number hover
this.cleanupLintErrorTooltip = setupLintErrorTooltip(editor);
}
}
@@ -227,6 +238,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) {
@@ -254,8 +275,13 @@ export default class CodeEditor extends React.Component {
componentWillUnmount() {
if (this.editor) {
this.editor?._destroyLinkAware?.();
this.editor.off('change', this._onEdit);
this.editor.off('scroll', this.onScroll);
// Clean up lint error tooltip
this.cleanupLintErrorTooltip?.();
this.editor = null;
}
}
@@ -290,6 +316,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

@@ -10,10 +10,10 @@ jest.mock('codemirror', () => {
const MOCK_THEME = {
codemirror: {
bg: "#1e1e1e",
border: "#333",
bg: '#1e1e1e',
border: '#333'
},
textLink: "#007acc",
textLink: '#007acc'
};
const setupEditorState = (editor, { value, cursorPosition }) => {
@@ -27,8 +27,8 @@ const setupEditorState = (editor, { value, cursorPosition }) => {
});
editor.state = {
completionActive: null,
}
completionActive: null
};
};
const setupEditorWithRef = () => {
@@ -47,5 +47,5 @@ describe('CodeEditor', () => {
jest.resetModules();
});
it("add CodeEditor related tests here", () => {});
});
it('add CodeEditor related tests here', () => {});
});

View File

@@ -27,7 +27,7 @@ const StyledWrapper = styled.div`
border: none;
outline: none;
padding: 1px 2px;
font-size: 13px;
font-size: ${(props) => props.theme.font.size.base};
margin: 0 1px;
height: 28px;
}
@@ -50,7 +50,7 @@ const StyledWrapper = styled.div`
.searchbar-result-count {
min-width: 28px;
text-align: center;
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
color: #aaa;
margin: 0 8px 0 1px;
white-space: nowrap;
@@ -74,7 +74,7 @@ const StyledWrapper = styled.div`
color: inherit;
border: none;
outline: none;
font-size: 13px;
font-size: ${(props) => props.theme.font.size.base};
padding: 1px 2px;
min-width: 80px;
}

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
}
.single-line-editor-wrapper {

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 (
@@ -43,16 +43,16 @@ const ApiKeyAuth = ({ collection }) => {
};
useEffect(() => {
!apikeyAuth?.placement &&
dispatch(
updateCollectionAuth({
mode: 'apikey',
collectionUid: collection.uid,
content: {
placement: 'header'
}
})
);
!apikeyAuth?.placement
&& dispatch(
updateCollectionAuth({
mode: 'apikey',
collectionUid: collection.uid,
content: {
placement: 'header'
}
})
);
}, [apikeyAuth]);
return (

View File

@@ -1,7 +1,7 @@
import styled from 'styled-components';
const Wrapper = styled.div`
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
.auth-mode-selector {
background: transparent;

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 (
@@ -87,7 +87,7 @@ const AuthMode = ({ collection }) => {
}}
>
NTLM Auth
</div>
</div>
<div
className="dropdown-item"
onClick={() => {

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
}
.single-line-editor-wrapper {

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

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
}
.single-line-editor-wrapper {

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

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
}
.single-line-editor-wrapper {

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

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
}
.single-line-editor-wrapper {

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

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
}
.single-line-editor-wrapper {

View File

@@ -6,25 +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 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) => {
dispatch(
@@ -67,10 +60,7 @@ const NTLMAuth = ({ collection }) => {
}
})
);
};
};
return (
<StyledWrapper className="mt-2 w-full">

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
}
.single-line-editor-wrapper {
max-width: 400px;

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';
@@ -10,14 +10,14 @@ import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCr
import OAuth2Implicit from 'components/RequestPane/Auth/OAuth2/Implicit/index';
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
const GrantTypeComponentMap = ({collection }) => {
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

@@ -2,7 +2,7 @@ import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
}
.single-line-editor-wrapper {

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,16 @@ 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) {
@@ -36,7 +35,7 @@ const Auth = ({ collection }) => {
}
case 'ntlm': {
return <NTLMAuth collection={collection} />;
}
}
case 'oauth2': {
return <OAuth2 collection={collection} />;
}

View File

@@ -9,8 +9,18 @@ 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 = ({ collection, 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();
@@ -29,7 +39,7 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }
domain: Yup.string()
.required()
.trim()
.test('not-empty-after-trim', 'Domain is required', value => value && value.trim().length > 0),
.test('not-empty-after-trim', 'Domain is required', (value) => value && value.trim().length > 0),
type: Yup.string().required().oneOf(['cert', 'pfx']),
certFilePath: Yup.string().when('type', {
is: (type) => type == 'cert',
@@ -63,7 +73,19 @@ const ClientCertSettings = ({ collection, 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();
}
@@ -81,9 +103,15 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }
};
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 handleTypeChange = (e) => {
@@ -99,34 +127,49 @@ const ClientCertSettings = ({ collection, 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>
<h1 className="font-semibold">Client Certificates</h1>
<h1 className="font-medium">Client Certificates</h1>
<ul className="mt-4">
{!clientCertConfig.length
? 'No client certificates added'
: clientCertConfig.map((clientCert, index) => (
<li key={`client-cert-${index}`} className="flex items-center available-certificates p-2 rounded-lg mb-2">
<div className="flex items-center w-full justify-between">
<div className="flex w-full items-center">
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
{clientCert.domain}
<li key={`client-cert-${index}`} className="flex items-center available-certificates p-2 rounded-lg mb-2">
<div className="flex items-center w-full justify-between">
<div className="flex w-full items-center">
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
{clientCert.domain}
</div>
<div className="flex w-full items-center">
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
</div>
<button onClick={() => handleRemove(index)} className="remove-certificate ml-2">
<IconTrash size={18} strokeWidth={1.5} />
</button>
</div>
<div className="flex w-full items-center">
<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">
<IconTrash size={18} strokeWidth={1.5} />
</button>
</div>
</li>
))}
</li>
))}
</ul>
<h1 className="font-semibold mt-8 mb-2">Add Client Certificate</h1>
<h1 className="font-medium mt-8 mb-2">Add Client Certificate</h1>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="domain">
@@ -329,10 +372,14 @@ const ClientCertSettings = ({ collection, clientCertConfig, onUpdate, onRemove }
<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,28 +31,28 @@ 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();
}
};
return (
<StyledWrapper className="h-full w-full relative flex flex-col">
<div className='flex flex-row w-full justify-between items-center mb-4'>
<div className='text-lg font-medium flex items-center gap-2'>
<div className="flex flex-row w-full justify-between items-center mb-4">
<div className="text-lg font-medium flex items-center gap-2">
<IconFileText size={20} strokeWidth={1.5} />
Documentation
</div>
<div className='flex flex-row gap-2 items-center justify-center'>
<div className="flex flex-row gap-2 items-center justify-center">
{isEditing ? (
<>
<div className="editing-mode" role="tab" onClick={handleDiscardChanges}>
@@ -81,14 +81,13 @@ const Docs = ({ collection }) => {
fontSize={get(preferences, 'font.codeFontSize')}
/>
) : (
<div className='h-full overflow-auto pl-1'>
<div className='h-[1px] min-h-[500px]'>
<div className="h-full overflow-auto pl-1">
<div className="h-[1px] min-h-[500px]">
{
docs?.length > 0 ?
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
:
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={documentationPlaceholder} />
}
docs?.length > 0
? <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
: <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={documentationPlaceholder} />
}
</div>
</div>
)}
@@ -98,7 +97,6 @@ const Docs = ({ collection }) => {
export default Docs;
const documentationPlaceholder = `
Welcome to your collection documentation! This space is designed to help you document your API collection effectively.

View File

@@ -6,7 +6,7 @@ const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
font-weight: 500;
table-layout: fixed;
thead,
@@ -16,7 +16,7 @@ const Wrapper = styled.div`
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
user-select: none;
}
td {
@@ -33,7 +33,7 @@ const Wrapper = styled.div`
}
.btn-add-header {
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
}
input[type='text'] {

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': {
@@ -122,8 +123,7 @@ const Headers = ({ collection }) => {
},
header,
'name'
)
}
)}
autocomplete={headerAutoCompleteList}
collection={collection}
/>
@@ -142,8 +142,7 @@ const Headers = ({ collection }) => {
},
header,
'value'
)
}
)}
collection={collection}
autocomplete={MimeTypes}
/>

View File

@@ -1,9 +1,9 @@
import React from "react";
import React from 'react';
import { getTotalRequestCountInCollection } from 'utils/collections/';
import { IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
import { areItemsLoading, getItemsLoadStats } from "utils/collections/index";
import { useState } from "react";
import ShareCollection from "components/ShareCollection/index";
import { IconBox, IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
import { areItemsLoading, getItemsLoadStats } from 'utils/collections/index';
import { useState } from 'react';
import ShareCollection from 'components/ShareCollection/index';
const Info = ({ collection }) => {
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
@@ -11,23 +11,23 @@ const Info = ({ collection }) => {
const isCollectionLoading = areItemsLoading(collection);
const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
const handleToggleShowShareCollectionModal = (value) => (e) => {
toggleShowShareCollectionModal(value);
}
};
return (
<div className="w-full flex flex-col h-fit">
<div className="rounded-lg py-6">
<div className="grid gap-6">
<div className="grid gap-5">
{/* Location Row */}
<div className="flex items-start">
<div className="flex-shrink-0 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<IconFolder className="w-5 h-5 text-blue-500" stroke={1.5} />
</div>
<div className="ml-4">
<div className="font-semibold text-sm">Location</div>
<div className="mt-1 text-sm text-muted break-all">
<div className="font-medium">Location</div>
<div className="mt-1 text-muted break-all text-xs">
{collection.pathname}
</div>
</div>
@@ -39,8 +39,8 @@ const Info = ({ collection }) => {
<IconWorld className="w-5 h-5 text-green-500" stroke={1.5} />
</div>
<div className="ml-4">
<div className="font-semibold text-sm">Environments</div>
<div className="mt-1 text-sm text-muted">
<div className="font-medium">Environments</div>
<div className="mt-1 text-muted text-xs">
{collection.environments?.length || 0} environment{collection.environments?.length !== 1 ? 's' : ''} configured
</div>
</div>
@@ -52,10 +52,10 @@ const Info = ({ collection }) => {
<IconApi className="w-5 h-5 text-purple-500" stroke={1.5} />
</div>
<div className="ml-4">
<div className="font-semibold text-sm">Requests</div>
<div className="mt-1 text-sm text-muted">
<div className="font-medium">Requests</div>
<div className="mt-1 text-muted text-xs">
{
isCollectionLoading? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
isCollectionLoading ? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
}
</div>
</div>
@@ -66,8 +66,8 @@ const Info = ({ collection }) => {
<IconShare className="w-5 h-5 text-indigo-500" stroke={1.5} />
</div>
<div className="ml-4 h-full flex flex-col justify-start">
<div className="font-semibold text-sm h-fit my-auto">Share</div>
<div className="mt-1 text-sm group-hover:underline text-link">
<div className="font-medium h-fit my-auto">Share</div>
<div className="group-hover:underline text-link text-xs">
Share Collection
</div>
</div>
@@ -79,4 +79,4 @@ const Info = ({ collection }) => {
);
};
export default Info;
export default Info;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { flattenItems } from "utils/collections";
import { flattenItems } from 'utils/collections';
import { IconAlertTriangle } from '@tabler/icons';
import StyledWrapper from "./StyledWrapper";
import StyledWrapper from './StyledWrapper';
import { useDispatch, useSelector } from 'react-redux';
import { isItemARequest, itemIsOpenedInTabs } from 'utils/tabs/index';
import { getDefaultRequestPaneTab } from 'utils/collections/index';
@@ -12,13 +12,13 @@ const RequestsNotLoaded = ({ collection }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const flattenedItems = flattenItems(collection.items);
const itemsFailedLoading = flattenedItems?.filter(item => item?.partial && !item?.loading);
const itemsFailedLoading = flattenedItems?.filter((item) => item?.partial && !item?.loading);
if (!itemsFailedLoading?.length) {
return null;
}
const handleRequestClick = (item) => e => {
const handleRequestClick = (item) => (e) => {
e.preventDefault();
if (isItemARequest(item)) {
dispatch(hideHomePage());
@@ -39,7 +39,7 @@ const RequestsNotLoaded = ({ collection }) => {
);
return;
}
}
};
return (
<StyledWrapper className="w-full card my-2">
@@ -61,7 +61,7 @@ const RequestsNotLoaded = ({ collection }) => {
<tbody>
{flattenedItems?.map((item, index) => (
item?.partial && !item?.loading ? (
<tr key={index} className='cursor-pointer' onClick={handleRequestClick(item)}>
<tr key={index} className="cursor-pointer" onClick={handleRequestClick(item)}>
<td className="py-1.5 px-3">
{item?.pathname?.split(`${collection?.pathname}/`)?.[1]}
</td>

View File

@@ -1,16 +1,16 @@
import StyledWrapper from "./StyledWrapper";
import Docs from "../Docs";
import Info from "./Info";
import StyledWrapper from './StyledWrapper';
import Docs from '../Docs';
import Info from './Info';
import { IconBox } from '@tabler/icons';
import RequestsNotLoaded from "./RequestsNotLoaded";
import RequestsNotLoaded from './RequestsNotLoaded';
const Overview = ({ collection }) => {
return (
<div className="h-full">
<div className="grid grid-cols-5 gap-4 h-full">
<div className="grid grid-cols-5 gap-5 h-full">
<div className="col-span-2">
<div className="text-xl font-semibold flex items-center gap-2">
<IconBox size={24} stroke={1.5} />
<div className="text-lg font-medium flex items-center gap-2">
<IconBox size={20} stroke={1.5} />
{collection?.name}
</div>
<Info collection={collection} />
@@ -22,6 +22,6 @@ const Overview = ({ collection }) => {
</div>
</div>
);
}
};
export default Overview;
export default Overview;

View File

@@ -1,29 +0,0 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
max-width: 800px;
.settings-label {
width: 110px;
}
.textbox {
border: 1px solid #ccc;
padding: 0.15rem 0.45rem;
box-shadow: none;
border-radius: 0px;
outline: none;
box-shadow: none;
transition: border-color ease-in-out 0.1s;
border-radius: 3px;
background-color: ${(props) => props.theme.modal.input.bg};
border: 1px solid ${(props) => props.theme.modal.input.border};
&:focus {
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
outline: none !important;
}
}
`;
export default StyledWrapper;

View File

@@ -1,114 +0,0 @@
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';
const PresetsSettings = ({ collection }) => {
const dispatch = useDispatch();
const {
brunoConfig: { presets: presets = {} }
} = collection;
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');
}
});
return (
<StyledWrapper className="h-full w-full">
<div className="text-xs mb-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="mb-3 flex items-center">
<label className="settings-label flex items-center" htmlFor="enabled">
Request Type
</label>
<div className="flex items-center">
<input
id="http"
className="cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
value="http"
checked={formik.values.requestType === 'http'}
/>
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
HTTP
</label>
<input
id="graphql"
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
value="graphql"
checked={formik.values.requestType === 'graphql'}
/>
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
GraphQL
</label>
<input
id="grpc"
className="ml-4 cursor-pointer"
type="radio"
name="requestType"
onChange={formik.handleChange}
value="grpc"
checked={formik.values.requestType === 'grpc'}
/>
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
gRPC
</label>
</div>
</div>
<div className="mb-3 flex items-center">
<label className="settings-label" htmlFor="requestUrl">
Base URL
</label>
<div className="flex items-center w-full">
<div className="flex items-center flex-grow input-container h-full">
<input
id="request-url"
type="text"
name="requestUrl"
placeholder="Request URL"
className="block textbox"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.requestUrl || ''}
style={{ width: '100%' }}
/>
</div>
</div>
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary">
Save
</button>
</div>
</form>
</StyledWrapper>
);
};
export default PresetsSettings;

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;
@@ -107,7 +112,7 @@ const ProtobufSettings = ({ collection }) => {
<div className="mb-6" data-testid="protobuf-proto-files-section">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center">
<label className="font-semibold text-sm flex items-center" htmlFor="protoFiles">
<label className="font-medium flex items-center" htmlFor="protoFiles">
Proto Files (
{protoFiles.length}
)
@@ -151,7 +156,7 @@ const ProtobufSettings = ({ collection }) => {
<td colSpan="3" className="border border-gray-200 dark:border-gray-700 px-3 py-8 text-center">
<div className="flex flex-col items-center">
<IconFile size={24} className="text-gray-400 mb-2" />
<span className="text-sm text-gray-500 dark:text-gray-400">No proto files added</span>
<span className="text-gray-500 dark:text-gray-400">No proto files added</span>
</div>
</td>
</tr>
@@ -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="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" />}
@@ -214,7 +219,7 @@ const ProtobufSettings = ({ collection }) => {
<div className="mb-6" data-testid="protobuf-import-paths-section">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center">
<label className="font-semibold text-sm flex items-center" htmlFor="importPaths">
<label className="font-medium flex items-center" htmlFor="importPaths">
Import Paths (
{importPaths.length}
)
@@ -260,7 +265,7 @@ const ProtobufSettings = ({ collection }) => {
<td colSpan="4" className="border border-gray-200 dark:border-gray-700 px-3 py-8 text-center">
<div className="flex flex-col items-center">
<IconFolder size={24} className="text-gray-400 mb-2" />
<span className="text-sm text-gray-500 dark:text-gray-400">No import paths added</span>
<span className="text-gray-500 dark:text-gray-400">No import paths added</span>
</div>
</td>
</tr>
@@ -283,7 +288,7 @@ const ProtobufSettings = ({ collection }) => {
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
<div className="flex items-center">
<IconFolder 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="font-medium text-gray-900 dark:text-gray-100">
{getBasename(collection.pathname, importPath.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,115 +1,164 @@
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
<InfoTip infotipId="request-var">
<div>
<ul>
<li><span style={{width: "50px", display: "inline-block"}}>global</span> - use global proxy config</li>
<li><span style={{width: "50px", display: "inline-block"}}>enabled</span> - use collection proxy config</li>
<li><span style={{width: "50px", display: "inline-block"}}>disable</span> - disable proxy</li>
<li><span style={{ width: '50px', display: 'inline-block' }}>global</span> - use global proxy config</li>
<li><span style={{ width: '50px', display: 'inline-block' }}>enabled</span> - use collection proxy config</li>
<li><span style={{ width: '50px', display: 'inline-block' }}>disable</span> - disable proxy</li>
</ul>
</div>
</InfoTip>
@@ -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,21 +353,18 @@ 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>
);
};
export default ProxySettings;
export default ProxySettings;

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

@@ -4,7 +4,7 @@ const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
font-weight: 500;
table-layout: fixed;
thead,
@@ -14,7 +14,7 @@ const Wrapper = styled.div`
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
user-select: none;
}
td {
@@ -31,7 +31,7 @@ const Wrapper = styled.div`
}
.btn-add-var {
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
}
input[type='text'] {

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}
@@ -127,8 +127,7 @@ const VarsTable = ({ collection, vars, varType }) => {
},
_var,
'value'
)
}
)}
collection={collection}
/>
</td>

View File

@@ -2,24 +2,20 @@ 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 handleSave = () => dispatch(saveCollectionSettings(collection.uid));
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Pre Request</div>
<VarsTable collection={collection} vars={requestVars} varType="request" />
</div>
<div className="flex-1">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<VarsTable collection={collection} vars={responseVars} varType="response" />
</div>
<div className="mt-6">
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
Save

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';
@@ -12,7 +9,6 @@ import Headers from './Headers';
import Auth from './Auth';
import Script from './Script';
import Test from './Tests';
import Presets from './Presets';
import Protobuf from './Protobuf';
import StyledWrapper from './StyledWrapper';
import Vars from './Vars/index';
@@ -31,65 +27,22 @@ 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 activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
const activeVarsCount = requestVars.filter((v) => v.enabled).length;
const authMode = (collection.draft?.root ? get(collection, 'draft.root.request.auth', {}) : get(collection, 'root.request.auth', {})).mode || 'none';
const presets = get(collection, 'brunoConfig.presets', []);
const hasPresets = presets && presets.requestUrl !== '';
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
const proxyConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.proxy', {}) : get(collection, 'brunoConfig.proxy', {});
const proxyEnabled = proxyConfig.hostname ? true : false;
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
const protobufConfig = get(collection, 'brunoConfig.protobuf', {});
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 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,19 +64,13 @@ const CollectionSettings = ({ collection }) => {
case 'tests': {
return <Test collection={collection} />;
}
case 'presets': {
return <Presets collection={collection} />;
}
case 'proxy': {
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
return <ProxySettings collection={collection} />;
}
case 'clientCert': {
return (
<ClientCertSettings
collection={collection}
clientCertConfig={clientCertConfig}
onUpdate={onClientCertSettingsUpdate}
onRemove={onClientCertSettingsRemove}
/>
);
}
@@ -165,10 +112,6 @@ const CollectionSettings = ({ collection }) => {
Tests
{hasTests && <StatusDot />}
</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 && proxyEnabled && <StatusDot />}

View File

@@ -125,7 +125,7 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
);
if (!isEmpty(validationErrors)) {
toast.error(Object.values(validationErrors).join("\n"));
toast.error(Object.values(validationErrors).join('\n'));
return;
}
@@ -208,9 +208,9 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
onClose={onClose}
handleCancel={onClose}
handleConfirm={onSubmit}
customHeader={
customHeader={(
<div className="flex items-center justify-between w-full">
<h2 className="text-sm font-bold">{title}</h2>
<h2 className="font-bold">{title}</h2>
<div className="ml-auto flex items-center ">
<ToggleSwitch
className="mr-2"
@@ -220,16 +220,16 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
setIsRawMode(e.target.checked);
}}
/>
<label className="text-sm font-normal mr-4 normal-case">Edit Raw</label>
<label className="font-normal mr-4 normal-case">Edit Raw</label>
</div>
</div>
}
)}
>
<form onSubmit={(e) => e.preventDefault()} className="px-2">
{isRawMode ? (
<div>
<div className="flex items-center gap-2 mb-1">
<label className="block text-sm">Set-Cookie String</label>
<label className="block">Set-Cookie String</label>
<IconInfoCircle id="cookie-raw-info" size={16} strokeWidth={1.5} className="text-gray-400" />
<Tooltip
anchorId="cookie-raw-info"
@@ -248,7 +248,7 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm mb-1">
<label className="block mb-1">
Domain<span className="text-red-600">*</span>{' '}
</label>
<input
@@ -262,11 +262,11 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
disabled={!!cookie}
/>
{formik.touched.domain && formik.errors.domain && (
<div className="text-red-500 text-sm mt-1">{formik.errors.domain}</div>
<div className="text-red-500 mt-1">{formik.errors.domain}</div>
)}
</div>
<div>
<label className="block text-sm mb-1">Path</label>
<label className="block mb-1">Path</label>
<input
type="text"
name="path"
@@ -276,11 +276,11 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
disabled={!!cookie}
/>
{formik.touched.path && formik.errors.path && (
<div className="text-red-500 text-sm mt-1">{formik.errors.path}</div>
<div className="text-red-500 mt-1">{formik.errors.path}</div>
)}
</div>
<div>
<label className="block text-sm mb-1">
<label className="block mb-1">
Key<span className="text-red-600">*</span>{' '}
</label>
<input
@@ -294,12 +294,12 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
disabled={!!cookie}
/>
{formik.touched.key && formik.errors.key && (
<div className="text-red-500 text-sm mt-1">{formik.errors.key}</div>
<div className="text-red-500 mt-1">{formik.errors.key}</div>
)}
</div>
<div>
<label className="block text-sm mb-1">
<label className="block mb-1">
Value<span className="text-red-600">*</span>{' '}
</label>
<input
@@ -312,7 +312,7 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
className="block textbox non-passphrase-input w-full"
/>
{formik.touched.value && formik.errors.value && (
<div className="text-red-500 text-sm mt-1">{formik.errors.value}</div>
<div className="text-red-500 mt-1">{formik.errors.value}</div>
)}
</div>
</div>
@@ -320,7 +320,7 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
{/* Date Picker */}
<div className="w-full flex items-end">
<div>
<label className="block text-sm mb-1">Expiration ({moment.tz.guess()})</label>
<label className="block mb-1">Expiration ({moment.tz.guess()})</label>
<input
type="datetime-local"
name="expires"
@@ -332,7 +332,7 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
min={moment().format(moment.HTML5_FMT.DATETIME_LOCAL)}
/>
{formik.touched.expires && formik.errors.expires && (
<div className="text-red-500 text-sm mt-1">{formik.errors.expires}</div>
<div className="text-red-500 mt-1">{formik.errors.expires}</div>
)}
</div>
@@ -346,7 +346,7 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
onChange={formik.handleChange}
className="mr-2"
/>
<span className="text-sm">Secure</span>
<span>Secure</span>
</label>
<label className="flex items-center">
@@ -357,7 +357,7 @@ const ModifyCookieModal = ({ onClose, domain, cookie }) => {
onChange={formik.handleChange}
className="mr-2"
/>
<span className="text-sm">HTTP Only</span>
<span>HTTP Only</span>
</label>
</div>
</div>

View File

@@ -7,7 +7,7 @@ const Wrapper = styled.div`
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
user-select: none;
}
}

View File

@@ -14,7 +14,7 @@ const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
<Modal onClose={onClose} handleCancel={onClose} title="Clear Domain Cookies" hideFooter={true}>
<div className="flex items-center font-normal">
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
<h1 className="ml-2 text-lg font-medium">Hold on..</h1>
</div>
<div className="font-normal mt-4">
Are you sure you want to clear all cookies for the domain {domain}?
@@ -39,7 +39,7 @@ const DeleteCookieModal = ({ onClose, cookieName, onDelete }) => (
<Modal onClose={onClose} handleCancel={onClose} title="Delete Cookie" hideFooter={true}>
<div className="flex items-center font-normal">
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
<h1 className="ml-2 text-lg font-medium">Hold on..</h1>
</div>
<div className="font-normal mt-4">
Are you sure you want to delete the cookie {cookieName}?
@@ -72,7 +72,7 @@ const CollectionProperties = ({ onClose }) => {
const [searchText, setSearchText] = useState(null);
const handleAddCookie = (domain) => {
if(domain) setCurrentDomain(domain);
if (domain) setCurrentDomain(domain);
setIsModifyCookieModalOpen(true);
};
@@ -157,7 +157,7 @@ const CollectionProperties = ({ onClose }) => {
// No cookies found
<div className="flex items-center justify-center flex-col">
<IconCookieOff size={48} strokeWidth={1.5} className="text-gray-500" />
<h2 className="text-lg font-semibold mt-4">No cookies found</h2>
<h2 className="text-lg font-medium mt-4">No cookies found</h2>
<p className="text-gray-500 mt-2">Add cookies to get started</p>
<button
type="submit"
@@ -175,7 +175,7 @@ const CollectionProperties = ({ onClose }) => {
// No search results
<div className="flex items-center justify-center flex-col">
<IconSearch size={48} />
<h2 className="text-lg font-semibold mt-4">No search results</h2>
<h2 className="text-lg font-medium mt-4">No search results</h2>
<p className="text-gray-500 mt-2">Try a different search term</p>
</div>
) : (
@@ -219,13 +219,13 @@ const CollectionProperties = ({ onClose }) => {
<table className="w-full">
<thead>
<tr className="text-left border-b border-gray-200 dark:border-neutral-600 text-gray-700 dark:text-gray-300">
<th className="py-2 px-4 font-semibold w-32">Name</th>
<th className="py-2 px-4 font-semibold w-52">Value</th>
<th className="py-2 px-4 font-semibold">Path</th>
<th className="py-2 px-4 font-semibold">Expires</th>
<th className="py-2 px-4 font-semibold text-center">Secure</th>
<th className="py-2 px-4 font-semibold text-center">HTTP Only</th>
<th className="py-2 px-4 font-semibold text-right w-24">Actions</th>
<th className="py-2 px-4 font-medium w-32">Name</th>
<th className="py-2 px-4 font-medium w-52">Value</th>
<th className="py-2 px-4 font-medium">Path</th>
<th className="py-2 px-4 font-medium">Expires</th>
<th className="py-2 px-4 font-medium text-center">Secure</th>
<th className="py-2 px-4 font-medium text-center">HTTP Only</th>
<th className="py-2 px-4 font-medium text-right w-24">Actions</th>
</tr>
</thead>
<tbody>

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

@@ -22,12 +22,12 @@ const StyledWrapper = styled.div`
align-items: center;
gap: 8px;
color: ${(props) => props.theme.console.titleColor};
font-size: 13px;
font-size: ${(props) => props.theme.font.size.base};
font-weight: 500;
.error-count {
color: ${(props) => props.theme.console.countColor};
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 400;
}
}
@@ -73,12 +73,12 @@ const StyledWrapper = styled.div`
p {
margin: 0;
font-size: 14px;
font-size: ${(props) => props.theme.font.size.base};
font-weight: 500;
}
span {
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
opacity: 0.7;
}
}
@@ -98,8 +98,8 @@ const StyledWrapper = styled.div`
padding: 8px 16px;
background: ${(props) => props.theme.console.headerBg};
border-bottom: 1px solid ${(props) => props.theme.console.border};
font-size: 11px;
font-weight: 600;
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 500;
color: ${(props) => props.theme.console.titleColor};
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -121,7 +121,7 @@ const StyledWrapper = styled.div`
border-bottom: 1px solid ${(props) => props.theme.console.border};
cursor: pointer;
transition: background-color 0.1s ease;
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
align-items: center;
&:hover {
@@ -149,15 +149,15 @@ const StyledWrapper = styled.div`
text-overflow: ellipsis;
white-space: nowrap;
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
}
.error-time {
color: ${(props) => props.theme.console.timestampColor};
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
text-align: right;
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { IconBug } from '@tabler/icons';
import {
import {
setSelectedError,
clearDebugErrors
} from 'providers/ReduxStore/slices/logs';
@@ -10,10 +10,10 @@ import StyledWrapper from './StyledWrapper';
const ErrorRow = ({ error, isSelected, onClick }) => {
const formatTime = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
});
@@ -38,18 +38,18 @@ const ErrorRow = ({ error, isSelected, onClick }) => {
};
return (
<div
<div
className={`error-row ${isSelected ? 'selected' : ''}`}
onClick={onClick}
>
<div className="error-message" title={error.message}>
{getShortMessage(error.message)}
</div>
<div className="error-location" title={error.filename}>
{getLocation(error)}
</div>
<div className="error-time">
{formatTime(error.timestamp)}
</div>
@@ -59,7 +59,7 @@ const ErrorRow = ({ error, isSelected, onClick }) => {
const DebugTab = () => {
const dispatch = useDispatch();
const { debugErrors, selectedError } = useSelector(state => state.logs);
const { debugErrors, selectedError } = useSelector((state) => state.logs);
const handleErrorClick = (error) => {
dispatch(setSelectedError(error));
@@ -85,7 +85,7 @@ const DebugTab = () => {
<div>Location</div>
<div className="text-right">Time</div>
</div>
<div className="errors-list">
{debugErrors.map((error, index) => (
<ErrorRow
@@ -103,4 +103,4 @@ const DebugTab = () => {
);
};
export default DebugTab;
export default DebugTab;

View File

@@ -26,12 +26,12 @@ const StyledWrapper = styled.div`
align-items: center;
gap: 8px;
color: ${(props) => props.theme.console.titleColor};
font-size: 13px;
font-size: ${(props) => props.theme.font.size.base};
font-weight: 500;
.error-time {
color: ${(props) => props.theme.console.countColor};
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 400;
}
}
@@ -73,7 +73,7 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.console.buttonColor};
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
&:hover {
@@ -111,8 +111,8 @@ const StyledWrapper = styled.div`
h4 {
margin: 0 0 12px 0;
font-size: 13px;
font-weight: 600;
font-size: ${(props) => props.theme.font.size.base};
font-weight: 500;
color: ${(props) => props.theme.console.titleColor};
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -131,15 +131,15 @@ const StyledWrapper = styled.div`
gap: 4px;
label {
font-size: 11px;
font-weight: 600;
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 500;
color: ${(props) => props.theme.console.titleColor};
text-transform: uppercase;
letter-spacing: 0.5px;
}
span {
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.console.messageColor};
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
word-break: break-all;
@@ -167,7 +167,7 @@ const StyledWrapper = styled.div`
p {
margin: 0;
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.console.messageColor};
line-height: 1.4;
}
@@ -184,7 +184,7 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.console.buttonColor};
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
text-decoration: none;
align-self: flex-start;
@@ -212,7 +212,7 @@ const StyledWrapper = styled.div`
.arguments {
margin: 0;
padding: 16px;
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
line-height: 1.5;
color: ${(props) => props.theme.console.messageColor};
background: transparent;
@@ -225,4 +225,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
import {
IconX,
IconBug,
IconFileText,
@@ -15,7 +15,7 @@ import StyledWrapper from './StyledWrapper';
const ErrorInfoTab = ({ error }) => {
const { version } = useApp();
const formatTimestamp = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleString();
@@ -23,7 +23,7 @@ const ErrorInfoTab = ({ error }) => {
const generateGitHubIssueUrl = () => {
const title = `Bug: ${error.message.substring(0, 50)}${error.message.length > 50 ? '...' : ''}`;
const body = `## Bug Report
### Error Details
@@ -66,7 +66,7 @@ ${error.args ? error.args.map((arg, index) => {
const encodedTitle = encodeURIComponent(title);
const encodedBody = encodeURIComponent(body);
return `https://github.com/usebruno/bruno/issues/new?template=BLANK_ISSUE&title=${encodedTitle}&body=${encodedBody}`;
};
@@ -84,33 +84,33 @@ ${error.args ? error.args.map((arg, index) => {
<label>Message:</label>
<span className="error-message-full">{error.message || 'No message available'}</span>
</div>
{error.filename && (
<div className="info-item">
<label>File:</label>
<span className="file-path">{error.filename}</span>
</div>
)}
{error.lineno && (
<div className="info-item">
<label>Line:</label>
<span>{error.lineno}{error.colno ? `:${error.colno}` : ''}</span>
</div>
)}
<div className="info-item">
<label>Timestamp:</label>
<span>{formatTimestamp(error.timestamp)}</span>
</div>
</div>
</div>
<div className="section">
<h4>Report Issue</h4>
<div className="report-section">
<p>Found a bug? Help us improve Bruno by reporting this error on GitHub.</p>
<button
<button
className="report-button"
onClick={handleReportIssue}
title="Report this error on GitHub"
@@ -127,11 +127,11 @@ ${error.args ? error.args.map((arg, index) => {
const StackTraceTab = ({ error }) => {
const formatStackTrace = (stack) => {
if (!stack) return 'Stack trace not available';
return stack
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0)
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join('\n');
};
@@ -152,18 +152,18 @@ const StackTraceTab = ({ error }) => {
const ArgumentsTab = ({ error }) => {
const formatArguments = (args) => {
if (!args || args.length === 0) return 'No arguments available';
try {
return args.map((arg, index) => {
// Handle special Error object format
if (arg && typeof arg === 'object' && arg.__type === 'Error') {
return `[${index}]: Error: ${arg.message}\n Name: ${arg.name}\n Stack: ${arg.stack || 'No stack trace'}`;
}
if (typeof arg === 'object' && arg !== null) {
return `[${index}]: ${JSON.stringify(arg, null, 2)}`;
}
return `[${index}]: ${String(arg)}`;
}).join('\n\n');
} catch (e) {
@@ -187,7 +187,7 @@ const ArgumentsTab = ({ error }) => {
const ErrorDetailsPanel = () => {
const dispatch = useDispatch();
const { selectedError } = useSelector(state => state.logs);
const { selectedError } = useSelector((state) => state.logs);
const [activeTab, setActiveTab] = useState('info');
if (!selectedError) return null;
@@ -222,8 +222,8 @@ const ErrorDetailsPanel = () => {
<span>Error Details</span>
<span className="error-time">({formatTime(selectedError.timestamp)})</span>
</div>
<button
<button
className="close-button"
onClick={handleClose}
title="Close details panel"
@@ -233,23 +233,23 @@ const ErrorDetailsPanel = () => {
</div>
<div className="panel-tabs">
<button
<button
className={`tab-button ${activeTab === 'info' ? 'active' : ''}`}
onClick={() => setActiveTab('info')}
>
<IconFileText size={14} strokeWidth={1.5} />
Info
</button>
<button
<button
className={`tab-button ${activeTab === 'stack' ? 'active' : ''}`}
onClick={() => setActiveTab('stack')}
>
<IconStack size={14} strokeWidth={1.5} />
Stack
</button>
<button
<button
className={`tab-button ${activeTab === 'args' ? 'active' : ''}`}
onClick={() => setActiveTab('args')}
>
@@ -265,4 +265,4 @@ const ErrorDetailsPanel = () => {
);
};
export default ErrorDetailsPanel;
export default ErrorDetailsPanel;

View File

@@ -22,12 +22,12 @@ const StyledWrapper = styled.div`
align-items: center;
gap: 8px;
color: ${(props) => props.theme.console.titleColor};
font-size: 13px;
font-size: ${(props) => props.theme.font.size.base};
font-weight: 500;
.request-count {
color: ${(props) => props.theme.console.countColor};
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 400;
}
}
@@ -59,12 +59,12 @@ const StyledWrapper = styled.div`
p {
margin: 0;
font-size: 14px;
font-size: ${(props) => props.theme.font.size.base};
font-weight: 500;
}
span {
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
opacity: 0.7;
}
}
@@ -84,8 +84,8 @@ const StyledWrapper = styled.div`
padding: 8px 16px;
background: ${(props) => props.theme.console.headerBg};
border-bottom: 1px solid ${(props) => props.theme.console.border};
font-size: 11px;
font-weight: 600;
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 500;
color: ${(props) => props.theme.console.titleColor};
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -107,7 +107,7 @@ const StyledWrapper = styled.div`
border-bottom: 1px solid ${(props) => props.theme.console.border};
cursor: pointer;
transition: background-color 0.1s ease;
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
align-items: center;
&:hover {
@@ -127,7 +127,7 @@ const StyledWrapper = styled.div`
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
font-weight: 500;
color: white;
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -135,8 +135,8 @@ const StyledWrapper = styled.div`
}
.status-badge {
font-weight: 600;
font-size: 12px;
font-weight: 500;
font-size: ${(props) => props.theme.font.size.sm};
}
.request-domain {
@@ -158,20 +158,20 @@ const StyledWrapper = styled.div`
.request-time {
color: ${(props) => props.theme.console.timestampColor};
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
}
.request-duration {
color: ${(props) => props.theme.console.messageColor};
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
text-align: right;
}
.request-size {
color: ${(props) => props.theme.console.messageColor};
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
text-align: right;
}
@@ -190,7 +190,7 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.console.buttonColor};
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
&:hover {
background: ${(props) => props.theme.console.buttonHoverBg};
@@ -225,7 +225,7 @@ const StyledWrapper = styled.div`
padding: 8px 12px;
background: ${(props) => props.theme.console.dropdownHeaderBg};
border-bottom: 1px solid ${(props) => props.theme.console.border};
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
color: ${(props) => props.theme.console.titleColor};
}
@@ -235,7 +235,7 @@ const StyledWrapper = styled.div`
border: none;
color: ${(props) => props.theme.console.buttonColor};
cursor: pointer;
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 500;
padding: 2px 4px;
border-radius: 2px;
@@ -278,16 +278,16 @@ const StyledWrapper = styled.div`
.filter-option-label {
color: ${(props) => props.theme.console.optionLabelColor};
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 400;
}
.filter-option-count {
color: ${(props) => props.theme.console.optionCountColor};
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 400;
margin-left: auto;
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -1,13 +1,13 @@
import React, { useState, useRef, useEffect, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
import {
IconFilter,
IconChevronDown,
IconNetwork,
IconNetwork
} from '@tabler/icons';
import {
updateNetworkFilter,
toggleAllNetworkFilters,
import {
updateNetworkFilter,
toggleAllNetworkFilters,
setSelectedRequest
} from 'providers/ReduxStore/slices/logs';
import StyledWrapper from './StyledWrapper';
@@ -27,8 +27,8 @@ const MethodBadge = ({ method }) => {
};
return (
<span
className="method-badge"
<span
className="method-badge"
style={{ backgroundColor: getMethodColor(method) }}
>
{method?.toUpperCase() || 'GET'}
@@ -46,10 +46,10 @@ const StatusBadge = ({ status, statusCode }) => {
};
const displayStatus = statusCode || status;
return (
<span
className="status-badge"
<span
className="status-badge"
style={{ color: getStatusColor(statusCode) }}
>
{displayStatus}
@@ -61,7 +61,7 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const allFiltersEnabled = Object.values(filters).every(f => f);
const allFiltersEnabled = Object.values(filters).every((f) => f);
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
useEffect(() => {
@@ -77,7 +77,7 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
return (
<div className="filter-dropdown" ref={dropdownRef}>
<button
<button
className="filter-dropdown-trigger"
onClick={() => setIsOpen(!isOpen)}
title="Filter requests by method"
@@ -88,21 +88,21 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
</span>
<IconChevronDown size={14} strokeWidth={1.5} />
</button>
{isOpen && (
<div className={`filter-dropdown-menu right`}>
<div className="filter-dropdown-menu right">
<div className="filter-dropdown-header">
<span>Filter by Method</span>
<button
<button
className="filter-toggle-all"
onClick={() => onToggleAll(!allFiltersEnabled)}
>
{allFiltersEnabled ? 'Hide All' : 'Show All'}
</button>
</div>
<div className="filter-dropdown-options">
{Object.keys(filters).map(method => (
{Object.keys(filters).map((method) => (
<label key={method} className="filter-option">
<input
type="checkbox"
@@ -126,13 +126,13 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
const RequestRow = ({ request, isSelected, onClick }) => {
const { data } = request;
const { request: req, response: res, timestamp } = data;
const formatTime = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
});
@@ -174,34 +174,34 @@ const RequestRow = ({ request, isSelected, onClick }) => {
};
return (
<div
<div
className={`request-row ${isSelected ? 'selected' : ''}`}
onClick={onClick}
>
<div className="request-method">
<MethodBadge method={req?.method} />
</div>
<div className="request-status">
<StatusBadge status={res?.status} statusCode={res?.statusCode} />
</div>
<div className="request-domain" title={getDomain()}>
{getDomain()}
</div>
<div className="request-path" title={getPath()}>
{getPath()}
</div>
<div className="request-time">
{formatTime(timestamp)}
</div>
<div className="request-duration">
{formatDuration(res?.duration)}
</div>
<div className="request-size">
{formatSize(res?.size)}
</div>
@@ -211,17 +211,17 @@ const RequestRow = ({ request, isSelected, onClick }) => {
const NetworkTab = () => {
const dispatch = useDispatch();
const { networkFilters, selectedRequest } = useSelector(state => state.logs);
const collections = useSelector(state => state.collections.collections);
const { networkFilters, selectedRequest } = useSelector((state) => state.logs);
const collections = useSelector((state) => state.collections.collections);
const allRequests = useMemo(() => {
const requests = [];
collections.forEach(collection => {
collections.forEach((collection) => {
if (collection.timeline) {
collection.timeline
.filter(entry => entry.type === 'request')
.forEach(entry => {
.filter((entry) => entry.type === 'request')
.forEach((entry) => {
requests.push({
...entry,
collectionName: collection.name,
@@ -230,12 +230,12 @@ const NetworkTab = () => {
});
}
});
return requests.sort((a, b) => a.timestamp - b.timestamp);
}, [collections]);
const filteredRequests = useMemo(() => {
return allRequests.filter(request => {
return allRequests.filter((request) => {
const method = request.data?.request?.method?.toUpperCase() || 'GET';
return networkFilters[method];
});
@@ -281,7 +281,7 @@ const NetworkTab = () => {
<div className="text-right">Duration</div>
<div className="text-right">Size</div>
</div>
<div className="requests-list">
{filteredRequests.map((request, index) => (
<RequestRow
@@ -299,4 +299,4 @@ const NetworkTab = () => {
);
};
export default NetworkTab;
export default NetworkTab;

View File

@@ -26,12 +26,12 @@ const StyledWrapper = styled.div`
align-items: center;
gap: 8px;
color: ${(props) => props.theme.console.titleColor};
font-size: 13px;
font-size: ${(props) => props.theme.font.size.base};
font-weight: 500;
.request-time {
color: ${(props) => props.theme.console.countColor};
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 400;
}
}
@@ -73,7 +73,7 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.console.buttonColor};
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
&:hover {
@@ -111,8 +111,8 @@ const StyledWrapper = styled.div`
h4 {
margin: 0;
font-size: 13px;
font-weight: 600;
font-size: ${(props) => props.theme.font.size.base};
font-weight: 500;
color: ${(props) => props.theme.console.titleColor};
padding-bottom: 4px;
border-bottom: 1px solid ${(props) => props.theme.console.border};
@@ -131,15 +131,15 @@ const StyledWrapper = styled.div`
gap: 2px;
.label {
font-size: 11px;
font-weight: 600;
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 500;
color: ${(props) => props.theme.console.countColor};
text-transform: uppercase;
letter-spacing: 0.5px;
}
.value {
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
color: ${(props) => props.theme.console.messageColor};
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
word-break: break-all;
@@ -160,7 +160,7 @@ const StyledWrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
background: ${(props) => props.theme.console.headerBg};
thead {
@@ -171,10 +171,10 @@ const StyledWrapper = styled.div`
td {
padding: 8px 12px;
font-weight: 600;
font-weight: 500;
color: ${(props) => props.theme.console.titleColor};
text-transform: uppercase;
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
letter-spacing: 0.5px;
border-bottom: 1px solid ${(props) => props.theme.console.border};
}
@@ -209,7 +209,7 @@ const StyledWrapper = styled.div`
.header-name,
.timeline-phase {
color: ${(props) => props.theme.console.countColor};
font-weight: 600;
font-weight: 500;
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
min-width: 120px;
}
@@ -234,7 +234,7 @@ const StyledWrapper = styled.div`
border-radius: 4px;
padding: 12px;
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
line-height: 1.4;
color: ${(props) => props.theme.console.messageColor};
overflow: auto;
@@ -249,7 +249,7 @@ const StyledWrapper = styled.div`
text-align: center;
color: ${(props) => props.theme.console.emptyColor};
font-style: italic;
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
background: ${(props) => props.theme.console.headerBg};
border: 1px solid ${(props) => props.theme.console.border};
border-radius: 4px;
@@ -285,7 +285,7 @@ const StyledWrapper = styled.div`
> div {
color: ${(props) => props.theme.console.buttonColor};
font-size: 12px !important;
font-size: ${(props) => props.theme.font.size.sm} !important;
padding: 6px 12px !important;
border-radius: 4px;
transition: all 0.2s ease;
@@ -336,7 +336,7 @@ const StyledWrapper = styled.div`
pre {
color: ${(props) => props.theme.console.messageColor} !important;
font-size: 11px !important;
font-size: ${(props) => props.theme.font.size.xs} !important;
line-height: 1.4 !important;
padding: 12px !important;
}
@@ -344,4 +344,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
import {
IconX,
IconFileText,
IconArrowRight,
@@ -117,7 +117,7 @@ const ResponseTab = ({ response, request, collection }) => {
<div className="response-body-container">
{response?.data || response?.dataBuffer ? (
<QueryResult
item={{ uid: uuid()}}
item={{ uid: uuid() }}
collection={collection}
data={response.data}
dataBuffer={response.dataBuffer}
@@ -155,8 +155,8 @@ const NetworkTab = ({ response }) => {
const RequestDetailsPanel = () => {
const dispatch = useDispatch();
const { selectedRequest } = useSelector(state => state.logs);
const collections = useSelector(state => state.collections.collections);
const { selectedRequest } = useSelector((state) => state.logs);
const collections = useSelector((state) => state.collections.collections);
const [activeTab, setActiveTab] = useState('request');
if (!selectedRequest) return null;
@@ -164,7 +164,7 @@ const RequestDetailsPanel = () => {
const { data } = selectedRequest;
const { request, response } = data;
const collection = collections.find(c => c.uid === selectedRequest.collectionUid);
const collection = collections.find((c) => c.uid === selectedRequest.collectionUid);
const handleClose = () => {
dispatch(clearSelectedRequest());
@@ -196,8 +196,8 @@ const RequestDetailsPanel = () => {
<span>Request Details</span>
<span className="request-time">({formatTime(selectedRequest.timestamp)})</span>
</div>
<button
<button
className="close-button"
onClick={handleClose}
title="Close details panel"
@@ -207,23 +207,23 @@ const RequestDetailsPanel = () => {
</div>
<div className="panel-tabs">
<button
<button
className={`tab-button ${activeTab === 'request' ? 'active' : ''}`}
onClick={() => setActiveTab('request')}
>
<IconArrowRight size={14} strokeWidth={1.5} />
Request
</button>
<button
<button
className={`tab-button ${activeTab === 'response' ? 'active' : ''}`}
onClick={() => setActiveTab('response')}
>
<IconFileText size={14} strokeWidth={1.5} />
Response
</button>
<button
<button
className={`tab-button ${activeTab === 'network' ? 'active' : ''}`}
onClick={() => setActiveTab('network')}
>
@@ -239,4 +239,4 @@ const RequestDetailsPanel = () => {
);
};
export default RequestDetailsPanel;
export default RequestDetailsPanel;

View File

@@ -37,7 +37,7 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.console.buttonColor};
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
border-radius: 4px 4px 0 0;
@@ -89,12 +89,12 @@ const StyledWrapper = styled.div`
align-items: center;
gap: 8px;
color: ${(props) => props.theme.console.titleColor};
font-size: 13px;
font-size: ${(props) => props.theme.font.size.base};
font-weight: 500;
.log-count {
color: ${(props) => props.theme.console.countColor};
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 400;
}
}
@@ -194,7 +194,7 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.console.buttonColor};
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
&:hover {
background: ${(props) => props.theme.console.buttonHoverBg};
@@ -235,7 +235,7 @@ const StyledWrapper = styled.div`
padding: 8px 12px;
background: ${(props) => props.theme.console.dropdownHeaderBg};
border-bottom: 1px solid ${(props) => props.theme.console.border};
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
color: ${(props) => props.theme.console.titleColor};
}
@@ -245,7 +245,7 @@ const StyledWrapper = styled.div`
border: none;
color: ${(props) => props.theme.console.buttonColor};
cursor: pointer;
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 500;
padding: 2px 4px;
border-radius: 2px;
@@ -288,13 +288,13 @@ const StyledWrapper = styled.div`
.filter-option-label {
color: ${(props) => props.theme.console.optionLabelColor};
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 400;
}
.filter-option-count {
color: ${(props) => props.theme.console.optionCountColor};
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 400;
margin-left: auto;
}
@@ -312,12 +312,12 @@ const StyledWrapper = styled.div`
p {
margin: 0;
font-size: 14px;
font-size: ${(props) => props.theme.font.size.base};
font-weight: 500;
}
span {
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
opacity: 0.7;
}
}
@@ -333,7 +333,7 @@ const StyledWrapper = styled.div`
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
font-weight: 500;
color: white;
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -346,7 +346,7 @@ const StyledWrapper = styled.div`
gap: 12px;
padding: 4px 16px;
font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
line-height: 1.4;
border-left: 2px solid transparent;
transition: background-color 0.1s ease;
@@ -431,13 +431,13 @@ const StyledWrapper = styled.div`
.log-timestamp {
color: ${(props) => props.theme.console.timestampColor};
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
font-weight: 400;
}
.log-level {
font-size: 9px;
font-weight: 600;
font-weight: 500;
padding: 2px 4px;
border-radius: 2px;
text-transform: uppercase;
@@ -465,7 +465,7 @@ const StyledWrapper = styled.div`
background: transparent !important;
.object-key-val {
font-size: 12px !important;
font-size: ${(props) => props.theme.font.size.sm} !important;
}
.object-key {
@@ -517,4 +517,4 @@ const StyledWrapper = styled.div`
}
`;
export default StyledWrapper;
export default StyledWrapper;

View File

@@ -0,0 +1,151 @@
import React from 'react';
import { IconTerminal, IconX } from '@tabler/icons';
import styled from 'styled-components';
import ToolHint from 'components/ToolHint/index';
const StyledSessionList = styled.div`
.session-list-item {
padding: 10px 12px;
cursor: pointer;
border-bottom: 1px solid ${(props) => props.theme.border || 'rgba(255, 255, 255, 0.05)'};
transition: all 0.2s;
display: flex;
flex-direction: column;
gap: 4px;
position: relative;
&:hover {
background: ${(props) => props.theme.sidebarHover || 'rgba(255, 255, 255, 0.05)'};
.session-close-btn {
opacity: 1;
}
}
&.active {
background: ${(props) => props.theme.sidebarActive || 'rgba(59, 142, 234, 0.12)'};
border-left: 2px solid ${(props) => props.theme.brandColor || '#3b8eea'};
}
&:last-child {
border-bottom: none;
}
}
.session-close-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
opacity: 0;
transition: opacity 0.2s;
padding: 4px;
cursor: pointer;
color: ${(props) => props.theme.textSecondary || '#888'};
&:hover {
color: ${(props) => props.theme.text};
background: ${(props) => props.theme.sidebarHover || 'rgba(255, 255, 255, 0.1)'};
border-radius: 4px;
}
}
.session-name {
font-size: 13px;
font-weight: 500;
color: ${(props) => props.theme.text};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 24px;
display: flex;
align-items: center;
gap: 6px;
}
.session-icon {
flex-shrink: 0;
opacity: 0.7;
}
.session-path {
font-size: 11px;
color: ${(props) => props.theme.textSecondary || '#888'};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`;
const SessionList = ({ sessions, activeSessionId, onSelectSession, onCloseSession }) => {
const getSessionDisplayInfo = (session) => {
if (session.name) {
return { name: session.name };
}
if (session.cwd) {
// Normalize path and get the last directory name
const normalizedPath = session.cwd.replace(/\\/g, '/').replace(/\/$/, '');
const pathParts = normalizedPath.split('/').filter((p) => p);
if (pathParts.length > 0) {
const folderName = pathParts[pathParts.length - 1];
return { name: folderName };
}
// If it's root or home directory
if (normalizedPath === '' || normalizedPath === '/' || normalizedPath.match(/^[A-Z]:\/?$/)) {
return { name: 'Root' };
}
}
// Fallback: use a cool name based on session ID
const shortId = session.sessionId.split('_')[1]?.slice(-6) || session.sessionId.slice(-6);
return { name: `Terminal ${shortId}` };
};
const getFullPath = (session) => {
if (session.cwd) {
return session.cwd;
}
return '~ (Home Directory)';
};
return (
<StyledSessionList>
{sessions.map((session) => {
const { name } = getSessionDisplayInfo(session);
return (
<ToolHint
key={session.sessionId}
text={getFullPath(session)}
toolhintId={`session-path-${session.sessionId}`}
place="bottom-start"
delayShow={100}
>
<div
className={`session-list-item ${activeSessionId === session.sessionId ? 'active' : ''}`}
onClick={() => onSelectSession(session.sessionId)}
>
<div className="session-name">
<IconTerminal className="session-icon" size={14} />
<span>{name}</span>
</div>
<div
className="session-close-btn"
onClick={(e) => {
e.stopPropagation();
onCloseSession(session.sessionId);
}}
>
<IconX size={14} />
</div>
</div>
</ToolHint>
);
})}
</StyledSessionList>
);
};
export default SessionList;

View File

@@ -0,0 +1,201 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
color: ${(props) => props.theme.text};
.xterm-rows {
color: ${(props) => props.theme.text} !important;
}
.terminal-content {
height: 100%;
width: 100%;
position: relative;
display: flex;
flex-direction: row;
}
.terminal-sessions-sidebar {
width: 200px;
min-width: 200px;
border-right: 1px solid ${(props) => props.theme.border || 'rgba(255, 255, 255, 0.08)'};
background: ${(props) => props.theme.sidebarBackground || props.theme.background};
display: flex;
flex-direction: column;
overflow-y: auto;
}
.terminal-sessions-header {
padding: 12px;
font-weight: 600;
font-size: 13px;
color: ${(props) => props.theme.text};
border-bottom: 1px solid ${(props) => props.theme.border || 'rgba(255, 255, 255, 0.08)'};
display: flex;
align-items: center;
justify-content: space-between;
}
.terminal-sessions-list {
flex: 1;
overflow-y: auto;
/* Custom scrollbar styling - subtle */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.15);
}
}
.terminal-session-item {
padding: 10px 12px;
cursor: pointer;
border-bottom: 1px solid ${(props) => props.theme.border};
transition: background 0.2s;
display: flex;
flex-direction: column;
gap: 4px;
&:hover {
background: ${(props) => props.theme.sidebarHover || 'rgba(255, 255, 255, 0.05)'};
}
&.active {
background: ${(props) => props.theme.sidebarActive || 'rgba(59, 142, 234, 0.15)'};
border-left: 3px solid ${(props) => props.theme.brandColor || '#3b8eea'};
}
}
.terminal-session-name {
font-size: 13px;
font-weight: 500;
color: ${(props) => props.theme.text};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.terminal-session-path {
font-size: 11px;
color: ${(props) => props.theme.textSecondary || '#888'};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.terminal-display-container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
}
.terminal-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #888;
font-size: 14px;
z-index: 10;
svg {
opacity: 0.7;
}
span {
font-weight: 500;
}
}
.terminal-container {
flex: 1;
position: relative;
.xterm {
height: 100% !important;
width: 100% !important;
padding: 8px;
}
.xterm-viewport {
background: transparent !important;
}
.xterm-screen {
background: transparent !important;
}
.xterm-decoration-overview-ruler {
display: none;
}
/* Custom scrollbar for terminal */
.xterm-viewport::-webkit-scrollbar {
width: 8px;
}
.xterm-viewport::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
.xterm-viewport::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
.xterm-viewport::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
}
/* Dark theme adjustments */
.xterm-helper-textarea {
position: absolute !important;
left: -9999px !important;
top: -9999px !important;
}
/* Selection styling */
.xterm .xterm-selection div {
background-color: rgba(255, 255, 255, 0.3) !important;
}
/* Cursor styling */
.xterm .xterm-cursor-layer .xterm-cursor {
background-color: #d4d4d4 !important;
}
/* Link styling */
.xterm .xterm-decoration-link {
text-decoration: underline;
color: #3b8eea;
}
.xterm .xterm-decoration-link:hover {
color: #5ba7f7;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,449 @@
import React, { useRef, useEffect, useState, useCallback } from 'react';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { IconTerminal2, IconPlus } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import SessionList from './SessionList';
import '@xterm/xterm/css/xterm.css';
// Terminal instances per session - Map<sessionId, { terminal, fitAddon, inputDisposable, resizeDisposable }>
const terminalInstances = new Map();
// Data listeners per session - Map<sessionId, { onData, onExit }>
const sessionListeners = new Map();
// Parking host for terminal DOM when view unmounts
let parkingHost = null;
// Export function to get current session ID (for backward compatibility)
export const getSessionId = () => {
// Return the first active session ID if any
if (terminalInstances.size > 0) {
return Array.from(terminalInstances.keys())[0];
}
return null;
};
const ensureParkingHost = () => {
if (parkingHost && document.body.contains(parkingHost)) return parkingHost;
parkingHost = document.createElement('div');
parkingHost.style.display = 'none';
parkingHost.setAttribute('data-terminal-parking-host', 'true');
document.body.appendChild(parkingHost);
return parkingHost;
};
const createTerminalForSession = (sessionId) => {
if (terminalInstances.has(sessionId)) {
return terminalInstances.get(sessionId);
}
const terminal = new Terminal({
cursorBlink: true,
fontSize: 14,
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
theme: {
background: '#1e1e1e',
foreground: '#d4d4d4',
cursor: '#d4d4d4',
selection: '#264f78',
black: '#1e1e1e',
red: '#f14c4c',
green: '#23d18b',
yellow: '#f5f543',
blue: '#3b8eea',
magenta: '#d670d6',
cyan: '#29b8db',
white: '#e5e5e5',
brightBlack: '#666666',
brightRed: '#f14c4c',
brightGreen: '#23d18b',
brightYellow: '#f5f543',
brightBlue: '#3b8eea',
brightMagenta: '#d670d6',
brightCyan: '#29b8db',
brightWhite: '#e5e5e5'
},
allowProposedApi: true
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
const inputDisposable = terminal.onData((data) => {
if (data && sessionId && window.ipcRenderer) {
window.ipcRenderer.send('terminal:input', sessionId, data);
}
});
const resizeDisposable = terminal.onResize(({ cols, rows }) => {
if (sessionId && window.ipcRenderer) {
window.ipcRenderer.send('terminal:resize', sessionId, { cols, rows });
}
});
const instance = {
terminal,
fitAddon,
inputDisposable,
resizeDisposable
};
terminalInstances.set(sessionId, instance);
// Setup IPC listeners for this session
if (window.ipcRenderer && !sessionListeners.has(sessionId)) {
const onData = (data) => {
if (!data) return;
const instance = terminalInstances.get(sessionId);
if (instance && instance.terminal) {
try {
instance.terminal.write(data);
} catch (err) {
console.warn('Failed to write terminal data:', err);
}
}
};
const onExit = ({ exitCode, signal } = {}) => {
const msg = `\r\n[Process exited with code ${exitCode ?? ''} ${signal ? `(signal ${signal})` : ''}]\r\n`;
const instance = terminalInstances.get(sessionId);
if (instance && instance.terminal) {
try {
instance.terminal.write(msg);
} catch (err) {
console.warn('Failed to write terminal exit message:', err);
}
}
// Cleanup on exit
cleanupTerminalInstance(sessionId);
};
window.ipcRenderer.on(`terminal:data:${sessionId}`, onData);
window.ipcRenderer.on(`terminal:exit:${sessionId}`, onExit);
sessionListeners.set(sessionId, { onData, onExit });
}
return instance;
};
const cleanupTerminalInstance = (sessionId) => {
const instance = terminalInstances.get(sessionId);
if (instance) {
try {
if (instance.inputDisposable) instance.inputDisposable.dispose();
if (instance.resizeDisposable) instance.resizeDisposable.dispose();
if (instance.terminal) {
instance.terminal.dispose();
}
} catch (err) {
console.warn('Error disposing terminal instance:', err);
}
terminalInstances.delete(sessionId);
}
// Remove IPC listeners
const listeners = sessionListeners.get(sessionId);
if (listeners && window.ipcRenderer) {
try {
window.ipcRenderer.removeAllListeners(`terminal:data:${sessionId}`);
window.ipcRenderer.removeAllListeners(`terminal:exit:${sessionId}`);
} catch (err) {
console.warn('Error removing IPC listeners:', err);
}
sessionListeners.delete(sessionId);
}
};
const openTerminalIntoContainer = async (container, sessionId) => {
if (!container || !sessionId) return;
const instance = createTerminalForSession(sessionId);
const { terminal, fitAddon } = instance;
if (!terminal.element) {
terminal.open(container);
} else {
// Move terminal element to new container
if (terminal.element.parentElement !== container) {
container.appendChild(terminal.element);
}
}
await new Promise((resolve) => setTimeout(resolve, 50));
try {
fitAddon.fit();
const { cols, rows } = terminal;
if (cols && rows && window.ipcRenderer) {
window.ipcRenderer.send('terminal:resize', sessionId, { cols, rows });
}
} catch (e) {
console.warn('Error fitting terminal:', e);
}
};
const TerminalTab = () => {
const terminalRef = useRef(null);
const [sessions, setSessions] = useState([]);
const [activeSessionId, setActiveSessionId] = useState(null);
const [isLoading, setIsLoading] = useState(true);
// Load sessions list
const loadSessions = useCallback(async (currentActiveSessionId = null) => {
if (!window.ipcRenderer) return [];
try {
const sessionList = await window.ipcRenderer.invoke('terminal:list-sessions');
setSessions(sessionList);
// Use functional state updates to get the current activeSessionId
setActiveSessionId((prevActiveSessionId) => {
const activeId = currentActiveSessionId !== null ? currentActiveSessionId : prevActiveSessionId;
// Auto-select first session if none selected
if (!activeId && sessionList.length > 0) {
return sessionList[0].sessionId;
}
// If active session no longer exists, select first available
if (activeId && !sessionList.find((s) => s.sessionId === activeId)) {
return sessionList.length > 0 ? sessionList[0].sessionId : null;
}
// Keep current selection if it still exists
return activeId;
});
return sessionList;
} catch (err) {
console.error('Failed to load sessions:', err);
return [];
}
}, []);
// Create new terminal session
const createNewSession = useCallback(async (cwd = null) => {
if (!window.ipcRenderer) return null;
try {
const options = cwd ? { cwd } : {};
const newSessionId = await window.ipcRenderer.invoke('terminal:create', options);
if (newSessionId) {
await loadSessions(newSessionId);
setActiveSessionId(newSessionId);
return newSessionId;
}
} catch (err) {
console.error('Failed to create terminal session:', err);
}
return null;
}, [loadSessions]);
// Listen for requests to open terminal at specific CWD
useEffect(() => {
const normalizePath = (path) => {
if (!path) return '';
// Normalize path separators and remove trailing separators for comparison
return path.replace(/\\/g, '/').replace(/\/$/, '') || '/';
};
const handleOpenTerminalAtCwd = async (event) => {
const { cwd } = event.detail;
if (!cwd) return;
const normalizedCwd = normalizePath(cwd);
// Check if session already exists at this CWD
const sessionList = await window.ipcRenderer.invoke('terminal:list-sessions');
const existingSession = sessionList.find((s) => normalizePath(s.cwd) === normalizedCwd);
if (existingSession) {
// Switch to existing session
await loadSessions(existingSession.sessionId);
setActiveSessionId(existingSession.sessionId);
} else {
// Create new session at this CWD
await createNewSession(cwd);
}
};
window.addEventListener('terminal:open-at-cwd', handleOpenTerminalAtCwd);
return () => {
window.removeEventListener('terminal:open-at-cwd', handleOpenTerminalAtCwd);
};
}, [loadSessions, createNewSession]);
// Close terminal session
const closeSession = async (sessionId) => {
if (!window.ipcRenderer) return;
try {
window.ipcRenderer.send('terminal:kill', sessionId);
cleanupTerminalInstance(sessionId);
// Load updated sessions (this will also handle active session switching)
const updatedSessions = await loadSessions();
// If we closed the active session and there are no sessions left, clear selection
if (activeSessionId === sessionId && updatedSessions.length === 0) {
setActiveSessionId(null);
}
} catch (err) {
console.error('Failed to close terminal session:', err);
}
};
// Load sessions on mount and set up polling
useEffect(() => {
if (!window.ipcRenderer) {
setIsLoading(false);
return;
}
let mounted = true;
const initialLoad = async () => {
const sessionList = await loadSessions();
if (mounted) {
setIsLoading(false);
}
};
initialLoad();
// Poll for session updates every 2 seconds
// Note: We don't pass currentActiveSessionId here to avoid stale closures
// The functional update inside loadSessions will use the current state
const pollInterval = setInterval(() => {
if (mounted) {
loadSessions();
}
}, 2000);
return () => {
mounted = false;
clearInterval(pollInterval);
};
}, []);
// Handle terminal display for active session
useEffect(() => {
if (!activeSessionId || !terminalRef.current) return;
let mounted = true;
const setupTerminal = async () => {
await openTerminalIntoContainer(terminalRef.current, activeSessionId);
if (mounted) {
const instance = terminalInstances.get(activeSessionId);
if (instance && instance.fitAddon) {
const onResize = () => {
try {
instance.fitAddon.fit();
} catch (e) {}
};
window.addEventListener('resize', onResize);
// Initial resize
setTimeout(() => {
try {
instance.fitAddon.fit();
const { cols, rows } = instance.terminal;
if (cols && rows && window.ipcRenderer) {
window.ipcRenderer.send('terminal:resize', activeSessionId, { cols, rows });
}
} catch (err) {
console.warn('Failed to perform initial resize:', err);
}
}, 100);
return () => {
window.removeEventListener('resize', onResize);
// Park terminal element when switching sessions
if (instance.terminal && instance.terminal.element) {
const host = ensureParkingHost();
if (instance.terminal.element.parentElement !== host) {
host.appendChild(instance.terminal.element);
}
}
};
}
}
};
const cleanup = setupTerminal();
return () => {
mounted = false;
Promise.resolve(cleanup).then((fn) => {
if (typeof fn === 'function') fn();
});
};
}, [activeSessionId]);
return (
<StyledWrapper>
<div className="terminal-content">
{/* Left Sidebar */}
<div className="terminal-sessions-sidebar">
<div className="terminal-sessions-header">
<span>Sessions</span>
<IconPlus
size={16}
style={{ cursor: 'pointer', color: '#888' }}
onClick={(e) => {
e.stopPropagation();
createNewSession();
}}
title="New Terminal Session"
/>
</div>
<div className="terminal-sessions-list">
{isLoading ? (
<div style={{ padding: '12px', color: '#888', fontSize: '13px' }}>
Loading sessions...
</div>
) : sessions.length === 0 ? (
<div style={{ padding: '12px', color: '#888', fontSize: '13px' }}>
No active sessions
</div>
) : (
<SessionList
sessions={sessions}
activeSessionId={activeSessionId}
onSelectSession={setActiveSessionId}
onCloseSession={closeSession}
/>
)}
</div>
</div>
{/* Right Terminal Display */}
<div className="terminal-display-container">
{!activeSessionId && window.ipcRenderer && (
<div className="terminal-loading">
<IconTerminal2 size={24} strokeWidth={1.5} />
<span>No terminal session selected</span>
</div>
)}
<div
ref={terminalRef}
className="terminal-container"
style={{
height: '100%',
width: '100%',
display: activeSessionId ? 'block' : 'none'
}}
/>
</div>
</div>
</StyledWrapper>
);
};
export default TerminalTab;

View File

@@ -2,12 +2,12 @@ import React, { useEffect, useRef, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import ReactJson from 'react-json-view';
import { useTheme } from 'providers/Theme';
import {
IconX,
IconTrash,
import {
IconX,
IconTrash,
IconFilter,
IconAlertTriangle,
IconAlertCircle,
IconAlertTriangle,
IconAlertCircle,
IconBug,
IconCode,
IconChevronDown,
@@ -15,10 +15,10 @@ import {
IconNetwork,
IconDashboard
} from '@tabler/icons';
import {
closeConsole,
clearLogs,
updateFilter,
import {
closeConsole,
clearLogs,
updateFilter,
toggleAllFilters,
setActiveTab,
clearDebugErrors,
@@ -27,6 +27,7 @@ import {
} from 'providers/ReduxStore/slices/logs';
import NetworkTab from './NetworkTab';
import TerminalTab from './TerminalTab';
import RequestDetailsPanel from './RequestDetailsPanel';
// import DebugTab from './DebugTab';
import ErrorDetailsPanel from './ErrorDetailsPanel';
@@ -35,7 +36,7 @@ import StyledWrapper from './StyledWrapper';
const LogIcon = ({ type }) => {
const iconProps = { size: 16, strokeWidth: 1.5 };
switch (type) {
case 'error':
return <IconAlertCircle className="log-icon error" {...iconProps} />;
@@ -52,20 +53,20 @@ const LogIcon = ({ type }) => {
const LogTimestamp = ({ timestamp }) => {
const date = new Date(timestamp);
const time = date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
const time = date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
});
return <span className="log-timestamp">{time}</span>;
};
const LogMessage = ({ message, args }) => {
const { displayedTheme } = useTheme();
const formatMessage = (msg, originalArgs) => {
if (originalArgs && originalArgs.length > 0) {
return originalArgs.map((arg, index) => {
@@ -84,7 +85,7 @@ const LogMessage = ({ message, args }) => {
name={false}
style={{
backgroundColor: 'transparent',
fontSize: '12px',
fontSize: '${(props) => props.theme.font.size.sm}',
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace'
}}
/>
@@ -98,7 +99,7 @@ const LogMessage = ({ message, args }) => {
};
const formattedMessage = formatMessage(message, args);
return (
<span className="log-message">
{Array.isArray(formattedMessage) ? formattedMessage.map((item, index) => (
@@ -112,7 +113,7 @@ const FilterDropdown = ({ filters, logCounts, onFilterToggle, onToggleAll }) =>
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const allFiltersEnabled = Object.values(filters).every(f => f);
const allFiltersEnabled = Object.values(filters).every((f) => f);
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
useEffect(() => {
@@ -128,7 +129,7 @@ const FilterDropdown = ({ filters, logCounts, onFilterToggle, onToggleAll }) =>
return (
<div className="filter-dropdown" ref={dropdownRef}>
<button
<button
className="filter-dropdown-trigger"
onClick={() => setIsOpen(!isOpen)}
title="Filter logs by type"
@@ -139,19 +140,19 @@ const FilterDropdown = ({ filters, logCounts, onFilterToggle, onToggleAll }) =>
</span>
<IconChevronDown size={14} strokeWidth={1.5} />
</button>
{isOpen && (
<div className={`filter-dropdown-menu right`}>
<div className="filter-dropdown-menu right">
<div className="filter-dropdown-header">
<span>Filter by Type</span>
<button
<button
className="filter-toggle-all"
onClick={() => onToggleAll(!allFiltersEnabled)}
>
{allFiltersEnabled ? 'Hide All' : 'Show All'}
</button>
</div>
<div className="filter-dropdown-options">
{Object.entries(filters).map(([filterType, enabled]) => (
<label key={filterType} className="filter-option">
@@ -178,7 +179,7 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const allFiltersEnabled = Object.values(filters).every(f => f);
const allFiltersEnabled = Object.values(filters).every((f) => f);
const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);
const getMethodColor = (method) => {
@@ -207,7 +208,7 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
return (
<div className="filter-dropdown" ref={dropdownRef}>
<button
<button
className="filter-dropdown-trigger"
onClick={() => setIsOpen(!isOpen)}
title="Filter requests by method"
@@ -218,19 +219,19 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
</span>
<IconChevronDown size={14} strokeWidth={1.5} />
</button>
{isOpen && (
<div className={`filter-dropdown-menu right`}>
<div className="filter-dropdown-menu right">
<div className="filter-dropdown-header">
<span>Filter by Method</span>
<button
<button
className="filter-toggle-all"
onClick={() => onToggleAll(!allFiltersEnabled)}
>
{allFiltersEnabled ? 'Hide All' : 'Show All'}
</button>
</div>
<div className="filter-dropdown-options">
{Object.entries(filters).map(([method, enabled]) => (
<label key={method} className="filter-option">
@@ -258,7 +259,7 @@ const NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggl
const ConsoleTab = ({ logs, filters, logCounts, onFilterToggle, onToggleAll, onClearLogs }) => {
const logsEndRef = useRef(null);
const prevLogsCountRef = useRef(0);
useEffect(() => {
// Only scroll when new logs are added, not when switching tabs
if (logsEndRef.current && logs.length > prevLogsCountRef.current) {
@@ -267,7 +268,7 @@ const ConsoleTab = ({ logs, filters, logCounts, onFilterToggle, onToggleAll, onC
prevLogsCountRef.current = logs.length;
}, [logs]);
const filteredLogs = logs.filter(log => filters[log.type]);
const filteredLogs = logs.filter((log) => filters[log.type]);
return (
<div className="tab-content">
@@ -299,8 +300,8 @@ const ConsoleTab = ({ logs, filters, logCounts, onFilterToggle, onToggleAll, onC
const Console = () => {
const dispatch = useDispatch();
const { logs, filters, activeTab, selectedRequest, selectedError, networkFilters, debugErrors } = useSelector(state => state.logs);
const collections = useSelector(state => state.collections.collections);
const { logs, filters, activeTab, selectedRequest, selectedError, networkFilters, debugErrors } = useSelector((state) => state.logs);
const collections = useSelector((state) => state.collections.collections);
const consoleRef = useRef(null);
const logCounts = logs.reduce((counts, log) => {
@@ -310,12 +311,12 @@ const Console = () => {
const allRequests = React.useMemo(() => {
const requests = [];
collections.forEach(collection => {
collections.forEach((collection) => {
if (collection.timeline) {
collection.timeline
.filter(entry => entry.type === 'request')
.forEach(entry => {
.filter((entry) => entry.type === 'request')
.forEach((entry) => {
requests.push({
...entry,
collectionName: collection.name,
@@ -324,12 +325,12 @@ const Console = () => {
});
}
});
return requests.sort((a, b) => a.timestamp - b.timestamp);
}, [collections]);
const filteredLogs = logs.filter(log => filters[log.type]);
const filteredRequests = allRequests.filter(request => {
const filteredLogs = logs.filter((log) => filters[log.type]);
const filteredRequests = allRequests.filter((request) => {
const method = request.data?.request?.method?.toUpperCase() || 'GET';
return networkFilters[method];
});
@@ -389,6 +390,8 @@ const Console = () => {
return <NetworkTab />;
case 'performance':
return <Performance />;
case 'terminal':
return <TerminalTab />;
// case 'debug':
// return <DebugTab />;
default:
@@ -419,7 +422,7 @@ const Console = () => {
/>
</div>
<div className="action-controls">
<button
<button
className="control-button"
onClick={handleClearLogs}
title="Clear all logs"
@@ -442,12 +445,14 @@ const Console = () => {
</div>
</div>
);
case 'terminal':
return null; // No controls needed for terminal
// case 'debug':
// return (
// <div className="tab-controls">
// <div className="action-controls">
// {debugErrors.length > 0 && (
// <button
// <button
// className="control-button"
// onClick={handleClearDebugErrors}
// title="Clear all errors"
@@ -463,32 +468,30 @@ const Console = () => {
}
};
return (
<StyledWrapper ref={consoleRef}>
<div
<div
className="console-resize-handle"
/>
<div className="console-header">
<div className="console-tabs">
<button
<button
className={`console-tab ${activeTab === 'console' ? 'active' : ''}`}
onClick={() => handleTabChange('console')}
>
<IconTerminal2 size={16} strokeWidth={1.5} />
<span>Console</span>
</button>
<button
<button
className={`console-tab ${activeTab === 'network' ? 'active' : ''}`}
onClick={() => handleTabChange('network')}
>
<IconNetwork size={16} strokeWidth={1.5} />
<span>Network</span>
</button>
<button
className={`console-tab ${activeTab === 'performance' ? 'active' : ''}`}
onClick={() => handleTabChange('performance')}
@@ -497,7 +500,15 @@ const Console = () => {
<span>Performance</span>
</button>
{/* <button
<button
className={`console-tab ${activeTab === 'terminal' ? 'active' : ''}`}
onClick={() => handleTabChange('terminal')}
>
<IconTerminal2 size={16} strokeWidth={1.5} />
<span>Terminal</span>
</button>
{/* <button
className={`console-tab ${activeTab === 'debug' ? 'active' : ''}`}
onClick={() => handleTabChange('debug')}
>
@@ -508,7 +519,7 @@ const Console = () => {
<div className="console-controls">
{renderTabControls()}
<button
<button
className="control-button close-button"
onClick={handlecloseConsole}
title="Close console"
@@ -541,4 +552,4 @@ const Console = () => {
);
};
export default Console;
export default Console;

View File

@@ -35,13 +35,13 @@ const StyledWrapper = styled.div`
h3 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
font-weight: 500;
color: ${(props) => props.theme.console.titleColor};
}
p {
margin: 0;
font-size: 13px;
font-size: ${(props) => props.theme.font.size.base};
color: ${(props) => props.theme.console.textMuted};
}
}
@@ -51,8 +51,8 @@ const StyledWrapper = styled.div`
h2 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
font-size: ${(props) => props.theme.font.size.base};
font-weight: 500;
color: ${(props) => props.theme.console.titleColor};
}
}
@@ -80,19 +80,19 @@ const StyledWrapper = styled.div`
}
.resource-title {
font-size: 12px;
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
}
.resource-value {
font-size: 18px;
font-weight: 600;
font-weight: 500;
color: ${(props) => props.theme.console.titleColor};
margin-bottom: 2px;
}
.resource-subtitle {
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
color: ${(props) => props.theme.console.buttonColor};
}
@@ -100,7 +100,7 @@ const StyledWrapper = styled.div`
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-size: ${(props) => props.theme.font.size.xs};
margin-top: 8px;
&.up {
@@ -115,6 +115,237 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.console.buttonColor};
}
}
.performance-header {
display: flex;
align-items: center;
border-bottom: 1px solid ${(props) => props.theme.console.border};
padding: 12px 16px;
background: ${(props) => props.theme.console.headerBg};
}
.performance-selector-wrapper {
display: flex;
align-items: center;
gap: 12px;
}
.performance-selector-label {
font-size: 13px;
font-weight: 500;
color: ${(props) => props.theme.console.titleColor};
user-select: none;
}
.performance-selector {
position: relative;
display: inline-flex;
align-items: center;
}
.performance-select {
appearance: none;
background: ${(props) => props.theme.console.bg};
border: 1px solid ${(props) => props.theme.console.border};
border-radius: 4px;
padding: 6px 32px 6px 12px;
font-size: 13px;
font-weight: 500;
color: ${(props) => props.theme.console.titleColor};
cursor: pointer;
outline: none;
transition: all 0.2s ease;
min-width: 250px;
max-width: 400px;
&:hover {
border-color: ${(props) => props.theme.colors.primary};
}
&:focus {
border-color: ${(props) => props.theme.colors.primary};
box-shadow: 0 0 0 2px ${(props) => props.theme.colors.primary}33;
}
option {
background: ${(props) => props.theme.console.bg};
color: ${(props) => props.theme.console.titleColor};
padding: 8px;
}
}
.performance-select-icon {
position: absolute;
right: 10px;
pointer-events: none;
color: ${(props) => props.theme.console.buttonColor};
}
.processes-table-container {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
h2 {
margin: 0 0 16px 0;
font-size: 14px;
font-weight: 600;
color: ${(props) => props.theme.console.titleColor};
flex-shrink: 0;
}
}
.no-processes {
padding: 32px;
text-align: center;
color: ${(props) => props.theme.console.buttonColor};
font-size: 13px;
}
.processes-table-wrapper {
flex: 1;
min-height: 0;
overflow: auto;
}
.processes-table {
width: 100%;
border-collapse: collapse;
background: ${(props) => props.theme.console.headerBg};
border: 1px solid ${(props) => props.theme.console.border};
border-radius: 4px;
overflow: hidden;
thead {
background: ${(props) => props.theme.console.bg};
border-bottom: 1px solid ${(props) => props.theme.console.border};
th {
padding: 10px 12px;
text-align: left;
font-size: 12px;
font-weight: 600;
color: ${(props) => props.theme.console.titleColor};
text-transform: uppercase;
letter-spacing: 0.5px;
&:first-child {
padding-left: 16px;
}
&:last-child {
padding-right: 16px;
}
}
}
tbody {
tr {
border-bottom: 1px solid ${(props) => props.theme.console.border};
transition: background 0.15s ease;
&:hover {
background: ${(props) => props.theme.console.bg};
}
&:last-child {
border-bottom: none;
}
}
td {
padding: 10px 12px;
font-size: 13px;
color: ${(props) => props.theme.console.textColor};
&:first-child {
padding-left: 16px;
}
&:last-child {
padding-right: 16px;
}
}
}
.pid-cell {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
color: ${(props) => props.theme.console.buttonColor};
}
.type-cell {
.process-type-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: 500;
text-transform: lowercase;
background: ${(props) => props.theme.console.border};
color: ${(props) => props.theme.console.buttonColor};
&.Browser {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
&.Renderer {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
&.Utility {
background: rgba(139, 92, 246, 0.2);
color: #8b5cf6;
}
&.Zygote {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
&.Sandbox {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
}
}
.title-cell {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cpu-cell {
font-weight: 500;
.high-cpu {
color: #ef4444;
}
.medium-cpu {
color: #f59e0b;
}
.low-cpu {
color: ${(props) => props.theme.console.buttonColor};
}
}
.memory-cell {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 12px;
}
.created-cell {
font-size: 12px;
color: ${(props) => props.theme.console.buttonColor};
}
}
`;
export default StyledWrapper;

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import { useSelector } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import {
@@ -6,11 +6,23 @@ import {
IconDatabase,
IconClock,
IconServer,
IconChevronDown,
IconChartLine
} from '@tabler/icons';
const getProcessOptions = (processes) => {
return [
{ value: 'cumulative', label: 'Cumulative (All Processes)' },
...(processes ?? []).map((process) => ({
value: String(process.pid),
label: `PID ${process.pid}${process.title ? ` - ${process.title}` : ''}${process.type ? ` (${process.type})` : ''}`
}))
];
};
const Performance = () => {
const { systemResources } = useSelector((state) => state.performance);
const [selectedPid, setSelectedPid] = useState('cumulative');
useEffect(() => {
const { ipcRenderer } = window;
@@ -82,47 +94,140 @@ const Performance = () => {
</div>
);
// Get process options for dropdown
const processOptions = useMemo(() => getProcessOptions(systemResources.processes), [systemResources.processes]);
// Get selected process data
const selectedProcess = useMemo(() => {
if (selectedPid === 'cumulative') {
return null; // Show cumulative view
}
const processes = systemResources.processes || [];
return processes.find((p) => String(p.pid) === selectedPid) || null;
}, [selectedPid, systemResources.processes]);
// Reset to cumulative if selected PID no longer exists
useEffect(() => {
if (selectedPid !== 'cumulative' && !selectedProcess) {
setSelectedPid('cumulative');
}
}, [selectedPid, selectedProcess]);
const renderCumulativeView = () => (
<div className="system-resources">
<h2>System Resources</h2>
<div className="resource-cards">
<SystemResourceCard
icon={IconCpu}
title="CPU Usage"
value={`${systemResources.cpu.toFixed(1)}%`}
subtitle="Total CPU usage"
color={systemResources.cpu > 80 ? 'danger' : systemResources.cpu > 60 ? 'warning' : 'success'}
/>
<SystemResourceCard
icon={IconDatabase}
title="Memory Usage"
value={formatBytes(systemResources.memory)}
subtitle="Total memory usage"
color={systemResources.memory > (500 * 1024 * 1024) ? 'danger' : 'default'}
/>
<SystemResourceCard
icon={IconClock}
title="Uptime"
value={formatUptime(systemResources.uptime)}
subtitle="Process runtime"
color="info"
/>
<SystemResourceCard
icon={IconServer}
title="Process ID"
value={systemResources.pid || 'N/A'}
subtitle="Main process PID"
color="default"
/>
</div>
</div>
);
const renderProcessView = (process) => {
if (!process) return null;
// Calculate uptime for individual process
const processUptime = process.creationTime
? (new Date() - new Date(process.creationTime)) / 1000
: 0;
return (
<div className="system-resources">
<h2>System Resources</h2>
<div className="resource-cards">
<SystemResourceCard
icon={IconCpu}
title="CPU Usage"
value={`${process.cpu.toFixed(1)}%`}
subtitle="Current CPU usage"
color={process.cpu > 80 ? 'danger' : process.cpu > 60 ? 'warning' : 'success'}
/>
<SystemResourceCard
icon={IconDatabase}
title="Memory Usage"
value={formatBytes(process.memory)}
subtitle="Current memory usage"
color={process.memory > (500 * 1024 * 1024) ? 'danger' : 'default'}
/>
<SystemResourceCard
icon={IconClock}
title="Uptime"
value={formatUptime(processUptime)}
subtitle="Process runtime"
color="info"
/>
<SystemResourceCard
icon={IconServer}
title="Process ID"
value={process.pid}
subtitle="Process PID"
color="default"
/>
</div>
</div>
);
};
return (
<StyledWrapper>
<div className="tab-content">
<div className="tab-content-area">
<div className="system-resources">
<h2>System Resources</h2>
<div className="resource-cards">
<SystemResourceCard
icon={IconCpu}
title="CPU Usage"
value={`${systemResources.cpu.toFixed(1)}%`}
subtitle="Current process"
color={systemResources.cpu > 80 ? 'danger' : systemResources.cpu > 60 ? 'warning' : 'success'}
/>
<SystemResourceCard
icon={IconDatabase}
title="Memory Usage"
value={formatBytes(systemResources.memory)}
subtitle="Current process"
color={systemResources.memory > 500 * 1024 * 1024 ? 'danger' : 'default'}
/>
<SystemResourceCard
icon={IconClock}
title="Uptime"
value={formatUptime(systemResources.uptime)}
subtitle="Process runtime"
color="info"
/>
<SystemResourceCard
icon={IconServer}
title="Process ID"
value={systemResources.pid || 'N/A'}
subtitle="Current PID"
color="default"
/>
<div className="performance-header">
<div className="performance-selector-wrapper">
<label htmlFor="process-selector" className="performance-selector-label">
View:
</label>
<div className="performance-selector">
<select
id="process-selector"
value={selectedPid}
onChange={(e) => setSelectedPid(e.target.value)}
className="performance-select"
>
{processOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<IconChevronDown size={16} className="performance-select-icon" />
</div>
</div>
</div>
<div className="tab-content-area">
{selectedPid === 'cumulative' ? renderCumulativeView() : renderProcessView(selectedProcess)}
</div>
</div>
</StyledWrapper>
);

View File

@@ -18,11 +18,11 @@ const Devtools = ({ mainSectionRef }) => {
const handleDevtoolsResize = useCallback((e) => {
if (!isResizingDevtools || !mainSectionRef.current) return;
const windowHeight = window.innerHeight;
const statusBarHeight = 22;
const mouseY = e.clientY;
// Calculate new devtools height - expanding upward from bottom
const newHeight = windowHeight - mouseY - statusBarHeight;
const clampedHeight = Math.min(MAX_DEVTOOLS_HEIGHT, Math.max(MIN_DEVTOOLS_HEIGHT, newHeight));
@@ -43,7 +43,7 @@ const Devtools = ({ mainSectionRef }) => {
document.addEventListener('mousemove', handleDevtoolsResize);
document.addEventListener('mouseup', handleDevtoolsResizeEnd);
document.body.style.userSelect = 'none';
return () => {
document.removeEventListener('mousemove', handleDevtoolsResize);
document.removeEventListener('mouseup', handleDevtoolsResizeEnd);
@@ -65,7 +65,7 @@ const Devtools = ({ mainSectionRef }) => {
return (
<>
<div
<div
onMouseDown={handleDevtoolsResizeStart}
style={{
height: '4px',
@@ -85,4 +85,4 @@ const Devtools = ({ mainSectionRef }) => {
);
};
export default Devtools;
export default Devtools;

View File

@@ -8,44 +8,66 @@ const Wrapper = styled.div`
}
.tippy-box {
min-width: 135px;
font-size: 0.8125rem;
min-width: 160px;
font-size: ${(props) => props.theme.font.size.base};
color: ${(props) => props.theme.dropdown.color};
background-color: ${(props) => props.theme.dropdown.bg};
box-shadow: ${(props) => props.theme.dropdown.shadow};
border-radius: 3px;
box-shadow: ${(props) => props.theme.shadow.sm};
border-radius: ${(props) => props.theme.border.radius.base};
max-height: 90vh;
overflow-y: auto;
max-width: unset !important;
padding: 0.25rem;
.tippy-content {
padding-left: 0;
padding-right: 0;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
padding-top: 0;
padding-bottom: 0;
.label-item {
display: flex;
align-items: center;
padding: 0.35rem 0.6rem;
background-color: ${(props) => props.theme.dropdown.labelBg};
padding: 0.375rem 0.625rem 0.25rem 0.625rem;
font-size: 0.6875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.025em;
color: ${(props) => props.theme.dropdown.color};
opacity: 0.6;
margin-top: 0.25rem;
&:first-child {
margin-top: 0;
}
}
.dropdown-item {
display: flex;
align-items: center;
padding: 0.35rem 0.6rem;
gap: 0.5rem;
padding: 0.275rem 0.625rem;
cursor: pointer;
border-radius: 6px;
margin: 0.0625rem 0;
font-size: 0.8125rem;
&.active {
color: ${(props) => props.theme.colors.text.yellow} !important;
.icon {
.dropdown-icon {
color: ${(props) => props.theme.colors.text.yellow} !important;
}
}
.icon {
.dropdown-icon {
flex-shrink: 0;
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
color: ${(props) => props.theme.dropdown.iconColor};
opacity: 0.8;
}
&:hover:not(:disabled) {
@@ -54,13 +76,39 @@ const Wrapper = styled.div`
&:disabled {
cursor: not-allowed;
color: gray;
opacity: 0.5;
}
&.delete-item {
color: ${(props) => props.theme.colors.text.danger};
.dropdown-icon {
color: ${(props) => props.theme.colors.text.danger};
}
&:hover {
background-color: ${({ theme }) => {
const hex = theme.colors.text.danger.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, 0.04)`; // 4% opacity
}} !important;
color: ${(props) => props.theme.colors.text.danger} !important;
}
}
&.border-top {
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
margin-top: 0.25rem;
padding-top: 0.375rem;
}
}
.dropdown-separator {
height: 1px;
background-color: ${(props) => props.theme.dropdown.separator};
margin: 0.25rem 0;
}
}
}
`;

View File

@@ -146,7 +146,7 @@ const ExportEnvironmentModal = ({ onClose, environments = [], environmentType })
{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">
<h3 className="font-medium text-theme">
{environmentType === 'global' ? 'Global Environments' : 'Collection Environments'}
</h3>
<button
@@ -175,7 +175,7 @@ const ExportEnvironmentModal = ({ onClose, environments = [], environmentType })
) : (
<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">
<h3 className="font-medium text-theme">
{environmentType === 'global' ? 'Global Environments' : 'Collection Environments'}
</h3>
</div>
@@ -191,7 +191,7 @@ const ExportEnvironmentModal = ({ onClose, environments = [], environmentType })
{/* Export Format Section */}
{selectedCount > 0 && (
<div className="mb-4">
<label className="block text-sm font-medium mb-2 text-theme">
<label className="block font-medium mb-2 text-theme">
Export Format
</label>
<div className="space-y-2">
@@ -207,7 +207,7 @@ const ExportEnvironmentModal = ({ onClose, environments = [], environmentType })
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={`font-medium ${option.disabled ? 'text-muted' : 'text-theme'}`}>{option.label}</div>
<div className="text-xs text-muted">{option.description}</div>
</div>
</label>
@@ -218,7 +218,7 @@ const ExportEnvironmentModal = ({ onClose, environments = [], environmentType })
{/* Location Input Section */}
<div className="mb-4">
<label htmlFor="export-location" className="block text-sm font-medium mb-2 text-theme">
<label htmlFor="export-location" className="block font-medium mb-2 text-theme">
Location
</label>
<div className="flex flex-col relative items-center">

View File

@@ -150,7 +150,7 @@ const ImportEnvironmentModal = ({ type = 'collection', collection, onClose, onEn
data-testid={importTestId}
>
<IconFileImport size={64} />
<span className="mt-2 block text-sm font-semibold">
<span className="mt-2 block font-medium">
{isDragOver ? 'Drop your environment files here' : 'Import your environments'}
</span>
<span className="mt-1 block text-xs text-muted">

View File

@@ -22,7 +22,7 @@ const Wrapper = styled.div`
.env-text {
color: ${(props) => props.theme.dropdown.selectedColor};
font-size: 0.875rem;
font-size: ${(props) => props.theme.font.size.base};
display: block;
}
@@ -34,7 +34,7 @@ const Wrapper = styled.div`
.env-text-inactive {
color: ${(props) => props.theme.dropdown.color};
font-size: 0.875rem;
font-size: ${(props) => props.theme.font.size.base};
opacity: 0.7;
}
@@ -51,7 +51,7 @@ const Wrapper = styled.div`
max-width: 650px !important;
min-height: 15.5rem;
max-height: 75vh;
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
position: relative;
overflow: hidden;
}
@@ -67,7 +67,7 @@ const Wrapper = styled.div`
align-items: center;
padding: 0.35rem 0.6rem;
cursor: pointer;
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
color: ${(props) => props.theme.dropdown.primaryText};
&:hover:not(:disabled) {
@@ -111,7 +111,7 @@ const Wrapper = styled.div`
.tab-button {
color: var(--color-tab-inactive);
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
.tab-content-wrapper {
position: relative;
@@ -163,7 +163,7 @@ const Wrapper = styled.div`
h3 {
color: ${(props) => props.theme.dropdown.primaryText};
font-size: 1rem;
font-weight: 600;
font-weight: 500;
margin-bottom: 0.5rem;
line-height: 1.4;
}
@@ -171,7 +171,7 @@ const Wrapper = styled.div`
p {
color: ${(props) => props.theme.dropdown.primaryText};
opacity: 0.75;
font-size: 0.6875rem;
font-size: ${(props) => props.theme.font.size.xs};
line-height: 1.5;
margin-bottom: 1rem;
max-width: 11.875rem;
@@ -192,7 +192,7 @@ const Wrapper = styled.div`
border-radius: 0.375rem;
width: 100%;
margin-bottom: 0.5rem;
font-size: 0.75rem;
font-size: ${(props) => props.theme.font.size.sm};
font-weight: 500;
display: flex;
align-items: center;
@@ -216,7 +216,7 @@ const Wrapper = styled.div`
justify-content: center;
padding: 2rem 1rem;
color: ${(props) => props.theme.dropdown.primaryText};
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
line-height: 1.5;
text-align: center;
opacity: 0.75;

View File

@@ -49,15 +49,15 @@ const EnvironmentSelector = ({ collection }) => {
};
// Get description based on active tab
const description =
activeTab === 'collection'
const description
= activeTab === 'collection'
? 'Create your first environment to begin working with your collection.'
: 'Create your first global environment to begin working across collections.';
// Environment selection handler
const handleEnvironmentSelect = (environment) => {
const action =
activeTab === 'collection'
const action
= activeTab === 'collection'
? selectEnvironment(environment ? environment.uid : null, collection.uid)
: selectGlobalEnvironment({ environmentUid: environment ? environment.uid : null });

View File

@@ -43,10 +43,10 @@ const CopyEnvironment = ({ collection, environment, onClose }) => {
return (
<Portal>
<Modal size="sm" title={'Copy Environment'} confirmText="Copy" handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
<Modal size="sm" title="Copy Environment" confirmText="Copy" handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
<div>
<label htmlFor="name" className="block font-semibold">
<label htmlFor="name" className="block font-medium">
New Environment Name
</label>
<input

View File

@@ -12,9 +12,9 @@ const CreateEnvironment = ({ collection, onClose, onEnvironmentCreated }) => {
const dispatch = useDispatch();
const inputRef = useRef();
const validateEnvironmentName = (name) => {
return !collection?.environments?.some((env) => env?.name?.toLowerCase().trim() === name?.toLowerCase().trim());
};
const validateEnvironmentName = (name) => {
return !collection?.environments?.some((env) => env?.name?.toLowerCase().trim() === name?.toLowerCase().trim());
};
const formik = useFormik({
enableReinitialize: true,
@@ -25,7 +25,7 @@ const CreateEnvironment = ({ collection, onClose, onEnvironmentCreated }) => {
name: Yup.string()
.min(1, 'Must be at least 1 character')
.max(255, 'Must be 255 characters or less')
.test('is-valid-filename', function(value) {
.test('is-valid-filename', function (value) {
const isValid = validateName(value);
return isValid ? true : this.createError({ message: validateNameError(value) });
})
@@ -60,14 +60,14 @@ const CreateEnvironment = ({ collection, onClose, onEnvironmentCreated }) => {
<Portal>
<Modal
size="sm"
title={'Create Environment'}
title="Create Environment"
confirmText="Create"
handleConfirm={onSubmit}
handleCancel={onClose}
>
<form className="bruno-form" onSubmit={e => e.preventDefault()}>
<form className="bruno-form" onSubmit={(e) => e.preventDefault()}>
<div>
<label htmlFor="name" className="block font-semibold">
<label htmlFor="name" className="block font-medium">
Environment Name
</label>
<div className="flex items-center mt-2">

View File

@@ -22,12 +22,12 @@ const DeleteEnvironment = ({ onClose, environment, collection }) => {
<StyledWrapper>
<Modal
size="sm"
title={'Delete Environment'}
title="Delete Environment"
confirmText="Delete"
handleConfirm={onConfirm}
handleCancel={onClose}
>
Are you sure you want to delete <span className="font-semibold">{environment.name}</span> ?
Are you sure you want to delete <span className="font-medium">{environment.name}</span> ?
</Modal>
</StyledWrapper>
</Portal>

View File

@@ -22,7 +22,7 @@ const ConfirmSwitchEnv = ({ onCancel }) => {
>
<div className="flex items-center font-normal">
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
<h1 className="ml-2 text-lg font-medium">Hold on..</h1>
</div>
<div className="font-normal mt-4">You have unsaved changes in this environment.</div>

View File

@@ -4,7 +4,7 @@ const Wrapper = styled.div`
table {
width: 100%;
border-collapse: collapse;
font-weight: 600;
font-weight: 500;
table-layout: fixed;
thead,
@@ -27,7 +27,7 @@ const Wrapper = styled.div`
thead {
color: ${(props) => props.theme.table.thead.color};
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
user-select: none;
}
thead td {
@@ -36,11 +36,11 @@ const Wrapper = styled.div`
}
.btn-add-param {
font-size: 0.8125rem;
font-size: ${(props) => props.theme.font.size.base};
}
.tooltip-mod {
font-size: 11px !important;
font-size: ${(props) => props.theme.font.size.xs} !important;
width: 150px !important;
}

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