Compare commits

...

84 Commits

Author SHA1 Message Date
Anoop M D
e31c552dee release: v1.26.0 2024-08-21 18:46:55 +05:30
Anoop M D
753a576c3c Feat/safe mode quickjs (#2848)
Safe Mode Sandbox using QuickJS
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
Co-authored-by: lohit <lohit.jiddimani@gmail.com>
2024-08-21 12:52:49 +05:30
Pragadesh-45
3ad4eda861 Style/assert option bg (#2867)
* remove old theme configs

* style bg manage from Assertion Comp for AssertionOperator
2024-08-21 10:37:59 +05:30
Sushant Kumar
d3e57d0ea6 refactor: Simplify logic around get method color (#2856)
* feat: Use theme provider to get method color

* fix: Use storeTheme instead of theme
2024-08-19 18:40:52 +05:30
Niklas Ziermann
77750ecc0b #2757: Visualize if request body, script or tests have content (#2809) 2024-08-19 18:37:47 +05:30
Chae Jeong Ah
a1783c46ed fix: invalid file path in shell-curl (#2855) 2024-08-19 18:18:39 +05:30
Daniel Roberto
017d2235b8 fix: remove duplicate tailwind classes in RequestNotFound component (#2801) 2024-08-16 20:08:33 +05:30
Adrian
df120787ca fix: remove scope with auth code grant (#2815) 2024-08-16 20:05:33 +05:30
Pragadesh-45
b872fdfe6d Bugfix- Import blank directory Exception (#2845)
* typofix: Loc is required

* handle empty dirpath on import collection

* fix: collection import bug fix

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-16 17:49:00 +05:30
Sushant Kumar
f0b7bf3430 feat: Reuse dictionary in preferences support page (#2834)
* feat: Re-use dictonary text in Preferences/Support component

* feat: Re-use dictionary text in Preferences/Support component
2024-08-16 15:49:27 +05:30
Chae Jeong Ah
4169bb7ea4 fix: add path params on newHttpRequest (#2843) 2024-08-16 15:46:38 +05:30
Sushant Kumar
74b1527513 feat: Add support for dictionary and use in Welcome page (#2819) 2024-08-14 15:53:04 +05:30
Daniel Roberto
3b8909e301 feat: add hotkey to close all tabs (#2800) 2024-08-14 15:43:32 +05:30
Rinku Chaudhari
1bedfc2046 feat: added request tab context menu (#2183)
* feat: added close menus on the request tab

* feat: added close to the left button

* feat: added new request and clone request buttons

* chore: fix prettier
2024-08-14 15:40:44 +05:30
Timon
8de6b72ab9 Fix/enospc (#2789)
* fix: Handle ENOSPC error from chokidar

Now listens to the error event to check if "ENOSPC" occurrs.
The watcher will then automaticly restart in polling mode, so that
the user still sees his reqeusts / collections.

Fixes: https://github.com/usebruno/bruno/issues/1877

* Add more code comments, add !forcePolling to prevent endless loops and update error message

* fix: Also listen for EMFILE for too many watched files
2024-08-14 15:20:17 +05:30
Joel Wetzell
9d84906f57 add ability for curl to import basic auth (#2778) 2024-08-14 15:18:24 +05:30
lohit
eceb114d6c fix/collection-search-validations unit-tests-fix (#2833)
* fix: updates

* fix: update test title

* fix: removed console
2024-08-14 13:23:00 +05:30
Anoop M D
22c096507d chore: bumped version 2024-08-14 10:36:32 +05:30
Sam Wooler
85ad4c0159 fix: align folder settings pane + scripts with request pane (#2817) 2024-08-14 10:24:17 +05:30
Sanjai Kumar
b23a866e60 Removed the logic that strips the backslash (#2793) 2024-08-12 17:21:51 +05:30
Sanjai Kumar
5706c4b138 Added the ablility to save the response in utf8 format. (#2792) 2024-08-12 17:20:56 +05:30
Rinku Chaudhari
0d3e7acf9b fix: query url overflowing (#2804)
* fix: query url overflowing

* fix: add margin to save btn instead of singlelineeditor
2024-08-11 19:24:51 +05:30
Sushant Kumar
7e305be817 (feat) Add shade to modal header in dark mode (#2784) 2024-08-08 18:39:29 +05:30
Niklas
4c3fe2f719 Add TRACE to allowed import methods (#2783) 2024-08-08 17:28:17 +05:30
Rinku Chaudhari
de226d2e44 feat: added runner delay (#2218)
* feat: added runner delay

* fix: check if delay is greater than 0

* fix: input type number and added missing onclick
2024-08-08 17:13:17 +05:30
Timon
1e0c88a291 fix: Handle ENOSPC error from chokidar (#2725)
* fix: Handle ENOSPC error from chokidar

Now listens to the error event to check if "ENOSPC" occurrs.
The watcher will then automaticly restart in polling mode, so that
the user still sees his reqeusts / collections.

Fixes: https://github.com/usebruno/bruno/issues/1877

* Add more code comments, add !forcePolling to prevent endless loops and update error message
2024-08-08 16:04:54 +05:30
Anoop M D
29db85a916 chore: placeholder ux improvements 2024-08-08 16:00:56 +05:30
Pragadesh-45
edb8708dde UX - improvements Input Placeholders (#2780)
* add placeholders

* placeholders for clone collection

* placeholders for inputs, placeholder-global-opacity, change cursor type for clickables

* revert placeholders for collection creation and collection cloning

* revert c-placeholder

* revert: cliert cert placeholder
2024-08-08 15:48:54 +05:30
lohit
9fdfee0083 fix: generate oced modal height style (#2772) 2024-08-07 17:32:27 +05:30
Anoop M D
72c3aaa5ba chore: bumped version 2024-08-07 12:21:46 +05:30
Anoop M D
eb1c10fd6e feat: mac specific os styling 2024-08-06 18:49:35 +05:30
Sam Ho
911e3aa589 feat: comment with keybinding (cmd + /) in JSON payload interface (#2634)
Co-authored-by: Sam Ho <kwunting.ho@bt.com>
2024-08-06 17:36:51 +05:30
Pragadesh-45
2358aa4cdc Bugfix/window UI distortion electron (#2765)
* set initial window: false

* set window: true after loading the window state

* added ready-to-show event
2024-08-06 16:08:32 +05:30
Joel Wetzell
800dbcfdbc add cache to cli-test job in tests workflow (#2766) 2024-08-06 16:05:05 +05:30
Anoop M D
d7ec3d1cc5 Revert "fix: BigInters are now correctly shown in the response (#2736)" (#2768)
This reverts commit 3e2a3b65a4.
2024-08-06 16:00:49 +05:30
Vincenzo De Petris
92073e7573 fix: draft variables and headers (#2651)
* fix: extract variables and headers from the provided request

* fix: handling draft headers and vars

* fix: handling draft headers and vars

---------

Co-authored-by: Vincenzo De Petris <vincenzodepetris@gmail.it>
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-08-06 14:32:54 +05:30
Timon
3e2a3b65a4 fix: BigInters are now correctly shown in the response (#2736)
Fixes: #1753
2024-08-06 13:01:30 +05:30
Chae Jeong Ah
aa4bcdca9b fix: type error in jsonToBru during import collection (#2761)
* fix: add optional chaining to prevent type error

* fix: remove debug code
2024-08-06 12:43:47 +05:30
Joel Wetzell
12fdbbb291 add cache to tests workflow (#2762) 2024-08-06 12:41:57 +05:30
Anoop M D
7cafed6c93 chore: temporarily reverting the change related to pr - #2713 2024-08-06 12:12:47 +05:30
Radovan Mihálik
8a9df14e16 Create contributing_sk.md (#1524) 2024-08-05 18:38:05 +05:30
Joel Wetzell
b2b41fec1a set content-type header during code generation (#2491)
* set content-type header during code generation

handles missing request body modes as well

* formatting
2024-08-05 18:22:17 +05:30
LucasVermersch
a8aa54cf1b Fix: contributing_fr.md (#2500)
* fix contributing_fr.md

* add space

---------

Co-authored-by: Lucas Vermersch <lvermersch@access-it.fr>
2024-08-05 18:20:03 +05:30
Rinku Chaudhari
60a8647e7c fix: enter key not submitting new request form (#2630) 2024-08-05 17:57:19 +05:30
Pragadesh-45
7ca59656f2 set inital window-show: false (#2713) 2024-08-05 17:55:06 +05:30
Anoop M D
4598bb1bdd fix(#2605): fix editor view height (#2758)
* fix(#2605): fix editor view height

* chore: disabled prettier on github actions
2024-08-05 17:49:04 +05:30
Joel Wetzell
adb843faa7 don't exclude cookies from request headers when importing from curl (#2748) 2024-08-05 12:27:00 +05:30
Bruno Grasselli
2b0d55ce6b Update missing sponsor in readme_pt_br.md (#2754) 2024-08-05 12:24:04 +05:30
Natalie Carey
c5ec7eea34 Feature: Add a show/hide privacy toggle to passwords and secrets in Auth options (#2750)
* mask support for SingleLineEditor

* add secret visibility toggle button

* move visibility toggle into SingleLineComponent

Co-authored-by: Liz MacLean <18120837+lizziemac@users.noreply.github.com>

* fix eye button focus state

* center enabled and secret toggle

* fix input field scales to 100% width

* Using a prvacy toggle for all sensitive auth details.

* Applied privacy toggle to Collection Auth settings.

---------

Co-authored-by: Max Bauer <krummbar@pm.me>
Co-authored-by: Liz MacLean <18120837+lizziemac@users.noreply.github.com>
2024-08-05 11:51:01 +05:30
Max Bauer
741250068f feat: masking support for SingleLineEditor (#2240)
* mask support for SingleLineEditor

* add secret visibility toggle button

* move visibility toggle into SingleLineComponent

Co-authored-by: Liz MacLean <18120837+lizziemac@users.noreply.github.com>

* fix eye button focus state

* center enabled and secret toggle

* fix input field scales to 100% width

---------

Co-authored-by: Liz MacLean <18120837+lizziemac@users.noreply.github.com>
2024-08-05 11:46:06 +05:30
Natalie Carey
7c33fd413e Refactored handlers into reusable functions for readability. (#2744) 2024-08-02 12:10:04 +05:30
Anoop M D
8f920a90c7 chore: bumped version 2024-08-01 18:16:54 +05:30
Anoop M D
640623b39a chore: version bumped to v1.23.0 2024-07-31 17:57:39 +05:30
lohit
37bec70fe6 fix: removed unused code (#2729) 2024-07-31 16:13:55 +05:30
lohit
98c53cf443 update natural to sequential (#2717) 2024-07-29 11:04:29 +05:30
Mateusz Pietryga
6fe96a8194 bugfix/test - update Jest configuration to fix unit tests (#2672) 2024-07-26 18:24:44 +05:30
Mateusz Pietryga
f2ba351f0d Fix: OAuth 2.0 Grant Type Authorization: "invalid_client" error / URL Encode of Client ID (#2129)
#2115
#1003
2024-07-26 18:17:38 +05:30
Timon
2e2c60d90e feat: Add flow option for "natural" flow in scripts (#2704)
- Adds a new key in the `bruno.json` under `scripts.flow`
- When concating post and tests scripts the flow will now be used
  to determine to correct order

Fixes: #2648 #2680 #2597 #2639
2024-07-26 18:10:10 +05:30
BruAlcaraz
1d2e06d419 Fix test results when the same request is executed more than 1 time (#2522) (#2551)
Co-authored-by: Alcaraz, Bruno <Bruno.Alcaraz@ulgroup.com>
2024-07-25 17:05:00 +05:30
BruAlcaraz
c99da3a581 Allow bru.setNextRequest() on Test Scripts (#2155) (#2552)
Co-authored-by: Alcaraz, Bruno <Bruno.Alcaraz@ulgroup.com>
2024-07-25 16:57:55 +05:30
Anoop M D
073c1aae12 chore: bumped version 2024-07-23 17:19:25 +05:30
Anoop M D
398c833393 chore: package-lock update 2024-07-19 18:45:01 +05:30
lohit
47724b1b1e scrollbar fix (#2670) 2024-07-19 18:43:51 +05:30
Sanjai Kumar
2804ce1eb3 Revert "fix: active enviroment after rename when there is single enviroment (…" (#2660)
This reverts commit 81497d8397.
2024-07-19 16:51:12 +05:30
lohit
9892f7cd40 Feat/electron bump (#2668)
* pr review changes

* collection root object in export json

* import environment updates

* tests run execution order fix for collection runs

* updated validations

* accept request flag in curl string for method type

* electron version bump to v31.2.1
2024-07-19 16:29:10 +05:30
lohit
7194998b0e update node-machine-id to @usebruno/node-machine-id (#2661)
* update node-machine-id to @usebruno/node-machine-id

* added lock file

* tests check

* tests check
2024-07-18 17:19:22 +05:30
lohit
ab9bcbe5ed feat/rename collectionVariables variable name to runtimeVariables (#2638)
* pr review changes

* collection root object in export json

* import environment updates

* tests run execution order fix for collection runs

* updated validations

* collectionVariables -> runtimeVariables

* removed husky, adjusted indentation

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-07-17 17:21:03 +05:30
Max Destors
e60aaf2ea9 Multipart request file - Fix init value and clear value (#2609) 2024-07-17 15:41:25 +05:30
Rinku Chaudhari
81497d8397 fix: active enviroment after rename when there is single enviroment (#2640) 2024-07-16 15:53:26 +05:30
Anoop M D
34a961967e chore: bumped version 2024-07-15 17:37:54 +05:30
Anoop M D
1b0495c7b0 Merge branch 'main' of github.com:usebruno/bruno 2024-07-15 17:37:10 +05:30
lohit
73214107c7 feat/request variables highlight (#2621)
* pr review changes

* collection root object in export json

* import environment updates

* tests run execution order fix for collection runs

* updated validations

* folder/request pre-vars green/red color highlight

* collection vars > request vars

* chore: removed unused logic

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-07-15 17:18:29 +05:30
Anoop M D
f159f73340 chore: bumped version 2024-07-10 16:28:58 +05:30
lohit
c2e6dee2da validations update for export collection (#2607)
* pr review changes

* collection root object in export json

* import environment updates

* tests run execution order fix for collection runs

* updated validations
2024-07-10 16:26:19 +05:30
Fabio GRANDE
f64dca16a7 Removed Underscores on variables (#2603)
Co-authored-by: Fabio Grande <fabio.grande@hdhome.it>
2024-07-10 12:58:19 +05:30
Anoop M D
b5b9e547c9 chore: bumped version 2024-07-10 12:57:05 +05:30
Anoop M D
1239baf687 chore: added graceful check while accessing path params 2024-07-10 11:55:12 +05:30
Fabio GRANDE
b2038c7cc2 CLI doesn't interpolate params on the URL #2587 (#2588)
Co-authored-by: Fabio Grande <fabio.grande@hdhome.it>
2024-07-10 11:52:18 +05:30
Fabio GRANDE
9f76834b2f Bruno CLI tries to execute folder.bru #2584 (#2585)
Co-authored-by: Fabio Grande <fabio.grande@hdhome.it>
2024-07-10 11:49:12 +05:30
Jorge Caridad
8094149fbe fixed typos in bruno-cli readme.md (#2600) 2024-07-10 11:29:23 +05:30
Fabio GRANDE
240d2d03f7 Cloning a dir now clone also folder.bru files recursively#2593 (#2596)
Co-authored-by: Fabio Grande <fabio.grande@hdhome.it>
2024-07-10 11:24:49 +05:30
lohit
58c8085a64 fix/collection export import (#2601)
* pr review changes

* collection root object in export json

* import environment updates

* tests run execution order fix for collection runs

* headers schema update, export only required parts of request

* update auth in object spread

* docs not present in folder level settings

* docs not present in folder level settings

---------

Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-07-10 11:21:58 +05:30
sfreeman422
e5425299a2 Import Bruno Collection fails - ValidationError: headers are required #2583 (#2598)
* Removed headers key on folderRootSchema as it seems unlikely that headers are actually required here

* Removed launch.json

* Added back package-locks

* reverted package-lock

* removed only the .required
2024-07-10 11:19:12 +05:30
lohit
f1e0b112ae fix/folder bru data loading issue in windows (#2595)
* pr review changes

* collection root object in export json

* import environment updates

* validations for folder.bru paths for windows
2024-07-10 11:04:55 +05:30
193 changed files with 10211 additions and 21130 deletions

View File

@@ -15,6 +15,8 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: './package-lock.json'
- name: Install dependencies
run: npm ci --legacy-peer-deps
@@ -23,8 +25,14 @@ jobs:
run: |
npm run build --workspace=packages/bruno-common
npm run build --workspace=packages/bruno-query
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
# tests
- name: Test Package bruno-js
run: npm run test --workspace=packages/bruno-js
- name: Test Package bruno-cli
run: npm run test --workspace=packages/bruno-cli
# test
- name: Test Package bruno-query
run: npm run test --workspace=packages/bruno-query
- name: Test Package bruno-lang
@@ -33,12 +41,8 @@ jobs:
run: npm run test --workspace=packages/bruno-schema
- name: Test Package bruno-app
run: npm run test --workspace=packages/bruno-app
- name: Test Package bruno-js
run: npm run test --workspace=packages/bruno-js
- name: Test Package bruno-common
run: npm run test --workspace=packages/bruno-common
- name: Test Package bruno-cli
run: npm run test --workspace=packages/bruno-cli
- name: Test Package bruno-electron
run: npm run test --workspace=packages/bruno-electron
@@ -50,6 +54,8 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: './package-lock.json'
- name: Install dependencies
run: npm ci --legacy-peer-deps
@@ -58,6 +64,7 @@ jobs:
run: |
npm run build --workspace=packages/bruno-query
npm run build --workspace=packages/bruno-common
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
- name: Run tests
run: |
@@ -71,15 +78,3 @@ jobs:
with:
files: packages/bruno-tests/collection/junit.xml
comment_mode: always
prettier:
name: Prettier
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Install dependencies
run: npm ci --legacy-peer-deps
- name: Run Prettier
run: npm run test:prettier:web

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx pretty-quick --staged

View File

@@ -57,6 +57,9 @@ npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
# bundle js sandbox libraries
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
# run next app (terminal 1)
npm run dev:web

View File

@@ -62,6 +62,9 @@ npm run build:graphql-docs
# construction de bruno query
npm run build:bruno-query
# construction de bruno common
npm run build:bruno-common
# démarrage de next (terminal 1)
npm run dev:web

View File

@@ -0,0 +1,84 @@
## Urobme bruno lepším, spoločne !!
Sme radi, že chcete zlepšiť bruno. Nižšie sú uvedené pokyny, ako začať s výchovou bruno na vašom počítači.
### Technologický zásobník
Bruno je vytvorené pomocou Next.js a React. Na dodávanie desktopovej verzie (ktorá podporuje lokálne kolekcie) používame aj electron.
Balíčky, ktoré používame:
- CSS - Tailwind
- Editory kódu - Codemirror
- Správa stavu - Redux
- Ikony - Tabler Icons
- Formuláre - formik
- Overovanie schém - Yup
- Klient požiadaviek - axios
- Sledovač súborového systému - chokidar
### Závislosti
Budete potrebovať [NodeJS v18.x alebo najnovšiu verziu LTS](https://nodejs.org/en/) a npm versiu 8.x. V projekte používame pracovné priestory npm
## Vývoj
Bruno sa vyvíja ako desktopová aplikácia. Aplikáciu je potrebné načítať spustením aplikácie Next.js v jednom termináli a potom spustiť aplikáciu electron v inom termináli.
### Závislosti
- NodeJS v18
### Miestny vývoj
```bash
# použite verziu nodejs 18
nvm use
# nainštalovať balíčky
npm i --legacy-peer-deps
# zostaviť balíčky
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
# spustite ďalšiu aplikáciu (terminál 1)
npm run dev:web
# spustite aplikáciu electron (terminál 2)
npm run dev:electron
```
### Riešenie problémov
Pri spustení `npm install` sa môžete stretnúť s chybou `Unsupported platform`. Ak chcete túto chybu odstrániť, musíte odstrániť súbory `node_modules`, `package-lock.json` a spustiť `npm install`. Tým by sa mali nainštalovať všetky potrebné balíky potrebné na spustenie aplikácie.
```shell
# Odstrániť node_modules v podadresároch
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# Odstráňte package-lock v podadresároch
find . -type f -name "package-lock.json" -delete
```
### Testovanie
````bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Vyrobenie Pull Request
- Prosím, aby PR boli malé a zamerané na jednu vec
- Prosím, dodržujte formát vytvárania vetiev
- feature/[názov funkcie]: Táto vetva by mala obsahovať zmeny pre konkrétnu funkciu
- Príklad: feature/dark-mode
- bugfix/[názov chyby]: Táto vetva by mala obsahovať iba opravy konkrétnej chyby
- Príklad: bugfix/bug-1

View File

@@ -103,6 +103,12 @@ Ou qualquer sistema de controle de versão de sua escolha.
<img src="../../assets/images/sponsors/commit-company.png" width="70"/>
#### Apoiadores Bronze
<a href="https://zuplo.link/bruno">
<img src="../../assets/images/sponsors/zuplo.png" width="120"/>
</a>
### Links Importantes 📌
- [Nossa Visão de Longo Prazo](https://github.com/usebruno/bruno/discussions/269)

26812
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -47,7 +47,6 @@
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
"prepare": "husky install"
},
"overrides": {
"rollup": "3.2.5"
},

View File

@@ -6,6 +6,7 @@ const StyledWrapper = styled.div`
border: solid 1px ${(props) => props.theme.codemirror.border};
font-family: ${(props) => (props.font ? props.font : 'default')};
line-break: anywhere;
flex: 1 1 0;
}
.CodeMirror-overlayscroll-horizontal div,

View File

@@ -16,6 +16,7 @@ import stripJsonComments from 'strip-json-comments';
let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
const TAB_SIZE = 2;
if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
@@ -121,7 +122,7 @@ export default class CodeEditor extends React.Component {
value: this.props.value || '',
lineNumbers: true,
lineWrapping: true,
tabSize: 2,
tabSize: TAB_SIZE,
mode: this.props.mode || 'application/ld+json',
keyMap: 'sublime',
autoCloseBrackets: true,
@@ -169,7 +170,33 @@ export default class CodeEditor extends React.Component {
'Ctrl-Y': 'foldAll',
'Cmd-Y': 'foldAll',
'Ctrl-I': 'unfoldAll',
'Cmd-I': 'unfoldAll'
'Cmd-I': 'unfoldAll',
'Cmd-/': (cm) => {
// comment/uncomment every selected line(s)
const selections = cm.listSelections();
selections.forEach((range) => {
for (let i = range.from().line; i <= range.to().line; i++) {
const selectedLine = cm.getLine(i);
// if commented line, remove comment
if (selectedLine.trim().startsWith('//')) {
cm.replaceRange(
selectedLine.replace(/^(\s*)\/\/\s?/, '$1'),
{ line: i, ch: 0 },
{ line: i, ch: selectedLine.length }
);
continue;
}
// otherwise add comment
cm.replaceRange(
selectedLine.search(/\S|$/) >= TAB_SIZE
? ' '.repeat(TAB_SIZE) + '// ' + selectedLine.trim()
: '// ' + selectedLine,
{ line: i, ch: 0 },
{ line: i, ch: selectedLine.length }
);
}
});
}
},
foldOptions: {
widget: (from, to) => {
@@ -289,7 +316,7 @@ export default class CodeEditor extends React.Component {
}
return (
<StyledWrapper
className="h-full w-full"
className="h-full w-full flex flex-col relative"
aria-label="Code Editor"
font={this.props.font}
ref={(node) => {

View File

@@ -138,6 +138,7 @@ const AwsV4Auth = ({ collection }) => {
onSave={handleSave}
onChange={(val) => handleSecretAccessKeyChange(val)}
collection={collection}
isSecret={true}
/>
</div>

View File

@@ -62,6 +62,7 @@ const BasicAuth = ({ collection }) => {
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
collection={collection}
isSecret={true}
/>
</div>
</StyledWrapper>

View File

@@ -37,6 +37,7 @@ const BearerAuth = ({ collection }) => {
onSave={handleSave}
onChange={(val) => handleTokenChange(val)}
collection={collection}
isSecret={true}
/>
</div>
</StyledWrapper>

View File

@@ -62,6 +62,7 @@ const DigestAuth = ({ collection }) => {
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
collection={collection}
isSecret={true}
/>
</div>
</StyledWrapper>

View File

@@ -78,7 +78,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label } = input;
const { key, label, isSecret } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
@@ -90,6 +90,7 @@ const OAuth2AuthorizationCode = ({ collection }) => {
onChange={(val) => handleChange(key, val)}
onRun={handleRun}
collection={collection}
isSecret={isSecret}
/>
</div>
</div>

View File

@@ -17,7 +17,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
label: 'Client Secret'
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',

View File

@@ -42,7 +42,7 @@ const OAuth2ClientCredentials = ({ collection }) => {
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label } = input;
const { key, label, isSecret } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
@@ -54,6 +54,7 @@ const OAuth2ClientCredentials = ({ collection }) => {
onChange={(val) => handleChange(key, val)}
onRun={handleRun}
collection={collection}
isSecret={isSecret}
/>
</div>
</div>

View File

@@ -9,7 +9,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
label: 'Client Secret'
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',

View File

@@ -44,7 +44,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label } = input;
const { key, label, isSecret } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
@@ -56,6 +56,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
onChange={(val) => handleChange(key, val)}
onRun={handleRun}
collection={collection}
isSecret={isSecret}
/>
</div>
</div>

View File

@@ -17,7 +17,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
label: 'Client Secret'
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',

View File

@@ -74,6 +74,7 @@ const PresetsSettings = ({ collection }) => {
id="request-url"
type="text"
name="requestUrl"
placeholder='Request URL'
className="block textbox"
autoComplete="off"
autoCorrect="off"

View File

@@ -1,14 +1,6 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
/* todo: find a better way */
height: calc(100vh - 240px);
.CodeMirror-scroll {
padding-bottom: 0px;
}
}
.editing-mode {
cursor: pointer;
color: ${(props) => props.theme.colors.text.yellow};

View File

@@ -37,8 +37,8 @@ const Documentation = ({ item, collection }) => {
}
return (
<StyledWrapper className="mt-1 h-full w-full relative">
<div className="editing-mode mb-2" role="tab" onClick={toggleViewMode}>
<StyledWrapper className="flex flex-col gap-y-1 h-full w-full relative">
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
{isEditing ? 'Preview' : 'Edit'}
</div>

View File

@@ -40,10 +40,15 @@ const Wrapper = styled.div`
color: ${(props) => props.theme.dropdown.iconColor};
}
&:hover {
&:hover:not(:disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg};
}
&:disabled {
cursor: not-allowed;
color: gray;
}
&.border-top {
border-top: solid 1px ${(props) => props.theme.dropdown.separator};
}

View File

@@ -5,7 +5,6 @@ import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { uuid } from 'utils/common';
import { maskInputValue } from 'utils/collections';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { variableNameRegex } from 'utils/common/regex';
@@ -96,10 +95,10 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
<table>
<thead>
<tr>
<td>Enabled</td>
<td className="text-center">Enabled</td>
<td>Name</td>
<td>Value</td>
<td>Secret</td>
<td className="text-center">Secret</td>
<td></td>
</tr>
</thead>
@@ -109,7 +108,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
<td className="text-center">
<input
type="checkbox"
className="mr-3 mousetrap"
className="mousetrap"
name={`${index}.enabled`}
checked={variable.enabled}
onChange={formik.handleChange}
@@ -130,23 +129,22 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
/>
<ErrorMessage name={`${index}.name`} />
</td>
<td>
{variable.secret ? (
<div className="overflow-hidden text-ellipsis">{maskInputValue(variable.value)}</div>
) : (
<td className="flex flex-row flex-nowrap">
<div className="overflow-hidden grow w-full relative">
<SingleLineEditor
theme={storedTheme}
collection={collection}
name={`${index}.value`}
value={variable.value}
isSecret={variable.secret}
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
/>
)}
</div>
</td>
<td>
<td className="text-center">
<input
type="checkbox"
className="mr-3 mousetrap"
className="mousetrap"
name={`${index}.secret`}
checked={variable.secret}
onChange={formik.handleChange}

View File

@@ -42,7 +42,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
};
const clear = () => {
onChange('');
onChange([]);
};
const renderButtonText = (filenames) => {

View File

@@ -44,8 +44,8 @@ const Script = ({ collection, folder }) => {
<div className="text-xs mb-4 text-muted">
Pre and post-request scripts that will run before and after any request inside this folder is sent.
</div>
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Pre Request</div>
<div className="flex flex-col flex-1 mt-2 gap-y-2">
<div className="title text-xs">Pre Request</div>
<CodeEditor
collection={collection}
value={requestScript || ''}
@@ -56,8 +56,8 @@ const Script = ({ collection, folder }) => {
font={get(preferences, 'font.codeFont', 'default')}
/>
</div>
<div className="flex-1 mt-6">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<div className="flex flex-col flex-1 mt-2 gap-y-2">
<div className="title text-xs">Post Response</div>
<CodeEditor
collection={collection}
value={responseScript || ''}

View File

@@ -50,7 +50,7 @@ const FolderSettings = ({ collection, folder }) => {
};
return (
<StyledWrapper>
<StyledWrapper className="flex flex-col h-full">
<div className="flex flex-col h-full relative px-4 py-4">
<div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>

View File

@@ -2,7 +2,6 @@ import styled from 'styled-components';
const StyledMarkdownBodyWrapper = styled.div`
background: transparent;
height: inherit;
.markdown-body {
background: transparent;
overflow-y: auto;

View File

@@ -1,10 +1,10 @@
import React, { useEffect, useState } from 'react';
import StyledWrapper from './StyledWrapper';
const ModalHeader = ({ title, handleCancel, customHeader }) => (
const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
<div className="bruno-modal-header">
{customHeader ? customHeader : <>{title ? <div className="bruno-modal-header-title">{title}</div> : null}</>}
{handleCancel ? (
{handleCancel && !hideClose ? (
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null}>
×
</div>
@@ -63,6 +63,7 @@ const Modal = ({
confirmDisabled,
hideCancel,
hideFooter,
hideClose,
disableCloseOnOutsideClick,
disableEscapeKey,
onClick,
@@ -100,7 +101,12 @@ const Modal = ({
return (
<StyledWrapper className={classes} onClick={onClick ? (e) => onClick(e) : null}>
<div className={`bruno-modal-card modal-${size}`}>
<ModalHeader title={title} handleCancel={() => closeModal({ type: 'icon' })} customHeader={customHeader} />
<ModalHeader
title={title}
hideClose={hideClose}
handleCancel={() => closeModal({ type: 'icon' })}
customHeader={customHeader}
/>
<ModalContent>{children}</ModalContent>
<ModalFooter
confirmText={confirmText}

View File

@@ -24,13 +24,15 @@ class MultiLineEditor extends Component {
componentDidMount() {
// Initialize CodeMirror as a single line editor
/** @type {import("codemirror").Editor} */
const variables = getAllVariables(this.props.collection, this.props.item);
this.editor = CodeMirror(this.editorRef.current, {
lineWrapping: false,
lineNumbers: false,
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
mode: 'brunovariables',
brunoVarInfo: {
variables: getAllVariables(this.props.collection)
variables
},
scrollbarStyle: null,
tabindex: 0,
@@ -85,7 +87,7 @@ class MultiLineEditor extends Component {
}
this.editor.setValue(String(this.props.value) || '');
this.editor.on('change', this._onEdit);
this.addOverlay();
this.addOverlay(variables);
}
_onEdit = () => {
@@ -103,10 +105,10 @@ class MultiLineEditor extends Component {
// event loop.
this.ignoreChangeEvent = true;
let variables = getAllVariables(this.props.collection);
let variables = getAllVariables(this.props.collection, this.props.item);
if (!isEqual(variables, this.variables)) {
this.editor.options.brunoVarInfo.variables = variables;
this.addOverlay();
this.addOverlay(variables);
}
if (this.props.theme !== prevProps.theme && this.editor) {
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
@@ -125,10 +127,8 @@ class MultiLineEditor extends Component {
this.editor.getWrapperElement().remove();
}
addOverlay = () => {
let variables = getAllVariables(this.props.collection);
addOverlay = (variables) => {
this.variables = variables;
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
this.editor.setOption('mode', 'brunovariables');
};

View File

@@ -1,39 +1,42 @@
import React from 'react';
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import { useDictionary } from 'providers/Dictionary/index';
const Support = () => {
const { dictionary } = useDictionary();
return (
<StyledWrapper>
<div className="rows">
<div className="mt-2">
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end">
<IconBook size={18} strokeWidth={2} />
<span className="label ml-2">Documentation</span>
<span className="label ml-2">{dictionary.documentation}</span>
</a>
</div>
<div className="mt-2">
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
<IconSpeakerphone size={18} strokeWidth={2} />
<span className="label ml-2">Report Issues</span>
<span className="label ml-2">{dictionary.reportIssues}</span>
</a>
</div>
<div className="mt-2">
<a href="https://discord.com/invite/KgcZUncpjq" target="_blank" className="flex items-end">
<IconBrandDiscord size={18} strokeWidth={2} />
<span className="label ml-2">Discord</span>
<span className="label ml-2">{dictionary.discord}</span>
</a>
</div>
<div className="mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end">
<IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">GitHub</span>
<span className="label ml-2">{dictionary.gitHub}</span>
</a>
</div>
<div className="mt-2">
<a href="https://twitter.com/use_bruno" target="_blank" className="flex items-end">
<IconBrandTwitter size={18} strokeWidth={2} />
<span className="label ml-2">Twitter</span>
<span className="label ml-2">{dictionary.twitter}</span>
</a>
</div>
</div>

View File

@@ -1,7 +1,4 @@
import React from 'react';
import { useTheme } from 'providers/Theme/index';
import darkTheme from 'themes/dark';
import lightTheme from 'themes/light';
/**
* Assertion operators
@@ -81,16 +78,10 @@ const AssertionOperator = ({ operator, onChange }) => {
}
};
const { storedTheme } = useTheme();
return (
<select value={operator} onChange={handleChange} className="mousetrap">
{operators.map((operator) => (
<option
style={{ backgroundColor: storedTheme === 'dark' ? darkTheme.bg : lightTheme.bg }}
key={operator}
value={operator}
>
<option key={operator} value={operator}>
{getLabel(operator)}
</option>
))}

View File

@@ -55,6 +55,9 @@ const Wrapper = styled.div`
position: relative;
top: 1px;
}
option {
background-color: ${(props) => props.theme.bg};
}
`;
export default Wrapper;

View File

@@ -136,6 +136,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onChange={(val) => handleAccessKeyIdChange(val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
@@ -148,6 +149,8 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onChange={(val) => handleSecretAccessKeyChange(val)}
onRun={handleRun}
collection={collection}
item={item}
isSecret={true}
/>
</div>
@@ -160,6 +163,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onChange={(val) => handleSessionTokenChange(val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
@@ -172,6 +176,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onChange={(val) => handleServiceChange(val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
@@ -184,6 +189,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onChange={(val) => handleRegionChange(val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
@@ -196,6 +202,7 @@ const AwsV4Auth = ({ onTokenChange, item, collection }) => {
onChange={(val) => handleProfileNameChange(val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
</StyledWrapper>

View File

@@ -55,6 +55,7 @@ const BasicAuth = ({ item, collection }) => {
onChange={(val) => handleUsernameChange(val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
@@ -67,6 +68,8 @@ const BasicAuth = ({ item, collection }) => {
onChange={(val) => handlePasswordChange(val)}
onRun={handleRun}
collection={collection}
item={item}
isSecret={true}
/>
</div>
</StyledWrapper>

View File

@@ -42,6 +42,8 @@ const BearerAuth = ({ item, collection }) => {
onChange={(val) => handleTokenChange(val)}
onRun={handleRun}
collection={collection}
item={item}
isSecret={true}
/>
</div>
</StyledWrapper>

View File

@@ -55,6 +55,7 @@ const DigestAuth = ({ item, collection }) => {
onChange={(val) => handleUsernameChange(val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
@@ -67,6 +68,8 @@ const DigestAuth = ({ item, collection }) => {
onChange={(val) => handlePasswordChange(val)}
onRun={handleRun}
collection={collection}
item={item}
isSecret={true}
/>
</div>
</StyledWrapper>

View File

@@ -80,7 +80,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label } = input;
const { key, label, isSecret } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
@@ -92,6 +92,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
onChange={(val) => handleChange(key, val)}
onRun={handleRun}
collection={collection}
item={item}
isSecret={isSecret}
/>
</div>
</div>

View File

@@ -17,7 +17,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
label: 'Client Secret'
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',

View File

@@ -43,7 +43,7 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label } = input;
const { key, label, isSecret } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
@@ -55,6 +55,8 @@ const OAuth2ClientCredentials = ({ item, collection }) => {
onChange={(val) => handleChange(key, val)}
onRun={handleRun}
collection={collection}
item={item}
isSecret={isSecret}
/>
</div>
</div>

View File

@@ -9,7 +9,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
label: 'Client Secret'
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',

View File

@@ -45,7 +45,7 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
return (
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
{inputsConfig.map((input) => {
const { key, label } = input;
const { key, label, isSecret } = input;
return (
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
<label className="block font-medium">{label}</label>
@@ -57,6 +57,8 @@ const OAuth2AuthorizationCode = ({ item, collection }) => {
onChange={(val) => handleChange(key, val)}
onRun={handleRun}
collection={collection}
item={item}
isSecret={isSecret}
/>
</div>
</div>

View File

@@ -9,7 +9,8 @@ const inputsConfig = [
},
{
key: 'password',
label: 'Password'
label: 'Password',
isSecret: true
},
{
key: 'clientId',
@@ -17,7 +18,8 @@ const inputsConfig = [
},
{
key: 'clientSecret',
label: 'Client Secret'
label: 'Client Secret',
isSecret: true
},
{
key: 'scope',

View File

@@ -110,6 +110,7 @@ const FormUrlEncodedParams = ({ item, collection }) => {
allowNewlines={true}
onRun={handleRun}
collection={collection}
item={item}
/>
</td>
<td>

View File

@@ -16,6 +16,8 @@ import StyledWrapper from './StyledWrapper';
import { find, get } from 'lodash';
import Documentation from 'components/Documentation/index';
const CONTENT_INDICATOR = '\u25CF';
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
@@ -82,12 +84,17 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const isMultipleContentTab = ['params', 'script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab);
// get the length of active params, headers, asserts and vars
const params = item.draft ? get(item, 'draft.request.params', []) : get(item, 'request.params', []);
const headers = item.draft ? get(item, 'draft.request.headers', []) : get(item, 'request.headers', []);
const assertions = item.draft ? get(item, 'draft.request.assertions', []) : get(item, 'request.assertions', []);
const requestVars = item.draft ? get(item, 'draft.request.vars.req', []) : get(item, 'request.vars.req', []);
const responseVars = item.draft ? get(item, 'draft.request.vars.res', []) : get(item, 'request.vars.res', []);
// get the length of active params, headers, asserts and vars as well as the contents of the body, tests and script
const getPropertyFromDraftOrRequest = (propertyKey) =>
item.draft ? get(item, `draft.${propertyKey}`, []) : get(item, propertyKey, []);
const params = getPropertyFromDraftOrRequest('request.params');
const body = getPropertyFromDraftOrRequest('request.body');
const headers = getPropertyFromDraftOrRequest('request.headers');
const script = getPropertyFromDraftOrRequest('request.script');
const assertions = getPropertyFromDraftOrRequest('request.assertions');
const tests = getPropertyFromDraftOrRequest('request.tests');
const requestVars = getPropertyFromDraftOrRequest('request.vars.req');
const responseVars = getPropertyFromDraftOrRequest('request.vars.res');
const activeParamsLength = params.filter((param) => param.enabled).length;
const activeHeadersLength = headers.filter((header) => header.enabled).length;
@@ -105,6 +112,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
</div>
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
Body
{body.mode !== 'none' && <sup className="ml-1 font-medium">{CONTENT_INDICATOR}</sup>}
</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers
@@ -119,6 +127,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
Script
{(script.req || script.res) && <sup className="ml-1 font-medium">{CONTENT_INDICATOR}</sup>}
</div>
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
Assert
@@ -126,6 +135,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
</div>
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
Tests
{tests && <sup className="ml-1 font-medium">{CONTENT_INDICATOR}</sup>}
</div>
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
Docs
@@ -137,7 +147,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
) : null}
</div>
<section
className={classnames('flex w-full', {
className={classnames('flex w-full flex-1', {
'mt-5': !isMultipleContentTab
})}
>

View File

@@ -24,7 +24,8 @@ const MultipartFormParams = ({ item, collection }) => {
addMultipartFormParam({
itemUid: item.uid,
collectionUid: collection.uid,
type: 'text'
type: 'text',
value: ''
})
);
};
@@ -34,7 +35,8 @@ const MultipartFormParams = ({ item, collection }) => {
addMultipartFormParam({
itemUid: item.uid,
collectionUid: collection.uid,
type: 'file'
type: 'file',
value: []
})
);
};
@@ -139,6 +141,7 @@ const MultipartFormParams = ({ item, collection }) => {
onRun={handleRun}
allowNewlines={true}
collection={collection}
item={item}
/>
)}
</td>

View File

@@ -4,6 +4,7 @@ const StyledWrapper = styled.div`
div.CodeMirror {
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
flex: 1 1 0;
}
textarea.cm-editor {

View File

@@ -147,6 +147,7 @@ const QueryParams = ({ item, collection }) => {
}
onRun={handleRun}
collection={collection}
item={item}
/>
</td>
<td>
@@ -214,6 +215,7 @@ const QueryParams = ({ item, collection }) => {
}
onRun={handleRun}
collection={collection}
item={item}
/>
</td>
</tr>

View File

@@ -74,7 +74,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
/>
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
<div
className="tooltip mr-3"
className="tooltip mx-3"
onClick={(e) => {
e.stopPropagation();
if (!item.draft) return;

View File

@@ -1,10 +1,6 @@
import styled from 'styled-components';
const Wrapper = styled.div`
div.CodeMirror {
/* todo: find a better way */
height: calc(100vh - 220px);
}
`;
export default Wrapper;

View File

@@ -119,6 +119,7 @@ const RequestHeaders = ({ item, collection }) => {
autocomplete={MimeTypes}
allowNewlines={true}
collection={collection}
item={item}
/>
</td>
<td>

View File

@@ -40,8 +40,8 @@ const Script = ({ item, collection }) => {
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Pre Request</div>
<div className="flex flex-col flex-1 mt-2 gap-y-2">
<div className="title text-xs">Pre Request</div>
<CodeEditor
collection={collection}
value={requestScript || ''}
@@ -53,8 +53,8 @@ const Script = ({ item, collection }) => {
onSave={onSave}
/>
</div>
<div className="flex-1 mt-6">
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<div className="flex flex-col flex-1 mt-2 gap-y-2">
<div className="title text-xs">Post Response</div>
<CodeEditor
collection={collection}
value={responseScript || ''}

View File

@@ -9,11 +9,11 @@ const Vars = ({ item, collection }) => {
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">
<div className="mt-2">
<div className="mb-1 title text-xs">Pre Request</div>
<VarsTable item={item} collection={collection} vars={requestVars} varType="request" />
</div>
<div className="flex-1">
<div>
<div className="mt-1 mb-1 title text-xs">Post Response</div>
<VarsTable item={item} collection={collection} vars={responseVars} varType="response" />
</div>

View File

@@ -30,7 +30,7 @@ const RequestNotFound = ({ itemUid }) => {
return (
<div className="mt-6 px-6">
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700 bg-yellow-100 p-4">
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700">
<div>Request no longer exists.</div>
<div className="mt-2">
This can happen when the .bru file associated with this request was deleted on your filesystem.

View File

@@ -18,6 +18,7 @@ import CollectionSettings from 'components/CollectionSettings';
import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper';
import SecuritySettings from 'components/SecuritySettings';
import FolderSettings from 'components/FolderSettings';
const MIN_LEFT_PANE_WIDTH = 300;
@@ -137,6 +138,10 @@ const RequestTabPanel = () => {
return <FolderSettings collection={collection} folder={folder} />;
}
if (focusedTab.type === 'security-settings') {
return <SecuritySettings collection={collection} />;
}
const item = findItemInCollection(collection, activeTabUid);
if (!item || !item.uid) {
return <RequestNotFound itemUid={activeTabUid} />;
@@ -158,10 +163,9 @@ const RequestTabPanel = () => {
<section className="main flex flex-grow pb-4 relative">
<section className="request-pane">
<div
className="px-4"
className="px-4 h-full"
style={{
width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`,
height: `calc(100% - ${DEFAULT_PADDING}px)`
width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`
}}
>
{item.type === 'graphql-request' ? (

View File

@@ -5,6 +5,7 @@ import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
import JsSandboxMode from 'components/SecuritySettings/JsSandboxMode';
const CollectionToolBar = ({ collection }) => {
const dispatch = useDispatch();
@@ -47,6 +48,9 @@ const CollectionToolBar = ({ collection }) => {
<span className="ml-2 mr-4 font-semibold">{collection?.name}</span>
</div>
<div className="flex flex-1 items-center justify-end">
<span className="mr-2">
<JsSandboxMode collection={collection} />
</span>
<span className="mr-2">
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
</span>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { IconVariable, IconSettings, IconRun, IconFolder } from '@tabler/icons';
import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
const SpecialTab = ({ handleCloseClick, type, tabName }) => {
const getTabInfo = (type, tabName) => {
@@ -12,6 +12,14 @@ const SpecialTab = ({ handleCloseClick, type, tabName }) => {
</>
);
}
case 'security-settings': {
return (
<>
<IconShieldLock size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Security</span>
</>
)
}
case 'folder-settings': {
return (
<div className="flex items-center flex-nowrap overflow-hidden">

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useRef, Fragment } from 'react';
import get from 'lodash/get';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
@@ -12,12 +12,18 @@ import ConfirmRequestClose from './ConfirmRequestClose';
import RequestTabNotFound from './RequestTabNotFound';
import SpecialTab from './SpecialTab';
import StyledWrapper from './StyledWrapper';
import Dropdown from 'components/Dropdown';
import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
import NewRequest from 'components/Sidebar/NewRequest/index';
const RequestTab = ({ tab, collection, folderUid }) => {
const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const [showConfirmClose, setShowConfirmClose] = useState(false);
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const handleCloseClick = (event) => {
event.stopPropagation();
event.preventDefault();
@@ -28,6 +34,19 @@ const RequestTab = ({ tab, collection, folderUid }) => {
);
};
const handleRightClick = (_event) => {
const menuDropdown = dropdownTippyRef.current;
if (!menuDropdown) {
return;
}
if (menuDropdown.state.isShown) {
menuDropdown.hide();
} else {
menuDropdown.show();
}
};
const handleMouseUp = (e) => {
if (e.button === 1) {
e.stopPropagation();
@@ -43,45 +62,11 @@ const RequestTab = ({ tab, collection, folderUid }) => {
const getMethodColor = (method = '') => {
const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
let color = '';
method = method.toLocaleLowerCase();
switch (method) {
case 'get': {
color = theme.request.methods.get;
break;
}
case 'post': {
color = theme.request.methods.post;
break;
}
case 'put': {
color = theme.request.methods.put;
break;
}
case 'delete': {
color = theme.request.methods.delete;
break;
}
case 'patch': {
color = theme.request.methods.patch;
break;
}
case 'options': {
color = theme.request.methods.options;
break;
}
case 'head': {
color = theme.request.methods.head;
break;
}
}
return color;
return theme.request.methods[method.toLocaleLowerCase()];
};
const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner'].includes(tab.type)) {
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
return (
<StyledWrapper className="flex items-center justify-between tab-container px-1">
{tab.type === 'folder-settings' ? (
@@ -143,6 +128,7 @@ const RequestTab = ({ tab, collection, folderUid }) => {
)}
<div
className="flex items-baseline tab-label pl-2"
onContextMenu={handleRightClick}
onMouseUp={(e) => {
if (!item.draft) return handleMouseUp(e);
@@ -159,6 +145,15 @@ const RequestTab = ({ tab, collection, folderUid }) => {
<span className="ml-1 tab-name" title={item.name}>
{item.name}
</span>
<RequestTabMenu
onDropdownCreate={onDropdownCreate}
tabIndex={tabIndex}
collectionRequestTabs={collectionRequestTabs}
tabItem={item}
collection={collection}
dropdownTippyRef={dropdownTippyRef}
dispatch={dispatch}
/>
</div>
<div
className="flex px-2 close-icon-container"
@@ -195,4 +190,124 @@ const RequestTab = ({ tab, collection, folderUid }) => {
);
};
function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, collection, dropdownTippyRef, dispatch }) {
const [showCloneRequestModal, setShowCloneRequestModal] = useState(false);
const [showAddNewRequestModal, setShowAddNewRequestModal] = useState(false);
const totalTabs = collectionRequestTabs.length || 0;
const currentTabUid = collectionRequestTabs[tabIndex]?.uid;
const currentTabItem = findItemInCollection(collection, currentTabUid);
const hasLeftTabs = tabIndex !== 0;
const hasRightTabs = totalTabs > tabIndex + 1;
const hasOtherTabs = totalTabs > 1;
async function handleCloseTab(event, tabUid) {
event.stopPropagation();
dropdownTippyRef.current.hide();
if (!tabUid) {
return;
}
try {
const item = findItemInCollection(collection, tabUid);
// silently save unsaved changes before closing the tab
if (item.draft) {
await dispatch(saveRequest(item.uid, collection.uid, true));
}
dispatch(closeTabs({ tabUids: [tabUid] }));
} catch (err) {}
}
function handleCloseOtherTabs(event) {
dropdownTippyRef.current.hide();
const otherTabs = collectionRequestTabs.filter((_, index) => index !== tabIndex);
otherTabs.forEach((tab) => handleCloseTab(event, tab.uid));
}
function handleCloseTabsToTheLeft(event) {
dropdownTippyRef.current.hide();
const leftTabs = collectionRequestTabs.filter((_, index) => index < tabIndex);
leftTabs.forEach((tab) => handleCloseTab(event, tab.uid));
}
function handleCloseTabsToTheRight(event) {
dropdownTippyRef.current.hide();
const rightTabs = collectionRequestTabs.filter((_, index) => index > tabIndex);
rightTabs.forEach((tab) => handleCloseTab(event, tab.uid));
}
function handleCloseSavedTabs(event) {
event.stopPropagation();
const savedTabs = collection.items.filter((item) => !item.draft);
const savedTabIds = savedTabs.map((item) => item.uid) || [];
dispatch(closeTabs({ tabUids: savedTabIds }));
}
function handleCloseAllTabs(event) {
collectionRequestTabs.forEach((tab) => handleCloseTab(event, tab.uid));
}
return (
<Fragment>
{showAddNewRequestModal && (
<NewRequest collection={collection} onClose={() => setShowAddNewRequestModal(false)} />
)}
{showCloneRequestModal && (
<CloneCollectionItem
item={currentTabItem}
collection={collection}
onClose={() => setShowCloneRequestModal(false)}
/>
)}
<Dropdown onCreate={onDropdownCreate} icon={<span></span>} placement="bottom-start">
<button
className="dropdown-item w-full"
onClick={() => {
dropdownTippyRef.current.hide();
setShowAddNewRequestModal(true);
}}
>
New Request
</button>
<button
className="dropdown-item w-full"
onClick={() => {
dropdownTippyRef.current.hide();
setShowCloneRequestModal(true);
}}
>
Clone Request
</button>
<button className="dropdown-item w-full" onClick={(e) => handleCloseTab(e, currentTabUid)}>
Close
</button>
<button disabled={!hasOtherTabs} className="dropdown-item w-full" onClick={handleCloseOtherTabs}>
Close Others
</button>
<button disabled={!hasLeftTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheLeft}>
Close to the Left
</button>
<button disabled={!hasRightTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheRight}>
Close to the Right
</button>
<button className="dropdown-item w-full" onClick={handleCloseSavedTabs}>
Close Saved
</button>
<button className="dropdown-item w-full" onClick={handleCloseAllTabs}>
Close All
</button>
</Dropdown>
</Fragment>
);
}
export default RequestTab;

View File

@@ -7,13 +7,14 @@ const Wrapper = styled.div`
padding: 0;
margin: 0;
display: flex;
position: relative;
overflow: scroll;
&::-webkit-scrollbar {
display: none;
}
scrollbar-width: none;
li {
display: inline-flex;
max-width: 150px;

View File

@@ -110,7 +110,14 @@ const RequestTabs = () => {
role="tab"
onClick={() => handleClick(tab)}
>
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} folderUid={tab.folderUid} />
<RequestTab
collectionRequestTabs={collectionRequestTabs}
tabIndex={index}
key={tab.uid}
tab={tab}
collection={activeCollection}
folderUid={tab.folderUid}
/>
</li>
);
})

View File

@@ -3,6 +3,7 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
position: absolute;
height: 100%;
width: calc(100% - 0.75rem);
z-index: 1;
background-color: ${(props) => props.theme.requestTabPanel.responseOverlayBg};

View File

@@ -13,7 +13,7 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
};
return (
<StyledWrapper className="px-3 w-full">
<StyledWrapper className="w-full">
<div className="overlay">
<div style={{ marginBottom: 15, fontSize: 26 }}>
<div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}>

View File

@@ -1,6 +1,19 @@
import styled from 'styled-components';
const Wrapper = styled.div`
.textbox {
border: 1px solid #ccc;
padding: 0.2rem 0.5rem;
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};
}
.item-path {
.link {
color: ${(props) => props.theme.textLink};

View File

@@ -23,6 +23,7 @@ const getRelativePath = (fullPath, pathname) => {
export default function RunnerResults({ collection }) {
const dispatch = useDispatch();
const [selectedItem, setSelectedItem] = useState(null);
const [delay, setDelay] = useState(null);
// ref for the runner output body
const runnerBodyRef = useRef();
@@ -78,11 +79,11 @@ export default function RunnerResults({ collection }) {
.filter(Boolean);
const runCollection = () => {
dispatch(runCollectionFolder(collection.uid, null, true));
dispatch(runCollectionFolder(collection.uid, null, true, Number(delay)));
};
const runAgain = () => {
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive));
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive, Number(delay)));
};
const resetRunner = () => {
@@ -116,6 +117,20 @@ export default function RunnerResults({ collection }) {
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
</div>
<div className="mt-6">
<label>Delay (in ms)</label>
<input
type="number"
className="block textbox mt-2 py-5"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={delay}
onChange={(e) => setDelay(e.target.value)}
/>
</div>
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
Run Collection
</button>
@@ -167,10 +182,14 @@ export default function RunnerResults({ collection }) {
</span>
{item.status !== 'error' && item.status !== 'completed' ? (
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
) : (
) : item.responseReceived?.status ? (
<span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}>
(<span className="mr-1">{get(item.responseReceived, 'status')}</span>
<span>{get(item.responseReceived, 'statusText')}</span>)
(<span className="mr-1">{item.responseReceived?.status}</span>
<span>{item.responseReceived?.statusText}</span>)
</span>
) : (
<span className="danger text-xs cursor-pointer" onClick={() => setSelectedItem(item)}>
(request failed)
</span>
)}
</div>

View File

@@ -0,0 +1,16 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.safe-mode {
padding: 0.15rem 0.3rem;
color: ${(props) => props.theme.colors.text.green};
border: solid 1px ${(props) => props.theme.colors.text.green} !important;
}
.developer-mode {
padding: 0.15rem 0.3rem;
color: ${(props) => props.theme.colors.text.yellow};
border: solid 1px ${(props) => props.theme.colors.text.yellow} !important;
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,45 @@
import { useDispatch } from 'react-redux';
import { IconShieldLock } from '@tabler/icons';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { uuid } from 'utils/common/index';
import JsSandboxModeModal from '../JsSandboxModeModal';
import StyledWrapper from './StyledWrapper';
const JsSandboxMode = ({ collection }) => {
const jsSandboxMode = collection?.securityConfig?.jsSandboxMode;
const dispatch = useDispatch();
const viewSecuritySettings = () => {
dispatch(
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'security-settings'
})
);
};
return (
<StyledWrapper className='flex'>
{jsSandboxMode === 'safe' && (
<div
className="flex items-center border rounded-md text-xs cursor-pointer safe-mode"
onClick={viewSecuritySettings}
>
Safe Mode
</div>
)}
{jsSandboxMode === 'developer' && (
<div
className="flex items-center border rounded-md text-xs cursor-pointer developer-mode"
onClick={viewSecuritySettings}
>
Developer Mode
</div>
)}
{!jsSandboxMode ? <JsSandboxModeModal collection={collection} /> : null}
</StyledWrapper>
);
};
export default JsSandboxMode;

View File

@@ -0,0 +1,22 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
max-width: 800px;
span.beta-tag {
display: flex;
align-items: center;
padding: 0.1rem 0.25rem;
font-size: 0.75rem;
border-radius: 0.25rem;
color: ${(props) => props.theme.colors.text.green};
border: solid 1px ${(props) => props.theme.colors.text.green} !important;
}
span.developer-mode-warning {
font-weight: 400;
color: ${(props) => props.theme.colors.text.yellow};
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,99 @@
import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import toast from 'react-hot-toast';
import { useState } from 'react';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import StyledWrapper from './StyledWrapper';
const JsSandboxModeModal = ({ collection, onClose }) => {
const dispatch = useDispatch();
const [jsSandboxMode, setJsSandboxMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe');
const handleChange = (e) => {
setJsSandboxMode(e.target.value);
};
const handleSave = () => {
dispatch(
saveCollectionSecurityConfig(collection?.uid, {
jsSandboxMode: jsSandboxMode
})
)
.then(() => {
toast.success('Sandbox mode updated successfully');
onClose();
})
.catch((err) => console.log(err) && toast.error('Failed to update sandbox mode'));
};
return (
<Portal>
<Modal
size="sm"
title={'JavaScript Sandbox'}
confirmText="Save"
handleConfirm={handleSave}
hideCancel={true}
hideClose={true}
disableCloseOnOutsideClick={true}
disableEscapeKey={true}
>
<StyledWrapper>
<div>
The collection might include JavaScript code in Variables, Scripts, Tests, and Assertions.
</div>
<div className='text-muted mt-6'>
Please choose the security level for the JavaScript code execution.
</div>
<div className="flex flex-col mt-4">
<label htmlFor="safe" className="flex flex-row items-center gap-2 cursor-pointer">
<input
type="radio"
id="safe"
name="jsSandboxMode"
value="safe"
checked={jsSandboxMode === 'safe'}
onChange={handleChange}
className="cursor-pointer"
/>
<span className={jsSandboxMode === 'safe' ? 'font-medium' : 'font-normal'}>
Safe Mode
</span>
<span className='beta-tag'>BETA</span>
</label>
<p className='text-sm text-muted mt-1'>
JavaScript code is executed in a secure sandbox and cannot excess your filesystem or execute system commands.
</p>
<label htmlFor="developer" className="flex flex-row gap-2 mt-6 cursor-pointer">
<input
type="radio"
id="developer"
name="jsSandboxMode"
value="developer"
checked={jsSandboxMode === 'developer'}
onChange={handleChange}
className="cursor-pointer"
/>
<span className={jsSandboxMode === 'developer' ? 'font-medium' : 'font-normal'}>
Developer Mode
<span className='ml-1 developer-mode-warning'>(use only if you trust the collections authors)</span>
</span>
</label>
<p className='text-sm text-muted mt-1'>
JavaScript code has access to the filesystem, can execute system commands and access sensitive information.
</p>
<small className='text-muted mt-6'>
* SAFE mode has been introduced v1.26 onwards and is in beta. Please report any issues on github.
</small>
</div>
</StyledWrapper>
</Modal>
</Portal>
);
};
export default JsSandboxModeModal;

View File

@@ -0,0 +1,22 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
max-width: 800px;
span.beta-tag {
display: flex;
align-items: center;
padding: 0.1rem 0.25rem;
font-size: 0.75rem;
border-radius: 0.25rem;
color: ${(props) => props.theme.colors.text.green};
border: solid 1px ${(props) => props.theme.colors.text.green} !important;
}
span.developer-mode-warning {
font-weight: 400;
color: ${(props) => props.theme.colors.text.yellow};
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,86 @@
import { useState } from 'react';
import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
import { useDispatch } from 'react-redux';
const SecuritySettings = ({ collection }) => {
const dispatch = useDispatch();
const [jsSandboxMode, setJsSandboxMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe');
const handleChange = (e) => {
setJsSandboxMode(e.target.value);
};
const handleSave = () => {
dispatch(
saveCollectionSecurityConfig(collection?.uid, {
jsSandboxMode: jsSandboxMode
})
)
.then(() => {
toast.success('Sandbox mode updated successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to update sandbox mode'));
};
return (
<StyledWrapper className="flex flex-col h-full relative px-4 py-4">
<div className='font-semibold mt-2'>JavaScript Sandbox</div>
<div className='mt-4'>
The collection might include JavaScript code in Variables, Scripts, Tests, and Assertions.
</div>
<div className="flex flex-col mt-4">
<div className="flex flex-col">
<label htmlFor="safe" className="flex flex-row items-center gap-2 cursor-pointer">
<input
type="radio"
id="safe"
name="jsSandboxMode"
value="safe"
checked={jsSandboxMode === 'safe'}
onChange={handleChange}
className="cursor-pointer"
/>
<span className={jsSandboxMode === 'safe' ? 'font-medium' : 'font-normal'}>
Safe Mode
</span>
<span className='beta-tag'>BETA</span>
</label>
<p className='text-sm text-muted mt-1'>
JavaScript code is executed in a secure sandbox and cannot excess your filesystem or execute system commands.
</p>
<label htmlFor="developer" className="flex flex-row gap-2 mt-6 cursor-pointer">
<input
type="radio"
id="developer"
name="jsSandboxMode"
value="developer"
checked={jsSandboxMode === 'developer'}
onChange={handleChange}
className="cursor-pointer"
/>
<span className={jsSandboxMode === 'developer' ? 'font-medium' : 'font-normal'}>
Developer Mode
<span className='ml-1 developer-mode-warning'>(use only if you trust the collections authors)</span>
</span>
</label>
<p className='text-sm text-muted mt-1'>
JavaScript code has access to the filesystem, can execute system commands and access sensitive information.
</p>
</div>
<button onClick={handleSave} className="submit btn btn-sm btn-secondary w-fit mt-6">
Save
</button>
<small className='text-muted mt-6'>
* SAFE mode has been introduced v1.26 onwards and is in beta. Please report any issues on github.
</small>
</div>
</StyledWrapper>
);
};
export default SecuritySettings;

View File

@@ -58,6 +58,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
id="collection-item-name"
type="text"
name="name"
placeholder='Enter Item name'
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"

View File

@@ -2,6 +2,7 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
position: relative;
height: 100%;
.copy-to-clipboard {
position: absolute;

View File

@@ -73,7 +73,7 @@ const GenerateCodeItem = ({ collection, item, onClose }) => {
const interpolatedUrl = interpolateUrl({
url: requestUrl,
envVars,
collectionVariables: collection.collectionVariables,
runtimeVariables: collection.runtimeVariables,
processEnvVars: collection.processEnvVariables
});

View File

@@ -91,13 +91,13 @@ const Collections = () => {
<input
type="text"
name="search"
placeholder="search"
id="search"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
className="block w-full pl-7 py-1 sm:text-sm"
placeholder="search"
value={searchText}
onChange={(e) => setSearchText(e.target.value.toLowerCase())}
/>
@@ -115,7 +115,7 @@ const Collections = () => {
)}
</div>
<div className="mt-4 flex flex-col overflow-y-auto absolute top-32 bottom-10 left-0 right-0">
<div className="mt-4 flex flex-col overflow-hidden hover:overflow-y-auto absolute top-32 bottom-10 left-0 right-0">
{collections && collections.length
? collections.map((c) => {
return (

View File

@@ -115,7 +115,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
collectionLocation: Yup.string()
.min(1, 'must be at least 1 character')
.max(500, 'must be 500 characters or less')
.required('name is required')
.required('Location is required')
}),
onSubmit: (values) => {
handleSubmit(values.collectionLocation);
@@ -124,7 +124,9 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
const browse = () => {
dispatch(browseDirectory())
.then((dirPath) => {
formik.setFieldValue('collectionLocation', dirPath);
if (typeof dirPath === 'string' && dirPath.length > 0) {
formik.setFieldValue('collectionLocation', dirPath);
}
})
.catch((error) => {
formik.setFieldValue('collectionLocation', '');
@@ -160,7 +162,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
type="text"
name="collectionLocation"
readOnly={true}
className="block textbox mt-2 w-full"
className="block textbox mt-2 w-full cursor-pointer"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"

View File

@@ -109,7 +109,8 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
collectionUid: collection.uid,
itemUid: item ? item.uid : null,
headers: request.headers,
body: request.body
body: request.body,
auth: request.auth
})
)
.then(() => onClose())
@@ -161,7 +162,16 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
return (
<StyledWrapper>
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
<form className="bruno-form" onSubmit={formik.handleSubmit}>
<form
className="bruno-form"
onSubmit={formik.handleSubmit}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
formik.handleSubmit();
}
}}
>
<div>
<label htmlFor="requestName" className="block font-semibold">
Type
@@ -220,6 +230,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
id="request-name"
type="text"
name="requestName"
placeholder="Request Name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
@@ -252,6 +263,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
id="request-url"
type="text"
name="requestUrl"
placeholder="Request URL"
className="px-3 w-full "
autoComplete="off"
autoCorrect="off"

View File

@@ -129,7 +129,7 @@ const Sidebar = () => {
Star
</GitHubButton> */}
</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.20.2</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.26.0</div>
</div>
</div>
</div>

View File

@@ -1,8 +1,9 @@
import React, { Component } from 'react';
import isEqual from 'lodash/isEqual';
import { getAllVariables } from 'utils/collections';
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
import { defineCodeMirrorBrunoVariablesMode, MaskedEditor } from 'utils/common/codemirror';
import StyledWrapper from './StyledWrapper';
import { IconEye, IconEyeOff } from '@tabler/icons';
let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
@@ -20,12 +21,28 @@ class SingleLineEditor extends Component {
this.cachedValue = props.value || '';
this.editorRef = React.createRef();
this.variables = {};
this.state = {
maskInput: props.isSecret || false // Always mask the input by default (if it's a secret)
};
}
componentDidMount() {
// Initialize CodeMirror as a single line editor
/** @type {import("codemirror").Editor} */
const variables = getAllVariables(this.props.collection, this.props.item);
const runHandler = () => {
if (this.props.onRun) {
this.props.onRun();
}
};
const saveHandler = () => {
if (this.props.onSave) {
this.props.onSave();
}
};
const noopHandler = () => {};
this.editor = CodeMirror(this.editorRef.current, {
lineWrapping: false,
lineNumbers: false,
@@ -37,21 +54,9 @@ class SingleLineEditor extends Component {
scrollbarStyle: null,
tabindex: 0,
extraKeys: {
Enter: () => {
if (this.props.onRun) {
this.props.onRun();
}
},
'Ctrl-Enter': () => {
if (this.props.onRun) {
this.props.onRun();
}
},
'Cmd-Enter': () => {
if (this.props.onRun) {
this.props.onRun();
}
},
Enter: runHandler,
'Ctrl-Enter': runHandler,
'Cmd-Enter': runHandler,
'Alt-Enter': () => {
if (this.props.allowNewlines) {
this.editor.setValue(this.editor.getValue() + '\n');
@@ -60,23 +65,11 @@ class SingleLineEditor extends Component {
this.props.onRun();
}
},
'Shift-Enter': () => {
if (this.props.onRun) {
this.props.onRun();
}
},
'Cmd-S': () => {
if (this.props.onSave) {
this.props.onSave();
}
},
'Ctrl-S': () => {
if (this.props.onSave) {
this.props.onSave();
}
},
'Cmd-F': () => {},
'Ctrl-F': () => {},
'Shift-Enter': runHandler,
'Cmd-S': saveHandler,
'Ctrl-S': saveHandler,
'Cmd-F': noopHandler,
'Ctrl-F': noopHandler,
// Tabbing disabled to make tabindex work
Tab: false,
'Shift-Tab': false
@@ -93,8 +86,24 @@ class SingleLineEditor extends Component {
this.editor.setValue(String(this.props.value) || '');
this.editor.on('change', this._onEdit);
this.addOverlay(variables);
this._enableMaskedEditor(this.props.isSecret);
this.setState({ maskInput: this.props.isSecret });
}
/** Enable or disable masking the rendered content of the editor */
_enableMaskedEditor = (enabled) => {
if (typeof enabled !== 'boolean') return;
console.log('Enabling masked editor: ' + enabled);
if (enabled == true) {
if (!this.maskedEditor) this.maskedEditor = new MaskedEditor(this.editor, '*');
this.maskedEditor.enable();
} else {
this.maskedEditor?.disable();
this.maskedEditor = null;
}
};
_onEdit = () => {
if (!this.ignoreChangeEvent && this.editor) {
this.cachedValue = this.editor.getValue();
@@ -122,6 +131,12 @@ class SingleLineEditor extends Component {
this.cachedValue = String(this.props.value);
this.editor.setValue(String(this.props.value) || '');
}
if (!isEqual(this.props.isSecret, prevProps.isSecret)) {
// If the secret flag has changed, update the editor to reflect the change
this._enableMaskedEditor(this.props.isSecret);
// also set the maskInput flag to the new value
this.setState({ maskInput: this.props.isSecret });
}
this.ignoreChangeEvent = false;
}
@@ -135,8 +150,35 @@ class SingleLineEditor extends Component {
this.editor.setOption('mode', 'brunovariables');
};
toggleVisibleSecret = () => {
const isVisible = !this.state.maskInput;
this.setState({ maskInput: isVisible });
this._enableMaskedEditor(isVisible);
};
/**
* @brief Eye icon to show/hide the secret value
* @returns ReactComponent The eye icon
*/
secretEye = (isSecret) => {
return isSecret === true ? (
<button className="mx-2" onClick={() => this.toggleVisibleSecret()}>
{this.state.maskInput === true ? (
<IconEyeOff size={18} strokeWidth={2} />
) : (
<IconEye size={18} strokeWidth={2} />
)}
</button>
) : null;
};
render() {
return <StyledWrapper ref={this.editorRef} className="single-line-editor"></StyledWrapper>;
return (
<div className="flex flex-row justify-between w-full overflow-x-auto">
<StyledWrapper ref={this.editorRef} className="single-line-editor grow" />
{this.secretEye(this.props.isSecret)}
</div>
);
}
}
export default SingleLineEditor;

View File

@@ -62,10 +62,10 @@ const EnvVariables = ({ collection, theme }) => {
);
};
const CollectionVariables = ({ collection, theme }) => {
const collectionVariablesFound = Object.keys(collection.collectionVariables).length > 0;
const RuntimeVariables = ({ collection, theme }) => {
const runtimeVariablesFound = Object.keys(collection.runtimeVariables).length > 0;
const collectionVariableArray = Object.entries(collection.collectionVariables).map(([name, value]) => ({
const runtimeVariableArray = Object.entries(collection.runtimeVariables).map(([name, value]) => ({
name,
value,
secret: false
@@ -73,11 +73,11 @@ const CollectionVariables = ({ collection, theme }) => {
return (
<>
<h1 className="font-semibold mb-2">Collection Variables</h1>
{collectionVariablesFound ? (
<KeyValueExplorer data={collectionVariableArray} theme={theme} />
<h1 className="font-semibold mb-2">Runtime Variables</h1>
{runtimeVariablesFound ? (
<KeyValueExplorer data={runtimeVariableArray} theme={theme} />
) : (
<div className="muted text-xs">No collection variables found</div>
<div className="muted text-xs">No runtime variables found</div>
)}
</>
);
@@ -90,13 +90,13 @@ const VariablesEditor = ({ collection }) => {
return (
<StyledWrapper className="px-4 py-4">
<CollectionVariables collection={collection} theme={reactInspectorTheme} />
<RuntimeVariables collection={collection} theme={reactInspectorTheme} />
<EnvVariables collection={collection} theme={reactInspectorTheme} />
<div className="mt-8 muted text-xs">
Note: As of today, collection variables can only be set via the API -{' '}
<span className="font-medium">getVar()</span> and <span className="font-medium">setVar()</span>. <br />
In the next release, we will add a UI to set and modify collection variables.
Note: As of today, runtime variables can only be set via the API - <span className="font-medium">getVar()</span>{' '}
and <span className="font-medium">setVar()</span>. <br />
In the next release, we will add a UI to set and modify runtime variables.
</div>
</StyledWrapper>
);

View File

@@ -9,9 +9,11 @@ import CreateCollection from 'components/Sidebar/CreateCollection';
import ImportCollection from 'components/Sidebar/ImportCollection';
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
import StyledWrapper from './StyledWrapper';
import { useDictionary } from 'providers/Dictionary/index';
const Welcome = () => {
const dispatch = useDispatch();
const { dictionary } = useDictionary();
const [importedCollection, setImportedCollection] = useState(null);
const [importedTranslationLog, setImportedTranslationLog] = useState({});
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
@@ -20,7 +22,7 @@ const Welcome = () => {
const handleOpenCollection = () => {
dispatch(openCollection()).catch(
(err) => console.log(err) && toast.error('An error occurred while opening the collection')
(err) => console.log(err) && toast.error(dictionary.errorWhileOpeningCollection)
);
};
@@ -38,12 +40,12 @@ const Welcome = () => {
.then(() => {
setImportCollectionLocationModalOpen(false);
setImportedCollection(null);
toast.success('Collection imported successfully');
toast.success(dictionary.collectionImportedSuccessfully);
})
.catch((err) => {
setImportCollectionLocationModalOpen(false);
console.error(err);
toast.error('An error occurred while importing the collection. Check the logs for more information.');
toast.error(dictionary.errorWhileImportingCollection);
});
};
@@ -66,46 +68,45 @@ const Welcome = () => {
<Bruno width={50} />
</div>
<div className="text-xl font-semibold select-none">bruno</div>
<div className="mt-4">Opensource IDE for exploring and testing APIs</div>
<div className="mt-4">{dictionary.aboutBruno}</div>
<div className="uppercase font-semibold heading mt-10">Collections</div>
<div className="uppercase font-semibold heading mt-10">{dictionary.collections}</div>
<div className="mt-4 flex items-center collection-options select-none">
<div className="flex items-center" onClick={() => setCreateCollectionModalOpen(true)}>
<IconPlus size={18} strokeWidth={2} />
<span className="label ml-2" id="create-collection">
Create Collection
{dictionary.createCollection}
</span>
</div>
<div className="flex items-center ml-6" onClick={handleOpenCollection}>
<IconFolders size={18} strokeWidth={2} />
<span className="label ml-2">Open Collection</span>
<span className="label ml-2">{dictionary.openCollection}</span>
</div>
<div className="flex items-center ml-6" onClick={() => setImportCollectionModalOpen(true)}>
<IconDownload size={18} strokeWidth={2} />
<span className="label ml-2" id="import-collection">
Import Collection
{dictionary.importCollection}
</span>
</div>
</div>
<div className="uppercase font-semibold heading mt-10 pt-6">Links</div>
<div className="mt-4 flex flex-col collection-options select-none">
<div className="flex items-center mt-2">
<a href="https://docs.usebruno.com" target="_blank" className="inline-flex items-center">
<IconBook size={18} strokeWidth={2} />
<span className="label ml-2">Documentation</span>
<span className="label ml-2">{dictionary.documentation}</span>
</a>
</div>
<div className="flex items-center mt-2">
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="inline-flex items-center">
<IconSpeakerphone size={18} strokeWidth={2} />
<span className="label ml-2">Report Issues</span>
<span className="label ml-2">{dictionary.reportIssues}</span>
</a>
</div>
<div className="flex items-center mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
<IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">GitHub</span>
<span className="label ml-2">{dictionary.gitHub}</span>
</a>
</div>
</div>

View File

@@ -0,0 +1,16 @@
export default {
aboutBruno: 'Opensource IDE for exploring and testing APIs',
collections: 'Collections',
createCollection: 'Create Collection',
openCollection: 'Open Collection',
importCollection: 'Import Collection',
documentation: 'Documentation',
reportIssues: 'Report Issues',
gitHub: 'GitHub',
collectionImportedSuccessfully: 'Collection imported successfully',
errorWhileOpeningCollection: 'An error occurred while opening the collection',
errorWhileImportingCollection:
'An error occurred while importing the collection. Check the logs for more information.',
discord: 'Discord',
twitter: 'Twitter'
};

View File

@@ -0,0 +1,5 @@
import en from './en.js';
export const dictionaries = {
en
};

View File

@@ -100,6 +100,11 @@ const GlobalStyle = createGlobalStyle`
}
}
input::placeholder {
color: ${(props) => props.theme.input.placeholder.color};
opacity: ${(props) => props.theme.input.placeholder.opacity};
}
@keyframes fade-in {
from {
opacity: 0;
@@ -168,7 +173,6 @@ const GlobalStyle = createGlobalStyle`
// (macos scrollbar styling is the ideal style reference)
@media not all and (pointer: coarse) {
* {
scrollbar-width: thin;
scrollbar-color: ${(props) => props.theme.scrollbar.color};
}

View File

@@ -14,6 +14,7 @@ import 'codemirror/lib/codemirror.css';
import 'graphiql/graphiql.min.css';
import 'react-tooltip/dist/react-tooltip.css';
import '@usebruno/graphql-docs/dist/esm/index.css';
import { DictionaryProvider } from 'providers/Dictionary/index';
function SafeHydrate({ children }) {
return <div suppressHydrationWarning>{typeof window === 'undefined' ? null : children}</div>;
@@ -59,13 +60,15 @@ function MyApp({ Component, pageProps }) {
<NoSsr>
<Provider store={ReduxStore}>
<ThemeProvider>
<ToastProvider>
<AppProvider>
<HotkeysProvider>
<Component {...pageProps} />
</HotkeysProvider>
</AppProvider>
</ToastProvider>
<DictionaryProvider>
<ToastProvider>
<AppProvider>
<HotkeysProvider>
<Component {...pageProps} />
</HotkeysProvider>
</AppProvider>
</ToastProvider>
</DictionaryProvider>
</ThemeProvider>
</Provider>
</NoSsr>

View File

@@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
import { get } from 'lodash';
import { useDispatch } from 'react-redux';
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
import ConfirmAppClose from './ConfirmAppClose';
@@ -18,6 +19,13 @@ export const AppProvider = (props) => {
dispatch(refreshScreenWidth());
}, []);
useEffect(() => {
const platform = get(navigator, 'platform', '');
if(platform && platform.toLowerCase().indexOf('mac') > -1) {
document.body.classList.add('os-mac');
}
}, []);
useEffect(() => {
const handleResize = () => {
dispatch(refreshScreenWidth());

View File

@@ -60,7 +60,7 @@ const trackStart = () => {
event: 'start',
properties: {
os: platformLib.os.family,
version: '1.20.2'
version: '1.25.0'
}
});
};

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { useState, useContext } from 'react';
import { dictionaries } from 'src/dictionaries/index';
export const DictionaryContext = React.createContext();
const DictionaryProvider = (props) => {
const [language, setLanguage] = useState('en');
const dictionary = dictionaries[language] ?? dictionaries.en;
return (
<DictionaryContext.Provider {...props} value={{ language, setLanguage, dictionary }}>
<>{props.children}</>
</DictionaryContext.Provider>
);
};
const useDictionary = () => {
const context = useContext(DictionaryContext);
if (context === undefined) {
throw new Error(`useDictionary must be used within a DictionaryProvider`);
}
return context;
};
export { useDictionary, DictionaryProvider };

View File

@@ -154,6 +154,31 @@ export const HotkeysProvider = (props) => {
};
}, [activeTabUid]);
// close all tabs
useEffect(() => {
Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if (activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
if (collection) {
const tabUids = tabs.filter((tab) => tab.collectionUid === collection.uid).map((tab) => tab.uid);
dispatch(
closeTabs({
tabUids: tabUids
})
);
}
}
return false; // this stops the event bubbling
});
return () => {
Mousetrap.unbind(['command+shift+w', 'ctrl+shift+w']);
};
}, [activeTabUid, tabs, collections, dispatch]);
return (
<HotkeysContext.Provider {...props} value="hotkey">
{showSaveRequestModal && (

View File

@@ -33,13 +33,14 @@ import {
requestCancelled,
resetRunResults,
responseReceived,
updateLastAction
updateLastAction,
setCollectionSecurityConfig
} from './index';
import { each } from 'lodash';
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform';
import { parseQueryParams, splitOnFirst } from 'utils/url/index';
import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
import { name } from 'file-loader';
@@ -192,10 +193,7 @@ export const sendCollectionOauth2Request = (collectionUid, itemUid) => (dispatch
const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);
const externalSecrets = getExternalCollectionSecretsForActiveEnvironment({ collection });
const secretVariables = getFormattedCollectionSecretVariables({ externalSecrets });
_sendCollectionOauth2Request(collection, environment, collectionCopy.collectionVariables, itemUid, secretVariables)
_sendCollectionOauth2Request(collection, environment, collectionCopy.runtimeVariables)
.then((response) => {
if (response?.data?.error) {
toast.error(response?.data?.error);
@@ -224,7 +222,7 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
const collectionCopy = cloneDeep(collection);
const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);
sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.collectionVariables)
sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)
.then((response) => {
return dispatch(
responseReceived({
@@ -284,7 +282,7 @@ export const cancelRunnerExecution = (cancelTokenUid) => (dispatch) => {
cancelNetworkRequest(cancelTokenUid).catch((err) => console.log(err));
};
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
export const runCollectionFolder = (collectionUid, folderUid, recursive, delay) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
@@ -314,8 +312,9 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dis
folder,
collectionCopy,
environment,
collectionCopy.collectionVariables,
recursive
collectionCopy.runtimeVariables,
recursive,
delay
)
.then(resolve)
.catch((err) => {
@@ -700,7 +699,7 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di
};
export const newHttpRequest = (params) => (dispatch, getState) => {
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body } = params;
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params;
return new Promise((resolve, reject) => {
const state = getState();
@@ -710,11 +709,20 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
}
const parts = splitOnFirst(requestUrl, '?');
const params = parseQueryParams(parts[1]);
each(params, (urlParam) => {
const queryParams = parseQueryParams(parts[1]);
each(queryParams, (urlParam) => {
urlParam.enabled = true;
urlParam.type = 'query';
});
const pathParams = parsePathParams(requestUrl);
each(pathParams, (pathParm) => {
pathParams.enabled = true;
pathParm.type = 'path'
});
const params = [...queryParams, ...pathParams];
const item = {
uid: uuid(),
type: requestType,
@@ -732,6 +740,9 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
sparql: null,
multipartForm: null,
formUrlEncoded: null
},
auth: auth ?? {
mode: 'none'
}
}
};
@@ -1036,16 +1047,18 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
name: brunoConfig.name,
pathname: pathname,
items: [],
collectionVariables: {},
runtimeVariables: {},
brunoConfig: brunoConfig
};
return new Promise((resolve, reject) => {
collectionSchema
.validate(collection)
.then(() => dispatch(_createCollection(collection)))
.then(resolve)
.catch(reject);
ipcRenderer.invoke('renderer:get-collection-security-config', pathname).then((securityConfig) => {
collectionSchema
.validate(collection)
.then(() => dispatch(_createCollection({ ...collection, securityConfig })))
.then(resolve)
.catch(reject);
});
});
};
@@ -1110,3 +1123,19 @@ export const importCollection = (collection, collectionLocation) => (dispatch, g
ipcRenderer.invoke('renderer:import-collection', collection, collectionLocation).then(resolve).catch(reject);
});
};
export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
ipcRenderer
.invoke('renderer:save-collection-security-config', collection?.pathname, securityConfig)
.then(async () => {
await dispatch(setCollectionSecurityConfig({ collectionUid, securityConfig }));
resolve();
})
.catch(reject);
});
};

View File

@@ -33,7 +33,6 @@ export const collectionsSlice = createSlice({
const collection = action.payload;
collection.settingsSelectedTab = 'headers';
collection.folderLevelSettingsSelectedTab = {};
// TODO: move this to use the nextAction approach
@@ -51,6 +50,12 @@ export const collectionsSlice = createSlice({
state.collections.push(collection);
}
},
setCollectionSecurityConfig: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
collection.securityConfig = action.payload.securityConfig;
}
},
brunoConfigUpdateEvent: (state, action) => {
const { collectionUid, brunoConfig } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
@@ -200,7 +205,7 @@ export const collectionsSlice = createSlice({
}
},
scriptEnvironmentUpdateEvent: (state, action) => {
const { collectionUid, envVariables, collectionVariables } = action.payload;
const { collectionUid, envVariables, runtimeVariables } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
@@ -230,7 +235,7 @@ export const collectionsSlice = createSlice({
});
}
collection.collectionVariables = collectionVariables;
collection.runtimeVariables = runtimeVariables;
}
},
processEnvUpdateEvent: (state, action) => {
@@ -717,7 +722,7 @@ export const collectionsSlice = createSlice({
uid: uuid(),
type: action.payload.type,
name: '',
value: '',
value: action.payload.value,
description: '',
enabled: true
});
@@ -1310,7 +1315,7 @@ export const collectionsSlice = createSlice({
}
if (isFolderRoot) {
const folderPath = path.dirname(file.meta.pathname);
const folderPath = getDirectoryName(file.meta.pathname);
const folderItem = findItemInCollectionByPathname(collection, folderPath);
if (folderItem) {
folderItem.root = file.data;
@@ -1566,29 +1571,29 @@ export const collectionsSlice = createSlice({
}
if (type === 'request-sent') {
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
item.status = 'running';
item.requestSent = action.payload.requestSent;
}
if (type === 'response-received') {
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
item.status = 'completed';
item.responseReceived = action.payload.responseReceived;
}
if (type === 'test-results') {
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
item.testResults = action.payload.testResults;
}
if (type === 'assertion-results') {
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
item.assertionResults = action.payload.assertionResults;
}
if (type === 'error') {
const item = collection.runnerResult.items.find((i) => i.uid === request.uid);
const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);
item.error = action.payload.error;
item.responseReceived = action.payload.responseReceived;
item.status = 'error';
@@ -1622,6 +1627,7 @@ export const collectionsSlice = createSlice({
export const {
createCollection,
setCollectionSecurityConfig,
brunoConfigUpdateEvent,
renameCollection,
removeCollection,

View File

@@ -24,7 +24,9 @@ export const tabsSlice = createSlice({
return;
}
if (['variables', 'collection-settings', 'collection-runner'].includes(action.payload.type)) {
if (
['variables', 'collection-settings', 'collection-runner', 'security-settings'].includes(action.payload.type)
) {
const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type);
if (tab) {
state.activeTabUid = tab.uid;

View File

@@ -58,6 +58,15 @@ body::-webkit-scrollbar-thumb,
border-radius: 5rem;
}
/*
* Mac-specific scrollbar styling
* This ensures that scrollbars are only visible when the user starts to scroll,
* providing a cleaner and more minimalistic appearance.
*/
body.os-mac * {
scrollbar-width: thin;
}
/*
* todo: this will be supported in the future to be changed via applying a theme
* making all the checkboxes and radios bigger

View File

@@ -20,7 +20,11 @@ const darkTheme = {
input: {
bg: 'rgb(65, 65, 65)',
border: 'rgb(65, 65, 65)',
focusBorder: 'rgb(65, 65, 65)'
focusBorder: 'rgb(65, 65, 65)',
placeholder: {
color: '#a2a2a2',
opacity: 0.75
}
},
variables: {
@@ -154,7 +158,7 @@ const darkTheme = {
modal: {
title: {
color: '#ccc',
bg: 'rgb(48, 48, 49)',
bg: 'rgb(38, 38, 39)',
iconColor: '#ccc'
},
body: {

View File

@@ -20,7 +20,11 @@ const lightTheme = {
input: {
bg: 'white',
border: '#ccc',
focusBorder: '#8b8b8b'
focusBorder: '#8b8b8b',
placeholder: {
color: '#a2a2a2',
opacity: 0.8
}
},
menubar: {

View File

@@ -2,10 +2,16 @@ const createContentType = (mode) => {
switch (mode) {
case 'json':
return 'application/json';
case 'text':
return 'text/plain';
case 'xml':
return 'application/xml';
case 'sparql':
return 'application/sparql-query';
case 'formUrlEncoded':
return 'application/x-www-form-urlencoded';
case 'graphql':
return 'application/json';
case 'multipartForm':
return 'multipart/form-data';
default:
@@ -13,13 +19,19 @@ const createContentType = (mode) => {
}
};
const createHeaders = (headers) => {
return headers
const createHeaders = (request, headers) => {
const enabledHeaders = headers
.filter((header) => header.enabled)
.map((header) => ({
name: header.name,
value: header.value
}));
const contentType = createContentType(request.body?.mode);
if (contentType !== '') {
enabledHeaders.push({ name: 'content-type', value: contentType });
}
return enabledHeaders;
};
const createQuery = (queryParams = []) => {
@@ -38,7 +50,11 @@ const createPostData = (body) => {
mimeType: contentType,
params: body[body.mode]
.filter((param) => param.enabled)
.map((param) => ({ name: param.name, value: param.value }))
.map((param) => ({
name: param.name,
value: param.value,
...(param.type === 'file' && { fileName: param.value })
}))
};
} else {
return {
@@ -54,7 +70,7 @@ export const buildHarRequest = ({ request, headers }) => {
url: encodeURI(request.url),
httpVersion: 'HTTP/1.1',
cookies: [],
headers: createHeaders(headers),
headers: createHeaders(request, headers),
queryString: createQuery(request.params),
postData: createPostData(request.body),
headersSize: 0,

View File

@@ -10,6 +10,7 @@ import isEqual from 'lodash/isEqual';
import cloneDeep from 'lodash/cloneDeep';
import { uuid } from 'utils/common';
import path from 'path';
import slash from 'utils/common/slash';
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
if (!str || !str.length || !isString(str)) {
@@ -98,7 +99,7 @@ export const findCollectionByItemUid = (collections, itemUid) => {
};
export const findItemByPathname = (items = [], pathname) => {
return find(items, (i) => i.pathname === pathname);
return find(items, (i) => slash(i.pathname) === slash(pathname));
};
export const findItemInCollectionByPathname = (collection, pathname) => {
@@ -382,17 +383,51 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
if (si.type == 'folder' && si?.root) {
di.root = {
request: {
headers: si?.root?.request?.headers,
script: si?.root?.request?.script,
vars: si?.root?.request?.vars,
tests: si?.root?.request?.tests
},
docs: si?.root?.request?.docs,
meta: {
name: si?.root?.meta?.name
}
request: {}
};
let { request, meta } = si?.root || {};
let { headers, script = {}, vars = {}, tests } = request || {};
// folder level headers
if (headers?.length) {
di.root.request.headers = headers;
}
// folder level script
if (Object.keys(script)?.length) {
di.root.request.script = {};
if (script?.req?.length) {
di.root.request.script.req = script?.req;
}
if (script?.res?.length) {
di.root.request.script.res = script?.res;
}
}
// folder level vars
if (Object.keys(vars)?.length) {
di.root.request.vars = {};
if (vars?.req?.length) {
di.root.request.vars.req = vars?.req;
}
if (vars?.res?.length) {
di.root.request.vars.res = vars?.res;
}
}
// folder level tests
if (tests?.length) {
di.root.request.tests = tests;
}
if (meta?.name) {
di.root.meta = {};
di.root.meta.name = meta?.name;
}
if (!Object.keys(di.root.request)?.length) {
delete di.root.request;
}
if (!Object.keys(di.root)?.length) {
delete di.root;
}
}
if (si.type === 'js') {
@@ -417,24 +452,59 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
collectionToSave.items = [];
collectionToSave.activeEnvironmentUid = collection.activeEnvironmentUid;
collectionToSave.environments = collection.environments || [];
collectionToSave.root = {
request: {
auth: collection?.root?.request?.auth,
headers: collection?.root?.request?.headers,
script: collection?.root?.request?.script,
vars: collection?.root?.request?.vars,
tests: collection?.root?.request?.tests
},
docs: collection?.root?.request?.docs,
meta: {
name: collection?.root?.meta?.name || collection?.name
}
request: {}
};
if (!collection?.root?.request?.auth?.mode) {
collectionToSave.root.request.auth = {
mode: 'none'
};
let { request, docs, meta } = collection?.root || {};
let { auth, headers, script = {}, vars = {}, tests } = request || {};
// collection level auth
if (auth?.mode) {
collectionToSave.root.request.auth = auth;
}
// collection level headers
if (headers?.length) {
collectionToSave.root.request.headers = headers;
}
// collection level script
if (Object.keys(script)?.length) {
collectionToSave.root.request.script = {};
if (script?.req?.length) {
collectionToSave.root.request.script.req = script?.req;
}
if (script?.res?.length) {
collectionToSave.root.request.script.res = script?.res;
}
}
// collection level vars
if (Object.keys(vars)?.length) {
collectionToSave.root.request.vars = {};
if (vars?.req?.length) {
collectionToSave.root.request.vars.req = vars?.req;
}
if (vars?.res?.length) {
collectionToSave.root.request.vars.res = vars?.res;
}
}
// collection level tests
if (tests?.length) {
collectionToSave.root.request.tests = tests;
}
// collection level docs
if (docs?.length) {
collectionToSave.root.docs = docs;
}
if (meta?.name) {
collectionToSave.root.meta = {};
collectionToSave.root.meta.name = meta?.name;
}
if (!Object.keys(collectionToSave.root.request)?.length) {
delete collectionToSave.root.request;
}
if (!Object.keys(collectionToSave.root)?.length) {
delete collectionToSave.root;
}
collectionToSave.brunoConfig = cloneDeep(collection?.brunoConfig);
@@ -718,11 +788,17 @@ export const getTotalRequestCountInCollection = (collection) => {
export const getAllVariables = (collection, item) => {
const environmentVariables = getEnvironmentVariables(collection);
let requestVariables = {};
if (item?.request) {
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
requestVariables = mergeFolderLevelVars(item?.request, requestTreePath);
}
const pathParams = getPathParams(item);
return {
...environmentVariables,
...collection.collectionVariables,
...requestVariables,
...collection.runtimeVariables,
pathParams: {
...pathParams
},
@@ -744,3 +820,36 @@ export const maskInputValue = (value) => {
.map(() => '*')
.join('');
};
const getTreePathFromCollectionToItem = (collection, _item) => {
let path = [];
let item = findItemInCollection(collection, _item?.uid);
while (item) {
path.unshift(item);
item = findParentItemInCollection(collection, item?.uid);
}
return path;
};
const mergeFolderLevelVars = (request, requestTreePath = []) => {
let requestVariables = {};
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
requestVariables[_var.name] = _var.value;
}
});
} else {
let vars = get(i, 'request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
requestVariables[_var.name] = _var.value;
}
});
}
}
return requestVariables;
};

View File

@@ -3,7 +3,7 @@ import filter from 'lodash/filter';
import find from 'lodash/find';
export const doesRequestMatchSearchText = (request, searchText = '') => {
return request.name.toLowerCase().includes(searchText.toLowerCase());
return request?.name?.toLowerCase().includes(searchText.toLowerCase());
};
export const doesFolderHaveItemsMatchSearchText = (item, searchText = '') => {

View File

@@ -12,6 +12,64 @@ const pathFoundInVariables = (path, obj) => {
return value !== undefined;
};
/**
* Changes the render behaviour for a given CodeMirror editor.
* Replaces all **rendered** characters, not the actual value, with the provided character.
*/
export class MaskedEditor {
/**
* @param {import('codemirror').Editor} editor CodeMirror editor instance
* @param {string} maskChar Target character being applied to all content
*/
constructor(editor, maskChar) {
this.editor = editor;
this.maskChar = maskChar;
this.enabled = false;
}
/**
* Set and apply new masking character
*/
enable = () => {
this.enabled = true;
this.editor.setValue(this.editor.getValue());
this.editor.on('inputRead', this.maskContent);
this.update();
};
/** Disables masking of the editor field. */
disable = () => {
this.enabled = false;
this.editor.off('inputRead', this.maskContent);
this.editor.setValue(this.editor.getValue());
};
/** Updates the rendered content if enabled. */
update = () => {
if (this.enabled) this.maskContent();
};
/** Replaces all rendered characters, with the provided character. */
maskContent = () => {
const content = this.editor.getValue();
this.editor.operation(() => {
// Clear previous masked text
this.editor.getAllMarks().forEach((mark) => mark.clear());
// Apply new masked text
for (let i = 0; i < content.length; i++) {
if (content[i] !== '\n') {
const maskedNode = document.createTextNode(this.maskChar);
this.editor.markText(
{ line: this.editor.posFromIndex(i).line, ch: this.editor.posFromIndex(i).ch },
{ line: this.editor.posFromIndex(i + 1).line, ch: this.editor.posFromIndex(i + 1).ch },
{ replacedWith: maskedNode, handleMouseEvents: true }
);
}
}
});
};
}
export const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams) => {
CodeMirror.defineMode('brunovariables', function (config, parserConfig) {
const { pathParams = {}, ...variables } = _variables || {};

View File

@@ -123,7 +123,7 @@ const curlToJson = (curlCommand) => {
request.urlWithoutQuery = 'http://' + request.urlWithoutQuery;
}
requestJson.url = request.urlWithoutQuery.replace(/\/$/, '');
requestJson.url = request.urlWithoutQuery;
requestJson.raw_url = request.url;
requestJson.method = request.method;
@@ -160,14 +160,15 @@ const curlToJson = (curlCommand) => {
}
if (request.auth) {
const splitAuth = request.auth.split(':');
const user = splitAuth[0] || '';
const password = splitAuth[1] || '';
requestJson.auth = {
user: repr(user),
password: repr(password)
};
if(request.auth.mode === 'basic'){
requestJson.auth = {
mode: 'basic',
basic: {
username: repr(request.auth.basic?.username),
password: repr(request.auth.basic?.password)
}
}
}
}
return Object.keys(requestJson).length ? requestJson : {};

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