Compare commits

..

93 Commits

Author SHA1 Message Date
Anoop M D
a18e5e5b94 Feat/safe mode quickjs (#2825)
* feat: testing quickjs sanbox

* feat: testing quickjs sanbox

* feat: testing quickjs sanbox
2024-08-13 19:06:17 +05:30
lohit
bde2c57a23 feat: safe mode updates (#2813)
* feat: safe mode updates
* feat: safe-mode updates
2024-08-12 16:52:59 +05:30
Anoop M D
9955b8796e feat: safe mode updates 2024-08-12 14:10:52 +05:30
Anoop M D
3b3fa8a856 feat: safe mode updates 2024-08-12 13:10:21 +05:30
Anoop M D
c687856a9e feat: safe mode updates 2024-08-12 12:28:41 +05:30
Anoop M D
033bba1805 feat: safe mode updates 2024-08-12 12:27:32 +05:30
Anoop M D
4fcb6f0980 feat: safe mode updates 2024-08-11 23:14:08 +05:30
Anoop M D
e4d2e5c1cf feat: safe mode updates 2024-08-11 19:22:48 +05:30
Anoop M D
969fe8e3cf feat: safe mode updates 2024-08-11 18:19:15 +05:30
Anoop M D
6d01c46d50 feat: safe mode updates 2024-08-11 18:02:05 +05:30
Anoop M D
751c7aa16d feat: safe mode updates 2024-08-11 16:11:12 +05:30
Anoop M D
b2baa1e48d feat: safe mode updates 2024-08-10 21:59:13 +05:30
Anoop M D
f834eb4425 feat: safe mode updates 2024-08-10 21:30:50 +05:30
lohit
26f8dd7a7b Safe Mode using isolated-vm (#2511)
feat: safe mode using isolated vm
2024-08-10 16:05:57 +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
Anoop M D
0988f2b86e chore: bumped version 2024-07-07 14:52:22 +05:30
Timon
e71e38f62e fix(#2573): Fix broken resequenzing of requests (#2574) 2024-07-06 23:07:36 +05:30
Anoop M D
589e173256 chore: version bump 2024-07-04 16:28:43 +05:30
lohit
e462eb6ecd import environments ui and collection root object for json export - updates (#2565)
* pr review changes

* collection root object in export json

* import environment updates
2024-07-04 16:10:38 +05:30
lohit
40f7be534a pr review changes (#2563) 2024-07-04 14:47:46 +05:30
lohit
c8f95a34e9 feat: bru hasEnvVar, hasVar, deleteVar (#2531)
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-07-04 13:21:27 +05:30
lohit
2aa7d26a89 add collection and folder data to exported bruno collection json (#2560)
* add collection and folder data to exported bruno collection json

* folder root data order
2024-07-04 13:18:40 +05:30
Baptiste Poulain
71353b0404 [Feature] : Bulk env import and UX/UI improvements (#2509)
* feat(bulk-env-import): bulk import working like a charm

* feat(bulk-env-import): refresh no env dialog's styling

* feat(bulk-env-import): group create and import env within initial modal, UI improvements

* feat(bulk-env-import): minor styling fixes

* feat(bulk-env-import): handle incorrect files in env importer

---------

Co-authored-by: bpoulaindev <bpoulainpro@gmail.com>
2024-07-04 12:01:24 +05:30
Sanjai Kumar
01605f6f2a Bugfix/links in docs (#2561)
* chore: fix markdown component

* Refactor MarkDown component to remove unnecessary useCallback hooks
2024-07-04 11:38:35 +05:30
Guillaume Leon
0d204694a6 Add autocompletion for headers value (#1142)
* feature: add autocompletion for headers value

* chore: rename file to autocompleteConstants and move it to codemirror utils
2024-07-04 11:28:46 +05:30
Nick Boyadjian
0d3765ad66 fix scrolling issue by setting the height of the CodeMirror using (#1058)
flex

Co-authored-by: Nick Boyadjian <nick.boyadjian@podium.com>
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
2024-07-04 11:26:34 +05:30
lohit
bd61e453ee fix/variables highlighting (#2502)
* js highlighting fix, only highlight path params pattern in url bar

* path param pattern matching validation update

* path param tooltip validation update
2024-07-01 19:25:55 +05:30
Anoop M D
02e23df349 chore: version bumped 2024-07-01 18:20:16 +05:30
lohit
61e0ac03fa updated testbench collection (#2542) 2024-07-01 15:40:31 +05:30
lohit
c895d7f357 feat: request vars, bru.getRequestVar function (#2541) 2024-07-01 14:15:25 +05:30
Anoop M D
45ff36d394 Folder level Headers, Scripts and Tests (#2529)
* [Feature] : Settings on folder level (#1334)

* feat(folder_settings): enable settings tab from folder, currently not using folder.bru

* feat(folder_settings): read and write in folder settings only in headers, ignore folder.bru file il requests list

* feat(folder_settings): merge collection and folder settings when sending network request

* feat(folder_settings): remove console, testing headers merging working fine

* feat(folder_settings): add missing endl for prettier check, remove redundant imports

---------

Co-authored-by: Baptiste POULAIN <baptistepoulain@MAC882.local>
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>

* feat: folder level scripts and tests

* feat: folder level variables (#2530)

---------

Co-authored-by: Baptiste Poulain <64689165+bpoulaindev@users.noreply.github.com>
Co-authored-by: Baptiste POULAIN <baptistepoulain@MAC882.local>
Co-authored-by: lohit <lohit.jiddimani@gmail.com>
2024-07-01 12:52:56 +05:30
Jean Lethiec
fd57b2ce94 Add collection var to CodeEditor props (#2501)
Co-authored-by: jean_lethiec <Jean_LETHIEC@connect-tech.sncf>
2024-06-28 20:10:14 +05:30
188 changed files with 11159 additions and 19784 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
@@ -24,7 +26,18 @@ jobs:
npm run build --workspace=packages/bruno-common
npm run build --workspace=packages/bruno-query
# test
# rebuild isolated-vm for bruno-js
- name: Rebuild bruno-js isolated-vm
run: |
cd packages/bruno-js/node_modules/isolated-vm
npm run rebuild || true
# 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
- name: Test Package bruno-query
run: npm run test --workspace=packages/bruno-query
- name: Test Package bruno-lang
@@ -33,12 +46,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 +59,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
@@ -59,6 +70,12 @@ jobs:
npm run build --workspace=packages/bruno-query
npm run build --workspace=packages/bruno-common
# rebuild isolated-vm for bruno-js
- name: Rebuild bruno-js isolated-vm
run: |
cd packages/bruno-js/node_modules/isolated-vm
npm run rebuild || true
- name: Run tests
run: |
cd packages/bruno-tests/collection
@@ -71,15 +88,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)

26170
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -47,12 +47,14 @@
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
"prepare": "husky install"
},
"overrides": {
"rollup": "3.2.5"
},
"dependencies": {
"isolated-vm": "^5.0.0",
"json-bigint": "^1.0.0",
"lossless-json": "^4.0.1"
"lossless-json": "^4.0.1",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0"
}
}

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');
@@ -58,10 +59,13 @@ if (!SERVER_RENDERED) {
'bru.cwd()',
'bru.getEnvName(key)',
'bru.getProcessEnv(key)',
'bru.hasEnvVar(key)',
'bru.getEnvVar(key)',
'bru.setEnvVar(key,value)',
'bru.hasVar(key)',
'bru.getVar(key)',
'bru.setVar(key,value)',
'bru.deleteVar(key)',
'bru.setNextRequest(requestName)'
];
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
@@ -118,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,
@@ -166,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) => {
@@ -286,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

@@ -13,6 +13,7 @@ import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/acti
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const Headers = ({ collection }) => {
@@ -117,6 +118,7 @@ const Headers = ({ collection }) => {
)
}
collection={collection}
autocomplete={MimeTypes}
/>
</td>
<td>

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

@@ -1,11 +1,11 @@
import React, { useEffect, useRef } from 'react';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import toast from 'react-hot-toast';
import { useFormik } from 'formik';
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
const CreateEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch();
@@ -27,7 +27,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
toast.success('Environment created in collection');
onClose();
})
.catch(() => toast.error('An error occurred while created the environment'));
.catch(() => toast.error('An error occurred while creating the environment'));
}
});
@@ -55,19 +55,21 @@ const CreateEnvironment = ({ collection, onClose }) => {
<label htmlFor="name" className="block font-semibold">
Environment Name
</label>
<input
id="environment-name"
type="text"
name="name"
ref={inputRef}
className="block textbox mt-2 w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
<div className="flex items-center mt-2">
<input
id="environment-name"
type="text"
name="name"
ref={inputRef}
className="block textbox w-full"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.name || ''}
/>
</div>
{formik.touched.name && formik.errors.name ? (
<div className="text-red-500">{formik.errors.name}</div>
) : null}

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

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

View File

@@ -4,45 +4,61 @@ import CreateEnvironment from './CreateEnvironment';
import EnvironmentList from './EnvironmentList';
import StyledWrapper from './StyledWrapper';
import ImportEnvironment from './ImportEnvironment';
import { IconFileAlert } from '@tabler/icons';
export const SharedButton = ({ children, className, onClick }) => {
return (
<button
type="button"
onClick={onClick}
className={`rounded bg-transparent px-2.5 py-2 w-fit text-xs font-semibold text-zinc-900 dark:text-zinc-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
${className}`}
>
{children}
</button>
);
};
const DefaultTab = ({ setTab }) => {
return (
<div className="text-center items-center flex flex-col">
<IconFileAlert size={64} strokeWidth={1} />
<span className="font-semibold mt-2">No environments found</span>
<span className="font-extralight mt-2 text-zinc-500 dark:text-zinc-400">
Get started by using the following buttons :
</span>
<div className="flex items-center justify-center mt-6">
<SharedButton onClick={() => setTab('create')}>
<span>Create Environment</span>
</SharedButton>
<span className="mx-4">Or</span>
<SharedButton onClick={() => setTab('import')}>
<span>Import Environment</span>
</SharedButton>
</div>
</div>
);
};
const EnvironmentSettings = ({ collection, onClose }) => {
const [isModified, setIsModified] = useState(false);
const { environments } = collection;
const [openCreateModal, setOpenCreateModal] = useState(false);
const [openImportModal, setOpenImportModal] = useState(false);
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
const [tab, setTab] = useState('default');
if (!environments || !environments.length) {
return (
<StyledWrapper>
<Modal
size="md"
title="Environments"
confirmText={'Close'}
handleConfirm={onClose}
handleCancel={onClose}
hideCancel={true}
>
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
{openImportModal && <ImportEnvironment collection={collection} onClose={() => setOpenImportModal(false)} />}
<div className="text-center flex flex-col">
<p>No environments found!</p>
<button
className="btn-create-environment text-link pr-2 py-3 mt-2 select-none"
onClick={() => setOpenCreateModal(true)}
>
<span>Create Environment</span>
</button>
<span>Or</span>
<button
className="btn-import-environment text-link pl-2 pr-2 py-3 select-none"
onClick={() => setOpenImportModal(true)}
>
<span>Import Environment</span>
</button>
</div>
<Modal size="md" title="Environments" handleCancel={onClose} hideCancel={true} hideFooter={true}>
{tab === 'create' ? (
<CreateEnvironment collection={collection} onClose={() => setTab('default')} />
) : tab === 'import' ? (
<ImportEnvironment collection={collection} onClose={() => setTab('default')} />
) : (
<></>
)}
<DefaultTab setTab={setTab} />
</Modal>
</StyledWrapper>
);

View File

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

View File

@@ -12,8 +12,8 @@ const FolderSettings = ({ collection, folder }) => {
const dispatch = useDispatch();
let tab = 'headers';
const { folderLevelSettingsSelectedTab } = collection;
if (folderLevelSettingsSelectedTab?.[folder.uid]) {
tab = folderLevelSettingsSelectedTab[folder.uid];
if (folderLevelSettingsSelectedTab?.[folder?.uid]) {
tab = folderLevelSettingsSelectedTab[folder?.uid];
}
const setTab = (tab) => {

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,15 +1,28 @@
import MarkdownIt from 'markdown-it';
import StyledWrapper from './StyledWrapper';
import * as React from 'react';
import React from 'react';
const md = new MarkdownIt();
const Markdown = ({ onDoubleClick, content }) => {
const handleOnClick = (event) => {
const target = event.target;
if (target.tagName === 'A') {
event.preventDefault();
const href = target.getAttribute('href');
if (href) {
window.open(href, '_blank');
return;
}
}
};
const handleOnDoubleClick = (event) => {
if (event?.detail === 2) {
if (event.detail === 2) {
onDoubleClick();
}
};
const htmlFromMarkdown = md.render(content || '');
return (
@@ -17,7 +30,8 @@ const Markdown = ({ onDoubleClick, content }) => {
<div
className="markdown-body"
dangerouslySetInnerHTML={{ __html: htmlFromMarkdown }}
onClick={handleOnDoubleClick}
onClick={handleOnClick}
onDoubleClick={handleOnDoubleClick}
/>
</StyledWrapper>
);

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

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

@@ -151,7 +151,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
</div>
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
</div>
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
<section className="flex w-full mt-5 flex-1">{getTabPanel(focusedTab.requestPaneTab)}</section>
</StyledWrapper>
);
};

View File

@@ -137,7 +137,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,8 +4,7 @@ const StyledWrapper = styled.div`
div.CodeMirror {
background: ${(props) => props.theme.codemirror.bg};
border: solid 1px ${(props) => props.theme.codemirror.border};
/* todo: find a better way */
height: calc(100vh - 220px);
flex: 1 1 0;
}
textarea.cm-editor {

View File

@@ -209,7 +209,7 @@ export default class QueryEditor extends React.Component {
return (
<>
<StyledWrapper
className="h-full w-full relative"
className="h-full w-full flex flex-col relative"
aria-label="Query Editor"
ref={(node) => {
this._node = node;

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

@@ -69,6 +69,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
onChange={(newValue) => onUrlChange(newValue)}
onRun={handleRun}
collection={collection}
highlightPathParams={true}
item={item}
/>
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>

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

@@ -9,6 +9,7 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const RequestHeaders = ({ item, collection }) => {
@@ -115,8 +116,10 @@ const RequestHeaders = ({ item, collection }) => {
)
}
onRun={handleRun}
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

@@ -18,6 +18,7 @@ import CollectionSettings from 'components/CollectionSettings';
import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper';
import SecuritySettings from 'components/SecuritySettings/index';
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

@@ -81,7 +81,7 @@ const RequestTab = ({ tab, collection, folderUid }) => {
return color;
};
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' ? (

View File

@@ -14,6 +14,8 @@ const Wrapper = styled.div`
display: none;
}
scrollbar-width: none;
li {
display: inline-flex;
max-width: 150px;

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.25 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.25 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

@@ -50,6 +50,7 @@ const CodeView = ({ language, item }) => {
</CopyToClipboard>
<CodeEditor
readOnly
collection={collection}
value={snippet}
font={get(preferences, 'font.codeFont', 'default')}
theme={displayedTheme}

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

@@ -60,7 +60,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
<button
type="button"
onClick={onClick}
className={`rounded bg-transparent px-2.5 py-1 text-xs font-semibold text-slate-900 dark:text-slate-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
className={`rounded bg-transparent px-2.5 py-1 text-xs font-semibold text-zinc-900 dark:text-zinc-50 shadow-sm ring-1 ring-inset ring-zinc-300 dark:ring-zinc-500 hover:bg-gray-50 dark:hover:bg-zinc-700
${className}`}
>
{children}

View File

@@ -160,7 +160,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

@@ -161,7 +161,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 +229,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 +262,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.18.0</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.24.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;
}
@@ -131,12 +146,39 @@ class SingleLineEditor extends Component {
addOverlay = (variables) => {
this.variables = variables;
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
this.editor.setOption('mode', 'combinedmode');
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams);
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">
<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

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

@@ -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.18.0'
version: '1.24.0'
}
});
};

View File

@@ -33,7 +33,8 @@ import {
requestCancelled,
resetRunResults,
responseReceived,
updateLastAction
updateLastAction,
setCollectionSecurityConfig
} from './index';
import { each } from 'lodash';
@@ -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) => {
@@ -1036,16 +1035,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 +1111,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
});
@@ -1179,7 +1184,6 @@ export const collectionsSlice = createSlice({
uid: uuid(),
name: '',
value: '',
type: 'request',
enabled: true
});
set(folder, 'root.request.vars.req', vars);
@@ -1189,7 +1193,6 @@ export const collectionsSlice = createSlice({
uid: uuid(),
name: '',
value: '',
type: 'response',
enabled: true
});
set(folder, 'root.request.vars.res', vars);
@@ -1312,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;
@@ -1568,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';
@@ -1624,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 = []) => {
@@ -54,7 +66,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

@@ -0,0 +1,56 @@
export const MimeTypes = [
'application/atom+xml',
'application/ecmascript',
'application/json',
'application/vnd.api+json',
'application/javascript',
'application/octet-stream',
'application/ogg',
'application/pdf',
'application/postscript',
'application/rdf+xml',
'application/rss+xml',
'application/soap+xml',
'application/font-woff',
'application/x-yaml',
'application/xhtml+xml',
'application/xml',
'application/xml-dtd',
'application/xop+xml',
'application/zip',
'application/gzip',
'application/graphql',
'application/x-www-form-urlencoded',
'audio/basic',
'audio/L24',
'audio/mp4',
'audio/mpeg',
'audio/ogg',
'audio/vorbis',
'audio/vnd.rn-realaudio',
'audio/vnd.wave',
'audio/webm',
'image/gif',
'image/jpeg',
'image/pjpeg',
'image/png',
'image/svg+xml',
'image/tiff',
'message/http',
'message/imdn+xml',
'message/partial',
'message/rfc822',
'multipart/mixed',
'multipart/alternative',
'multipart/related',
'multipart/form-data',
'multipart/signed',
'multipart/encrypted',
'text/cmd',
'text/css',
'text/csv',
'text/html',
'text/plain',
'text/vcard',
'text/xml'
];

View File

@@ -31,8 +31,8 @@ if (!SERVER_RENDERED) {
if (str.startsWith('{{')) {
variableName = str.replace('{{', '').replace('}}', '').trim();
variableValue = interpolate(get(options.variables, variableName), options.variables);
} else if (str.startsWith(':')) {
variableName = str.replace(':', '').trim();
} else if (str.startsWith('/:')) {
variableName = str.replace('/:', '').trim();
variableValue =
options.variables && options.variables.pathParams ? options.variables.pathParams[variableName] : undefined;
}

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) => {
@@ -380,6 +381,55 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
}
}
if (si.type == 'folder' && si?.root) {
di.root = {
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') {
di.fileContent = si.raw;
}
@@ -403,6 +453,60 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
collectionToSave.activeEnvironmentUid = collection.activeEnvironmentUid;
collectionToSave.environments = collection.environments || [];
collectionToSave.root = {
request: {}
};
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);
// delete proxy password if present
@@ -684,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
},
@@ -710,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

@@ -12,8 +12,67 @@ const pathFoundInVariables = (path, obj) => {
return value !== undefined;
};
export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
CodeMirror.defineMode('combinedmode', function (config, parserConfig) {
/**
* 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 || {};
const variablesOverlay = {
token: function (stream) {
if (stream.match('{{', true)) {
@@ -37,13 +96,13 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
const urlPathParamsOverlay = {
token: function (stream) {
if (stream.match(':', true)) {
if (stream.match('/:', true)) {
let ch;
let word = '';
while ((ch = stream.next()) != null) {
if (ch === '/' || ch === '?' || ch === '&' || ch === '=') {
stream.backUp(1);
const found = pathFoundInVariables(word, variables?.pathParams);
const found = pathFoundInVariables(word, pathParams);
const status = found ? 'valid' : 'invalid';
const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;
return `variable-${status} ${randomClass}`;
@@ -53,21 +112,24 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
// If we've consumed all characters and the word is not empty, it might be a path parameter at the end of the URL.
if (word) {
const found = pathFoundInVariables(word, variables?.pathParams);
const found = pathFoundInVariables(word, pathParams);
const status = found ? 'valid' : 'invalid';
const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;
return `variable-${status} ${randomClass}`;
}
}
stream.skipTo(':') || stream.skipToEnd();
stream.skipTo('/:') || stream.skipToEnd();
return null;
}
};
return CodeMirror.overlayMode(
CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay),
urlPathParamsOverlay
);
let baseMode = CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || mode), variablesOverlay);
if (highlightPathParams) {
return CodeMirror.overlayMode(baseMode, urlPathParamsOverlay);
} else {
return baseMode;
}
});
};

View File

@@ -72,11 +72,10 @@ const parseCurlCommand = (curlCommand) => {
parsedArguments.header.forEach((header) => {
if (header.indexOf('Cookie') !== -1) {
cookieString = header;
} else {
const components = header.split(/:(.*)/);
if (components[1]) {
headers[components[0]] = components[1].trim();
}
}
const components = header.split(/:(.*)/);
if (components[1]) {
headers[components[0]] = components[1].trim();
}
});
}
@@ -119,15 +118,16 @@ const parseCurlCommand = (curlCommand) => {
cookies = cookie.parse(cookieString.replace(/^Cookie: /gi, ''), cookieParseOptions);
}
let method;
if (parsedArguments.X === 'POST') {
let parsedMethodArgument = parsedArguments.X || parsedArguments.request || parsedArguments.T;
if (parsedMethodArgument === 'POST') {
method = 'post';
} else if (parsedArguments.X === 'PUT' || parsedArguments.T) {
} else if (parsedMethodArgument === 'PUT') {
method = 'put';
} else if (parsedArguments.X === 'PATCH') {
} else if (parsedMethodArgument === 'PATCH') {
method = 'patch';
} else if (parsedArguments.X === 'DELETE') {
} else if (parsedMethodArgument === 'DELETE') {
method = 'delete';
} else if (parsedArguments.X === 'OPTIONS') {
} else if (parsedMethodArgument === 'OPTIONS') {
method = 'options';
} else if (
(parsedArguments.d ||

View File

@@ -57,10 +57,20 @@ const parsePostmanEnvironment = (str) => {
const importEnvironment = () => {
return new Promise((resolve, reject) => {
fileDialog({ accept: 'application/json' })
.then(readFile)
.then(parsePostmanEnvironment)
.then((environment) => resolve(environment))
fileDialog({ multiple: true, accept: 'application/json' })
.then((files) => {
return Promise.all(
Object.values(files ?? {}).map((file) =>
readFile([file])
.then(parsePostmanEnvironment)
.catch((err) => {
console.error(`Error processing file: ${file.name || 'undefined'}`, err);
throw err;
})
)
);
})
.then((environments) => resolve(environments))
.catch((err) => {
console.log(err);
reject(new BrunoError('Import Environment failed'));

View File

@@ -1,9 +1,9 @@
import { safeStringifyJSON } from 'utils/common';
export const sendNetworkRequest = async (item, collection, environment, collectionVariables) => {
export const sendNetworkRequest = async (item, collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
if (['http-request', 'graphql-request'].includes(item.type)) {
sendHttpRequest(item, collection, environment, collectionVariables)
sendHttpRequest(item, collection, environment, runtimeVariables)
.then((response) => {
resolve({
state: 'success',
@@ -22,22 +22,22 @@ export const sendNetworkRequest = async (item, collection, environment, collecti
});
};
const sendHttpRequest = async (item, collection, environment, collectionVariables) => {
const sendHttpRequest = async (item, collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer
.invoke('send-http-request', item, collection, environment, collectionVariables)
.invoke('send-http-request', item, collection, environment, runtimeVariables)
.then(resolve)
.catch(reject);
});
};
export const sendCollectionOauth2Request = async (collection, environment, collectionVariables) => {
export const sendCollectionOauth2Request = async (collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer
.invoke('send-collection-oauth2-request', collection, environment, collectionVariables)
.invoke('send-collection-oauth2-request', collection, environment, runtimeVariables)
.then(resolve)
.catch(reject);
});

View File

@@ -107,14 +107,14 @@ export const isValidUrl = (url) => {
}
};
export const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) => {
export const interpolateUrl = ({ url, envVars, runtimeVariables, processEnvVars }) => {
if (!url || !url.length || typeof url !== 'string') {
return;
}
return interpolate(url, {
...envVars,
...collectionVariables,
...runtimeVariables,
process: {
env: {
...processEnvVars

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