mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 01:18:32 +00:00
Compare commits
45 Commits
v1.11.0
...
revert-192
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2695ff2bf | ||
|
|
acca7984a4 | ||
|
|
0cce14b4d5 | ||
|
|
179c30b15b | ||
|
|
5a31daf84a | ||
|
|
920c490548 | ||
|
|
484f6ef0c1 | ||
|
|
e7ff0ba5a8 | ||
|
|
615ed0c584 | ||
|
|
0173d8812d | ||
|
|
65dbf9c8ba | ||
|
|
67df18e6e0 | ||
|
|
aa5fee01c3 | ||
|
|
84128052c0 | ||
|
|
fbee23329d | ||
|
|
5b2ca0ea03 | ||
|
|
cf08118458 | ||
|
|
17dac6be67 | ||
|
|
64b90b4cc3 | ||
|
|
2c83e98502 | ||
|
|
ce5dd41267 | ||
|
|
1349a79750 | ||
|
|
c20beab0a0 | ||
|
|
5539cc1a2d | ||
|
|
d20de4da0a | ||
|
|
50e3943fb1 | ||
|
|
c357d3da4b | ||
|
|
9b89f33103 | ||
|
|
0f69c30a86 | ||
|
|
753ca4341f | ||
|
|
e278116356 | ||
|
|
ae3c76a6c1 | ||
|
|
0b2b16abcc | ||
|
|
8503752e09 | ||
|
|
9b7cdb2d48 | ||
|
|
7741a3e4ee | ||
|
|
82c600a0e6 | ||
|
|
f8ba781340 | ||
|
|
f96f763f14 | ||
|
|
a546457e0f | ||
|
|
2b0ad29b93 | ||
|
|
cdbb15f33e | ||
|
|
14911b4def | ||
|
|
410eecc884 | ||
|
|
2cd0e065bd |
34
.github/ISSUE_TEMPLATE/BugReport.yaml
vendored
Normal file
34
.github/ISSUE_TEMPLATE/BugReport.yaml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'I have checked the following:'
|
||||
options:
|
||||
- label: I use the newest version of bruno.
|
||||
required: true
|
||||
- label: I've searched existing issues and found nothing related to my issue.
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of the bug.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: .bru file to reproduce the bug
|
||||
description: Attach your .bru file here that can reqroduce the problem.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots/Live demo link
|
||||
description: Add some screenshots to help explain the problem.
|
||||
validations:
|
||||
required: true
|
||||
26
.github/ISSUE_TEMPLATE/FeatureRequest.yaml
vendored
Normal file
26
.github/ISSUE_TEMPLATE/FeatureRequest.yaml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for this project.
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'I have checked the following:'
|
||||
options:
|
||||
- label: I've searched existing issues and found nothing related to my issue.
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Suggest an idea for this project.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature you want to add
|
||||
description: A clear and concise description of the feature you want to be added.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Mockups or Images of the feature
|
||||
description: Add some images to support your feature.
|
||||
validations:
|
||||
required: true
|
||||
8
.github/ISSUE_TEMPLATE/config.yaml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yaml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Discussions
|
||||
url: https://github.com/usebruno/bruno/discussions
|
||||
about: You can ask general questions or give feedback here.
|
||||
- name: Discord Server
|
||||
url: https://discord.com/invite/KgcZUncpjq
|
||||
about: Join our Discord community to chat about Bruno.
|
||||
2
.github/workflows/release-snap.yml
vendored
2
.github/workflows/release-snap.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
25
.github/workflows/tests.yml
vendored
25
.github/workflows/tests.yml
vendored
@@ -5,15 +5,6 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
# Assign permissions for unit tests to be reported.
|
||||
# See https://github.com/dorny/test-reporter/issues/168
|
||||
permissions:
|
||||
statuses: write
|
||||
checks: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
unit-test:
|
||||
name: Unit Tests
|
||||
@@ -21,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Install dependencies
|
||||
@@ -56,7 +47,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
@@ -75,19 +66,17 @@ jobs:
|
||||
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit
|
||||
|
||||
- name: Publish Test Report
|
||||
uses: dorny/test-reporter@v1
|
||||
if: success() || failure()
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
if: always()
|
||||
with:
|
||||
name: Test Report
|
||||
path: packages/bruno-tests/collection/junit.xml
|
||||
reporter: java-junit
|
||||
|
||||
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@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Install dependencies
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
bun.lockb
|
||||
node_modules
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
|
||||
BIN
assets/images/sponsors/zuplo.png
Normal file
BIN
assets/images/sponsors/zuplo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@@ -56,7 +56,7 @@ npm run dev:electron
|
||||
|
||||
### Dépannage
|
||||
|
||||
Vous pourriez rencontrer une erreur `Unsupported platform` durant le lancement de `npm install`. Pour résoudre cela, veuillez supprimer le répertoire `node_modules` ainsi que le fichier `package-lock.json` et lancez à nouveau `npm install`. Cela devrait isntaller tous les paquets nécessaires pour lancer l'application.
|
||||
Vous pourriez rencontrer une erreur `Unsupported platform` durant le lancement de `npm install`. Pour résoudre cela, veuillez supprimer le répertoire `node_modules` ainsi que le fichier `package-lock.json` et lancez à nouveau `npm install`. Cela devrait installer tous les paquets nécessaires pour lancer l'application.
|
||||
|
||||
```shell
|
||||
# Efface les répertoires node_modules dans les sous-répertoires
|
||||
@@ -85,6 +85,6 @@ npm test --workspace=packages/bruno-lang
|
||||
- Merci de conserver les PR minimes et focalisées sur un seul objectif
|
||||
- Merci de suivre le format de nom des branches :
|
||||
- feature/[feature name]: Cette branche doit contenir une fonctionnalité spécifique
|
||||
- Exemple: feature/dark-mode
|
||||
- Exemple : feature/dark-mode
|
||||
- bugfix/[bug name]: Cette branche doit contenir seulement une solution pour un bug spécifique
|
||||
- Exemple: bugfix/bug-1
|
||||
- Exemple : bugfix/bug-1
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
### Publier Bruno dans un nouveau gestionnaire de paquets
|
||||
|
||||
Bien que notre code soit open source et disponible pour tout le monde, nous vous remercions de nous contacter avant de considérer sa publication sur un nouveau gestionnaire de paquets. En tant que createur de Bruno, je détiens la marque `Bruno` pour ce projet et j'aimerais gérer moi-même sa distribution. Si vous voyez Bruno sur un nouveau gestionnaire de paquets, merci de créer une _issue_ Github.
|
||||
Bien que notre code soit open source et disponible pour tout le monde, nous vous remercions de nous contacter avant de considérer sa publication sur un nouveau gestionnaire de paquets. En tant que créateur de Bruno, je détiens la marque `Bruno` pour ce projet et j'aimerais gérer moi-même sa distribution. Si vous voyez Bruno sur un nouveau gestionnaire de paquets, merci de créer une _issue_ GitHub.
|
||||
|
||||
Bien que la majorité de nos fonctionnalités soient gratuites et open source (ce qui couvre les apis REST et GraphQL), nous nous efforçons de trouver un équilibre harmonieux entre les principes de l'open source et la pérennité - https://github.com/usebruno/bruno/discussions/269
|
||||
Bien que la majorité de nos fonctionnalités soient gratuites et open source (ce qui couvre les APIs REST et GraphQL), nous nous efforçons de trouver un équilibre harmonieux entre les principes de l'open source et la pérennité - https://github.com/usebruno/bruno/discussions/269
|
||||
|
||||
@@ -75,7 +75,7 @@ sudo apt install bruno
|
||||
- [网站](https://www.usebruno.com)
|
||||
- [价格](https://www.usebruno.com/pricing)
|
||||
- [下载](https://www.usebruno.com/downloads)
|
||||
- [Github 赞助](https://github.com/sponsors/helloanoop).
|
||||
- [GitHub 赞助](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### 展示 🎥
|
||||
|
||||
@@ -85,7 +85,7 @@ sudo apt install bruno
|
||||
|
||||
### 支持 ❤️
|
||||
|
||||
如果您喜欢 Bruno 并想支持我们的开源工作,请考虑通过 [Github Sponsors](https://github.com/sponsors/helloanoop) 来赞助我们。
|
||||
如果您喜欢 Bruno 并想支持我们的开源工作,请考虑通过 [GitHub Sponsors](https://github.com/sponsors/helloanoop) 来赞助我们。
|
||||
|
||||
### 分享评价 📣
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE de código abierto para explorar y probar APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
@@ -85,7 +85,7 @@ O cualquier otro sistema de control de versiones que prefieras
|
||||
- [Precios](https://www.usebruno.com/pricing)
|
||||
- [Descargas](https://www.usebruno.com/downloads)
|
||||
|
||||
### Casos de uso 🎥
|
||||
### Casos de uso 🎥
|
||||
|
||||
- [Testimonios](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Centro de Conocimiento](https://github.com/usebruno/bruno/discussions/386)
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
|
||||
[English](/readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | **Français** | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md)
|
||||
|
||||
Bruno est un nouveau client API, innovant, qui a pour but de révolutionner le _status quo_ que représente Postman et les autres outils.
|
||||
Bruno est un nouveau client API, innovant, qui a pour but de révolutionner le _statu quo_ que représente Postman et les autres outils.
|
||||
|
||||
Bruno sauvegarde vos collections directement sur votre système de fichiers. Nous utilisons un langage de balise de type texte pour décrire les requêtes API.
|
||||
|
||||
Vous pouvez utiliser git ou tout autre gestionnaire de version pour travailler de manière collaborative sur vos collections d'APIs.
|
||||
|
||||
Bruno ne fonctionne qu'en mode déconnecté. Il n'y a pas de d'abonnement ou de synchronisation avec le cloud Bruno, il n'y en aura jamais. Nous sommes conscients de la confidentialité de vos données et nous sommes convaincus qu'elles doivent rester sur vos appareils. Vous pouvez lire notre vision à long terme [ici (en anglais)](https://github.com/usebruno/bruno/discussions/269).
|
||||
Bruno ne fonctionne qu'en mode déconnecté. Il n'y a pas d'abonnement ou de synchronisation avec le cloud Bruno, il n'y en aura jamais. Nous sommes conscients de la confidentialité de vos données et nous sommes convaincus qu'elles doivent rester sur vos appareils. Vous pouvez lire notre vision à long terme [ici (en anglais)](https://github.com/usebruno/bruno/discussions/269).
|
||||
|
||||
|
||||
📢 Regarder notre présentation récente lors de la conférence India FOSS 3.0 (en anglais) [ici](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
@@ -75,7 +75,7 @@ Ou n'importe quel système de gestion de sources
|
||||
- [Site web](https://www.usebruno.com)
|
||||
- [Prix](https://www.usebruno.com/pricing)
|
||||
- [Téléchargement](https://www.usebruno.com/downloads)
|
||||
- [Sponsors Github](https://github.com/sponsors/helloanoop)
|
||||
- [Sponsors GitHub](https://github.com/sponsors/helloanoop)
|
||||
|
||||
### Showcase 🎥
|
||||
|
||||
@@ -89,7 +89,7 @@ Ouaf! Si vous aimez le projet, cliquez sur le bouton ⭐ !!
|
||||
|
||||
### Partage de témoignages 📣
|
||||
|
||||
Si Bruno vous a aidé dans votre travail, au sein de votre équipe, merci de penser à partager votre témoignage sur la [page discussion Github dédiée](https://github.com/usebruno/bruno/discussions/343)
|
||||
Si Bruno vous a aidé dans votre travail, au sein de votre équipe, merci de penser à partager votre témoignage sur la [page discussion GitHub dédiée](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
### Publier Bruno sur un nouveau gestionnaire de paquets
|
||||
|
||||
@@ -125,7 +125,7 @@ Même si vous n'êtes pas en mesure de contribuer directement via du code, n'hé
|
||||
**Logo**
|
||||
|
||||
Le logo est issu de [OpenMoji](https://openmoji.org/library/emoji-1F436/).
|
||||
Licence: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
Licence : CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### Licence 📄
|
||||
|
||||
|
||||
@@ -79,11 +79,11 @@ sudo apt install bruno
|
||||
|
||||
### 후기 공유 📣
|
||||
|
||||
Bruno가 여러분과 여러분의 팀에 도움이 되었다면, 잊지 말고 공유해 주세요. [Github discussion 공유 링크](https://github.com/usebruno/bruno/discussions/343)
|
||||
Bruno가 여러분과 여러분의 팀에 도움이 되었다면, 잊지 말고 공유해 주세요. [GitHub discussion 공유 링크](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
### 새 패키지 관리자에게 게시
|
||||
|
||||
더 많은 정보를 확인하시려명 링크를 클릭해 주세요.[배포 가이드](publishing.md)
|
||||
더 많은 정보를 확인하시려면 링크를 클릭해 주세요. [배포 가이드](../../publishing.md)
|
||||
|
||||
### 컨트리뷰트 👩💻🧑💻
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ Lub dowolny inny system kontroli wersji, który wybierzesz
|
||||
- [Strona Internetowa](https://www.usebruno.com)
|
||||
- [Cennik](https://www.usebruno.com/pricing)
|
||||
- [Pobieranie](https://www.usebruno.com/downloads)
|
||||
- [Sponsorzy Github](https://github.com/sponsors/helloanoop).
|
||||
- [Sponsorzy GitHub](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### Zobacz 🎥
|
||||
|
||||
@@ -83,7 +83,7 @@ Lub dowolny inny system kontroli wersji, który wybierzesz
|
||||
|
||||
### Wsparcie ❤️
|
||||
|
||||
Jeśli podoba Ci się Bruno i chcesz wspierać naszą pracę opensource, rozważ sponsorowanie nas przez [Sponsorzy Github](https://github.com/sponsors/helloanoop).
|
||||
Jeśli podoba Ci się Bruno i chcesz wspierać naszą pracę opensource, rozważ sponsorowanie nas przez [Sponsorzy GitHub](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### Udostępnij Opinie 📣
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ Ou qualquer sistema de controle de versão de sua escolha.
|
||||
- [Website](https://www.usebruno.com)
|
||||
- [Preços](https://www.usebruno.com/pricing)
|
||||
- [Download](https://www.usebruno.com/downloads)
|
||||
- [Github Sponsors](https://github.com/sponsors/helloanoop)
|
||||
- [GitHub Sponsors](https://github.com/sponsors/helloanoop)
|
||||
|
||||
### Showcase 🎥
|
||||
|
||||
@@ -104,7 +104,7 @@ Ou qualquer sistema de controle de versão de sua escolha.
|
||||
|
||||
### Apoie ❤️
|
||||
|
||||
Au-au! Se você gosta do projeto e deseja apoiar nosso trabalho, considere nos ajudando via [Github Sponsors](https://github.com/sponsors/helloanoop).
|
||||
Au-au! Se você gosta do projeto e deseja apoiar nosso trabalho, considere nos ajudando via [GitHub Sponsors](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### Compartilhe sua experiência 📣
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ Veya seçtiğiniz herhangi bir sürüm kontrol sistemi
|
||||
- [Web sitesi](https://www.usebruno.com)
|
||||
- [Fiyatlandırma](https://www.usebruno.com/pricing)
|
||||
- [İndir](https://www.usebruno.com/downloads)
|
||||
- [Github Sponsorları](https://github.com/sponsors/helloanoop).
|
||||
- [GitHub Sponsorları](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### Vitrin 🎥
|
||||
|
||||
@@ -83,7 +83,7 @@ Veya seçtiğiniz herhangi bir sürüm kontrol sistemi
|
||||
|
||||
### Destek ❤️
|
||||
|
||||
Bruno'yu seviyorsanız ve açık kaynak çalışmalarımızı desteklemek istiyorsanız, [Github Sponsorları](https://github.com/sponsors/helloanoop) aracılığıyla bize sponsor olmayı düşünün.
|
||||
Bruno'yu seviyorsanız ve açık kaynak çalışmalarımızı desteklemek istiyorsanız, [GitHub Sponsorları](https://github.com/sponsors/helloanoop) aracılığıyla bize sponsor olmayı düşünün.
|
||||
|
||||
### Referansları Paylaşın 📣
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ sudo apt install bruno
|
||||
- [網站](https://www.usebruno.com)
|
||||
- [定價](https://www.usebruno.com/pricing)
|
||||
- [下載](https://www.usebruno.com/downloads)
|
||||
- [Github 贊助](https://github.com/sponsors/helloanoop).
|
||||
- [GitHub 贊助](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### 展示 🎥
|
||||
|
||||
@@ -83,7 +83,7 @@ sudo apt install bruno
|
||||
|
||||
### 贊助支持 ❤️
|
||||
|
||||
如果您喜歡 Bruno 和希望支持我們在開源上的工作,請考慮使用 [Github Sponsors](https://github.com/sponsors/helloanoop) 來贊助我們。
|
||||
如果您喜歡 Bruno 和希望支持我們在開源上的工作,請考慮使用 [GitHub Sponsors](https://github.com/sponsors/helloanoop) 來贊助我們。
|
||||
|
||||
### 分享感想 📣
|
||||
|
||||
|
||||
885
package-lock.json
generated
885
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -47,10 +47,12 @@
|
||||
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"json-bigint": "^1.0.0"
|
||||
"json-bigint": "^1.0.0",
|
||||
"lossless-json": "^4.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
"@fortawesome/react-fontawesome": "^0.1.16",
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"@tabler/icons": "^1.46.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.6.0",
|
||||
"@usebruno/schema": "0.7.0",
|
||||
"axios": "^1.5.1",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "5.65.2",
|
||||
|
||||
@@ -37,7 +37,7 @@ const GrantTypeSelector = ({ collection }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// initalize redux state with a default oauth2 grant type
|
||||
// initialize redux state with a default oauth2 grant type
|
||||
// authorization_code - default option
|
||||
!oAuth?.grantType &&
|
||||
dispatch(
|
||||
|
||||
@@ -37,7 +37,11 @@ const Auth = ({ collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-2">
|
||||
<StyledWrapper className="w-full h-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Configures authentication for the entire collection. This applies to all requests using the{' '}
|
||||
<span className="font-medium">Inherit</span> option in the <span className="font-medium">Auth</span> tab.
|
||||
</div>
|
||||
<div className="flex flex-grow justify-start items-center">
|
||||
<AuthMode collection={collection} />
|
||||
</div>
|
||||
|
||||
@@ -30,13 +30,13 @@ const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center font-semibold mt-4 mb-2">
|
||||
<IconCertificate className="mr-1 certificate-icon" size={24} strokeWidth={1.5} /> Client Certificates
|
||||
</div>
|
||||
<StyledWrapper className="w-full h-full">
|
||||
<div className="text-xs mb-4 text-muted">Add client certificates to be used for specific domains.</div>
|
||||
|
||||
<h1 className="font-semibold">Client Certificates</h1>
|
||||
<ul className="mt-4">
|
||||
{!clientCertConfig.length
|
||||
? 'None'
|
||||
? 'No client certificates added'
|
||||
: clientCertConfig.map((clientCert) => (
|
||||
<li key={uuid()} className="flex items-center available-certificates p-2 rounded-lg mb-2">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
@@ -52,7 +52,7 @@ const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h1 className="font-semibold mt-8 mb-2">Add Client Certicate</h1>
|
||||
<h1 className="font-semibold mt-8 mb-2">Add Client Certificate</h1>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="domain">
|
||||
|
||||
@@ -63,7 +63,10 @@ const Headers = ({ collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Add request headers that will be sent with every request in this collection.
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -23,6 +23,7 @@ function countRequests(items) {
|
||||
const Info = ({ collection }) => {
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<div className="text-xs mb-4 text-muted">General information about the collection.</div>
|
||||
<table className="w-full border-collapse">
|
||||
<tbody>
|
||||
<tr className="">
|
||||
|
||||
@@ -27,8 +27,10 @@ const PresetsSettings = ({ collection }) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<h1 className="font-medium mb-3">Collection Presets</h1>
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
These presets will be used as the default values for new requests in this collection.
|
||||
</div>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
@@ -66,7 +68,7 @@ const PresetsSettings = ({ collection }) => {
|
||||
<label className="settings-label" htmlFor="requestUrl">
|
||||
Base URL
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center w-full">
|
||||
<div className="flex items-center flex-grow input-container h-full">
|
||||
<input
|
||||
id="request-url"
|
||||
@@ -79,6 +81,7 @@ const PresetsSettings = ({ collection }) => {
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.requestUrl || ''}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -95,8 +95,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
}, [proxyConfig]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<h1 className="font-medium mb-3">Proxy Settings</h1>
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">Configure proxy settings for this collection.</div>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
|
||||
@@ -38,7 +38,10 @@ const Script = ({ collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Write pre and post-request scripts that will run before and after any request in this collection is sent.
|
||||
</div>
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<CodeEditor
|
||||
|
||||
@@ -27,6 +27,7 @@ const Tests = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<div className="text-xs mb-4 text-muted">These tests will run any time a request in this collection is sent.</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={tests || ''}
|
||||
|
||||
@@ -147,9 +147,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
Info
|
||||
</div>
|
||||
</div>
|
||||
<section className={`flex ${['auth', 'script', 'docs', 'clientCert'].includes(tab) ? '' : 'mt-4'}`}>
|
||||
{getTabPanel(tab)}
|
||||
</section>
|
||||
<section className="mt-4 h-full">{getTabPanel(tab)}</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-enviroment {
|
||||
.current-environment {
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border-radius: 15px;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="current-enviroment flex items-center justify-center pl-3 pr-2 py-1 select-none">
|
||||
<div ref={ref} className="current-environment flex items-center justify-center pl-3 pr-2 py-1 select-none">
|
||||
{activeEnvironment ? activeEnvironment.name : 'No Environment'}
|
||||
<IconCaretDown className="caret" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { uuid } from 'utils/common';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
import { maskInputValue } from 'utils/collections';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -120,13 +121,17 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
collection={collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
/>
|
||||
{variable.secret ? (
|
||||
<div className="overflow-hidden text-ellipsis">{maskInputValue(variable.value)}</div>
|
||||
) : (
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
collection={collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
|
||||
@@ -84,8 +84,9 @@ const Notifications = () => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div
|
||||
className="relative"
|
||||
<a
|
||||
title="Notifications"
|
||||
className="relative cursor-pointer"
|
||||
onClick={() => {
|
||||
dispatch(fetchNotifications());
|
||||
setShowNotificationsModal(true);
|
||||
@@ -97,9 +98,9 @@ const Notifications = () => {
|
||||
className={`mr-2 hover:text-gray-700 ${unreadNotifications?.length > 0 ? 'bell' : ''}`}
|
||||
/>
|
||||
{unreadNotifications.length > 0 && (
|
||||
<div className="notification-count text-xs">{unreadNotifications.length}</div>
|
||||
<span className="notification-count text-xs">{unreadNotifications.length}</span>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{showNotificationsModal && (
|
||||
<Modal
|
||||
|
||||
@@ -21,6 +21,9 @@ const General = ({ close }) => {
|
||||
enabled: Yup.boolean(),
|
||||
filePath: Yup.string().nullable()
|
||||
}),
|
||||
keepDefaultCaCertificates: Yup.object({
|
||||
enabled: Yup.boolean()
|
||||
}),
|
||||
storeCookies: Yup.boolean(),
|
||||
sendCookies: Yup.boolean(),
|
||||
timeout: Yup.mixed()
|
||||
@@ -43,6 +46,9 @@ const General = ({ close }) => {
|
||||
enabled: get(preferences, 'request.customCaCertificate.enabled', false),
|
||||
filePath: get(preferences, 'request.customCaCertificate.filePath', null)
|
||||
},
|
||||
keepDefaultCaCertificates: {
|
||||
enabled: get(preferences, 'request.keepDefaultCaCertificates.enabled', false)
|
||||
},
|
||||
timeout: preferences.request.timeout,
|
||||
storeCookies: get(preferences, 'request.storeCookies', true),
|
||||
sendCookies: get(preferences, 'request.sendCookies', true)
|
||||
@@ -68,6 +74,9 @@ const General = ({ close }) => {
|
||||
enabled: newPreferences.customCaCertificate.enabled,
|
||||
filePath: newPreferences.customCaCertificate.filePath
|
||||
},
|
||||
keepDefaultCaCertificates: {
|
||||
enabled: newPreferences.keepDefaultCaCertificates.enabled
|
||||
},
|
||||
timeout: newPreferences.timeout,
|
||||
storeCookies: newPreferences.storeCookies,
|
||||
sendCookies: newPreferences.sendCookies
|
||||
@@ -158,6 +167,23 @@ const General = ({ close }) => {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="keepDefaultCaCertificatesEnabled"
|
||||
type="checkbox"
|
||||
name="keepDefaultCaCertificates.enabled"
|
||||
checked={formik.values.keepDefaultCaCertificates.enabled}
|
||||
onChange={formik.handleChange}
|
||||
className={`mousetrap mr-0 ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
|
||||
disabled={formik.values.customCaCertificate.enabled ? false : true}
|
||||
/>
|
||||
<label
|
||||
className={`block ml-2 select-none ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
|
||||
htmlFor="keepDefaultCaCertificatesEnabled"
|
||||
>
|
||||
Keep default CA Certificates
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="storeCookies"
|
||||
|
||||
@@ -37,7 +37,7 @@ const GrantTypeSelector = ({ item, collection }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// initalize redux state with a default oauth2 grant type
|
||||
// initialize redux state with a default oauth2 grant type
|
||||
// authorization_code - default option
|
||||
!oAuth?.grantType &&
|
||||
dispatch(
|
||||
|
||||
@@ -27,7 +27,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
const variables = item.draft
|
||||
? get(item, 'draft.request.body.graphql.variables')
|
||||
: get(item, 'request.body.graphql.variables');
|
||||
const { storedTheme } = useTheme();
|
||||
const { displayedTheme } = useTheme();
|
||||
const [schema, setSchema] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -61,7 +61,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
return (
|
||||
<QueryEditor
|
||||
collection={collection}
|
||||
theme={storedTheme}
|
||||
theme={displayedTheme}
|
||||
schema={schema}
|
||||
width={leftPaneWidth}
|
||||
onSave={onSave}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { humanizeRequestBodyMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import jsonBigint from 'json-bigint';
|
||||
import { parse, stringify } from 'lossless-json';
|
||||
|
||||
const RequestBodyMode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -38,8 +38,8 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
const onPrettify = () => {
|
||||
if (body?.json && bodyMode === 'json') {
|
||||
try {
|
||||
const bodyJson = jsonBigint.parse(body.json);
|
||||
const prettyBodyJson = jsonBigint.stringify(bodyJson, null, 2);
|
||||
const bodyJson = parse(body.json);
|
||||
const prettyBodyJson = stringify(bodyJson, null, 2);
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: prettyBodyJson,
|
||||
|
||||
@@ -65,6 +65,16 @@ const QueryResultPreview = ({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case 'preview-audio': {
|
||||
return (
|
||||
<audio controls src={`data:${contentType.replace(/\;(.*)/, '')};base64,${dataBuffer}`} className="mx-auto" />
|
||||
);
|
||||
}
|
||||
case 'preview-video': {
|
||||
return (
|
||||
<video controls src={`data:${contentType.replace(/\;(.*)/, '')};base64,${dataBuffer}`} className="mx-auto" />
|
||||
);
|
||||
}
|
||||
default:
|
||||
case 'raw': {
|
||||
return (
|
||||
|
||||
@@ -67,6 +67,10 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
allowedPreviewModes.unshift('preview-image');
|
||||
} else if (contentType.includes('pdf')) {
|
||||
allowedPreviewModes.unshift('preview-pdf');
|
||||
} else if (contentType.includes('audio')) {
|
||||
allowedPreviewModes.unshift('preview-audio');
|
||||
} else if (contentType.includes('video')) {
|
||||
allowedPreviewModes.unshift('preview-video');
|
||||
}
|
||||
|
||||
return allowedPreviewModes;
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function RunnerResults({ collection }) {
|
||||
|
||||
const autoScrollRunnerBody = () => {
|
||||
if (runnerBodyRef?.current) {
|
||||
// mimicks the native terminal scroll style
|
||||
// mimics the native terminal scroll style
|
||||
runnerBodyRef.current.scrollTo(0, 100000);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as Yup from 'yup';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isItemAFolder } from 'utils/tabs';
|
||||
import { renameItem } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -21,7 +21,12 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
onSubmit: async (values) => {
|
||||
// if there is unsaved changes in the request,
|
||||
// save them before renaming the request
|
||||
if (!isFolder && item.draft) {
|
||||
await dispatch(saveRequest(item.uid, collection.uid, true));
|
||||
}
|
||||
dispatch(renameItem(values.name, item.uid, collection.uid));
|
||||
onClose();
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@ import { useDrag, useDrop } from 'react-dnd';
|
||||
import { IconChevronRight, IconDots } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { moveItem, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import { moveItem } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
|
||||
@@ -99,7 +99,7 @@ const GoldenEdition = ({ onClose }) => {
|
||||
|
||||
const goldenEditonOrganizations = [
|
||||
'Centralized License Management',
|
||||
'Intergration with Secret Managers',
|
||||
'Integration with Secret Managers',
|
||||
'Private Collection Registry',
|
||||
'Request Forms',
|
||||
'Priority Support'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import importBrunoCollection from 'utils/importers/bruno-collection';
|
||||
import importPostmanCollection from 'utils/importers/postman-collection';
|
||||
import importInsomniaCollection from 'utils/importers/insomnia-collection';
|
||||
@@ -7,6 +7,14 @@ import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
const [options, setOptions] = useState({
|
||||
enablePostmanTranslations: {
|
||||
enabled: true,
|
||||
label: 'Auto translate postman scripts',
|
||||
subLabel:
|
||||
"When enabled, Bruno will try as best to translate the scripts from the imported collection to Bruno's format."
|
||||
}
|
||||
});
|
||||
const handleImportBrunoCollection = () => {
|
||||
importBrunoCollection()
|
||||
.then((collection) => {
|
||||
@@ -16,7 +24,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
};
|
||||
|
||||
const handleImportPostmanCollection = () => {
|
||||
importPostmanCollection()
|
||||
importPostmanCollection(options)
|
||||
.then((collection) => {
|
||||
handleSubmit(collection);
|
||||
})
|
||||
@@ -38,21 +46,61 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
|
||||
})
|
||||
.catch((err) => toastError(err, 'OpenAPI v3 Import collection failed'));
|
||||
};
|
||||
|
||||
const toggleOptions = (event, optionKey) => {
|
||||
setOptions({
|
||||
...options,
|
||||
[optionKey]: {
|
||||
...options[optionKey],
|
||||
enabled: !options[optionKey].enabled
|
||||
}
|
||||
});
|
||||
};
|
||||
const CollectionButton = ({ children, className, onClick }) => {
|
||||
return (
|
||||
<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}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<div>
|
||||
<div className="text-link hover:underline cursor-pointer" onClick={handleImportBrunoCollection}>
|
||||
Bruno Collection
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-sm">Select the type of your existing collection :</h3>
|
||||
<div className="mt-4 grid grid-rows-2 grid-flow-col gap-2">
|
||||
<CollectionButton onClick={handleImportBrunoCollection}>Bruno Collection</CollectionButton>
|
||||
<CollectionButton onClick={handleImportPostmanCollection}>Postman Collection</CollectionButton>
|
||||
<CollectionButton onClick={handleImportInsomniaCollection}>Insomnia Collection</CollectionButton>
|
||||
<CollectionButton onClick={handleImportOpenapiCollection}>OpenAPI V3 Spec</CollectionButton>
|
||||
</div>
|
||||
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportPostmanCollection}>
|
||||
Postman Collection
|
||||
</div>
|
||||
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportInsomniaCollection}>
|
||||
Insomnia Collection
|
||||
</div>
|
||||
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportOpenapiCollection}>
|
||||
OpenAPI V3 Spec
|
||||
<div className="flex justify-start w-full mt-4 max-w-[450px]">
|
||||
{Object.entries(options || {}).map(([key, option]) => (
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex h-6 items-center">
|
||||
<input
|
||||
id="comments"
|
||||
aria-describedby="comments-description"
|
||||
name="comments"
|
||||
type="checkbox"
|
||||
checked={option.enabled}
|
||||
onChange={(e) => toggleOptions(e, key)}
|
||||
className="h-3.5 w-3.5 rounded border-zinc-300 dark:ring-offset-zinc-800 bg-transparent text-indigo-600 dark:text-indigo-500 focus:ring-indigo-600 dark:focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2 text-sm leading-6">
|
||||
<label htmlFor="comments" className="font-medium text-gray-900 dark:text-zinc-50">
|
||||
{option.label}
|
||||
</label>
|
||||
<p id="comments-description" className="text-zinc-500 text-xs dark:text-zinc-400">
|
||||
{option.subLabel}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -93,26 +93,29 @@ const Sidebar = () => {
|
||||
<Collections />
|
||||
</div>
|
||||
|
||||
<div className="footer flex px-1 py-2 absolute bottom-0 left-0 right-0 items-center cursor-pointer select-none">
|
||||
<div className="footer flex px-1 py-2 absolute bottom-0 left-0 right-0 items-center select-none">
|
||||
<div className="flex items-center ml-1 text-xs ">
|
||||
<IconSettings
|
||||
size={18}
|
||||
strokeWidth={1.5}
|
||||
className="mr-2 hover:text-gray-700"
|
||||
<a
|
||||
title="Preferences"
|
||||
className="mr-2 cursor-pointer hover:text-gray-700"
|
||||
onClick={() => dispatch(showPreferences(true))}
|
||||
/>
|
||||
<IconCookie
|
||||
size={18}
|
||||
strokeWidth={1.5}
|
||||
className="mr-2 hover:text-gray-700"
|
||||
>
|
||||
<IconSettings size={18} strokeWidth={1.5} />
|
||||
</a>
|
||||
<a
|
||||
title="Cookies"
|
||||
className="mr-2 cursor-pointer hover:text-gray-700"
|
||||
onClick={() => setCookiesOpen(true)}
|
||||
/>
|
||||
<IconHeart
|
||||
size={18}
|
||||
strokeWidth={1.5}
|
||||
className="mr-2 hover:text-gray-700"
|
||||
>
|
||||
<IconCookie size={18} strokeWidth={1.5} />
|
||||
</a>
|
||||
<a
|
||||
title="Golden Edition"
|
||||
className="mr-2 cursor-pointer hover:text-gray-700"
|
||||
onClick={() => setGoldenEditonOpen(true)}
|
||||
/>
|
||||
>
|
||||
<IconHeart size={18} strokeWidth={1.5} />
|
||||
</a>
|
||||
<Notifications />
|
||||
</div>
|
||||
<div className="pl-1" style={{ position: 'relative', top: '3px' }}>
|
||||
@@ -126,7 +129,7 @@ const Sidebar = () => {
|
||||
Star
|
||||
</GitHubButton> */}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.11.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.12.3</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,6 +49,10 @@ const StyledWrapper = styled.div`
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-selected {
|
||||
background-color: rgba(212, 125, 59, 0.3);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import filter from 'lodash/filter';
|
||||
import { Inspector } from 'react-inspector';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import { findEnvironmentInCollection, maskInputValue } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons';
|
||||
|
||||
const KeyValueExplorer = ({ data, theme }) => {
|
||||
data = data || {};
|
||||
const KeyValueExplorer = ({ data = [], theme }) => {
|
||||
const [showSecret, setShowSecret] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SecretToggle showSecret={showSecret} onClick={() => setShowSecret(!showSecret)} />
|
||||
<table className="border-collapse">
|
||||
<tbody>
|
||||
{Object.entries(data).map(([key, value]) => (
|
||||
<tr key={key}>
|
||||
<td className="px-2 py-1">{key}</td>
|
||||
{data.map((envVar) => (
|
||||
<tr key={envVar.name}>
|
||||
<td className="px-2 py-1">{envVar.name}</td>
|
||||
<td className="px-2 py-1">
|
||||
<Inspector data={value} theme={theme} />
|
||||
<Inspector
|
||||
data={!showSecret && envVar.secret ? maskInputValue(envVar.value) : envVar.value}
|
||||
theme={theme}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -41,10 +46,6 @@ const EnvVariables = ({ collection, theme }) => {
|
||||
|
||||
const envVars = get(environment, 'variables', []);
|
||||
const enabledEnvVars = filter(envVars, (variable) => variable.enabled);
|
||||
const envVarsObj = enabledEnvVars.reduce((acc, curr) => {
|
||||
acc[curr.name] = curr.value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -53,7 +54,7 @@ const EnvVariables = ({ collection, theme }) => {
|
||||
<span className="muted ml-2">({environment.name})</span>
|
||||
</div>
|
||||
{enabledEnvVars.length > 0 ? (
|
||||
<KeyValueExplorer data={envVarsObj} theme={theme} />
|
||||
<KeyValueExplorer data={enabledEnvVars} theme={theme} />
|
||||
) : (
|
||||
<div className="muted text-xs">No environment variables found</div>
|
||||
)}
|
||||
@@ -64,11 +65,17 @@ const EnvVariables = ({ collection, theme }) => {
|
||||
const CollectionVariables = ({ collection, theme }) => {
|
||||
const collectionVariablesFound = Object.keys(collection.collectionVariables).length > 0;
|
||||
|
||||
const collectionVariableArray = Object.entries(collection.collectionVariables).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
secret: false
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="font-semibold mb-2">Collection Variables</h1>
|
||||
{collectionVariablesFound ? (
|
||||
<KeyValueExplorer data={collection.collectionVariables} theme={theme} />
|
||||
<KeyValueExplorer data={collectionVariableArray} theme={theme} />
|
||||
) : (
|
||||
<div className="muted text-xs">No collection variables found</div>
|
||||
)}
|
||||
@@ -96,3 +103,12 @@ const VariablesEditor = ({ collection }) => {
|
||||
};
|
||||
|
||||
export default VariablesEditor;
|
||||
|
||||
const SecretToggle = ({ showSecret, onClick }) => (
|
||||
<div className="cursor-pointer mb-2 text-xs" onClick={onClick}>
|
||||
<div className="flex items-center">
|
||||
{showSecret ? <IconEyeOff size={16} strokeWidth={1.5} /> : <IconEye size={16} strokeWidth={1.5} />}
|
||||
<span className="pl-1">{showSecret ? 'Hide secret variable values' : 'Show secret variable values'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,9 @@ const GlobalStyle = createGlobalStyle`
|
||||
.text-link {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
.text-muted {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.btn {
|
||||
text-align: center;
|
||||
|
||||
@@ -60,7 +60,7 @@ const trackStart = () => {
|
||||
event: 'start',
|
||||
properties: {
|
||||
os: platformLib.os.family,
|
||||
version: '1.11.0'
|
||||
version: '1.12.3'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,6 +17,9 @@ const initialState = {
|
||||
enabled: false,
|
||||
filePath: null
|
||||
},
|
||||
keepDefaultCaCertificates: {
|
||||
enabled: false
|
||||
},
|
||||
timeout: 0
|
||||
},
|
||||
font: {
|
||||
|
||||
@@ -55,7 +55,7 @@ export const renameCollection = (newName, collectionUid) => (dispatch, getState)
|
||||
});
|
||||
};
|
||||
|
||||
export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||
export const saveRequest = (itemUid, collectionUid, saveSilently) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
|
||||
@@ -76,7 +76,11 @@ export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||
itemSchema
|
||||
.validate(itemToSave)
|
||||
.then(() => ipcRenderer.invoke('renderer:save-request', item.pathname, itemToSave))
|
||||
.then(() => toast.success('Request saved successfully'))
|
||||
.then(() => {
|
||||
if (!saveSilently) {
|
||||
toast.success('Request saved successfully');
|
||||
}
|
||||
})
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error('Failed to save request!');
|
||||
@@ -414,6 +418,15 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
||||
.then(() => ipcRenderer.invoke('renderer:new-request', fullName, itemToSave))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
|
||||
dispatch(
|
||||
insertTaskIntoQueue({
|
||||
uid: uuid(),
|
||||
type: 'OPEN_REQUEST',
|
||||
collectionUid,
|
||||
itemPathname: fullName
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||
}
|
||||
@@ -434,6 +447,15 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
||||
.then(() => ipcRenderer.invoke('renderer:new-request', fullName, itemToSave))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
|
||||
dispatch(
|
||||
insertTaskIntoQueue({
|
||||
uid: uuid(),
|
||||
type: 'OPEN_REQUEST',
|
||||
collectionUid,
|
||||
itemPathname: fullName
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return reject(new Error('Duplicate request names are not allowed under the same folder'));
|
||||
}
|
||||
|
||||
@@ -360,7 +360,7 @@ export const collectionsSlice = createSlice({
|
||||
urlParam.uid = existingParam ? existingParam.uid : uuid();
|
||||
urlParam.enabled = true;
|
||||
|
||||
// once found, remove it - trying our best here to accomodate duplicate query params
|
||||
// once found, remove it - trying our best here to accommodate duplicate query params
|
||||
if (existingParam) {
|
||||
enabledParams = filter(enabledParams, (p) => p.uid !== existingParam.uid);
|
||||
}
|
||||
|
||||
@@ -9,20 +9,31 @@ export const ThemeProvider = (props) => {
|
||||
const isBrowserThemeLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
const [displayedTheme, setDisplayedTheme] = useState(isBrowserThemeLight ? 'light' : 'dark');
|
||||
const [storedTheme, setStoredTheme] = useLocalStorage('bruno.theme', 'system');
|
||||
const toggleHtml = () => {
|
||||
const html = document.querySelector('html');
|
||||
if (html) {
|
||||
html.classList.toggle('dark');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
|
||||
if (storedTheme !== 'system') return;
|
||||
setDisplayedTheme(e.matches ? 'light' : 'dark');
|
||||
toggleHtml();
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
root.classList.remove('light', 'dark');
|
||||
if (storedTheme === 'system') {
|
||||
const isBrowserThemeLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
setDisplayedTheme(isBrowserThemeLight ? 'light' : 'dark');
|
||||
root.classList.add(isBrowserThemeLight ? 'light' : 'dark');
|
||||
} else {
|
||||
setDisplayedTheme(storedTheme);
|
||||
root.classList.add(storedTheme);
|
||||
}
|
||||
}, [storedTheme, setDisplayedTheme, window.matchMedia]);
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ body::-webkit-scrollbar-thumb,
|
||||
border-radius: 5rem;
|
||||
}
|
||||
|
||||
/*
|
||||
/*
|
||||
* todo: this will be supported in the future to be changed via applying a theme
|
||||
* making all the checkboxes and radios bigger
|
||||
* input[type='checkbox'],
|
||||
|
||||
@@ -8,7 +8,7 @@ const lightTheme = {
|
||||
text: {
|
||||
green: '#047857',
|
||||
danger: 'rgb(185, 28, 28)',
|
||||
muted: '#4b5563',
|
||||
muted: '#838383',
|
||||
purple: '#8e44ad',
|
||||
yellow: '#d97706'
|
||||
},
|
||||
|
||||
@@ -639,3 +639,14 @@ export const getAllVariables = (collection) => {
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const maskInputValue = (value) => {
|
||||
if (!value || typeof value !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return value
|
||||
.split('')
|
||||
.map(() => '*')
|
||||
.join('');
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ const parseCurlCommand = (curlCommand) => {
|
||||
curlCommand = curlCommand.trim();
|
||||
|
||||
const parsedArguments = yargs(curlCommand, {
|
||||
boolean: ['I', 'head', 'compressed', 'L', 'k', 'silent', 's'],
|
||||
boolean: ['I', 'head', 'compressed', 'L', 'k', 'silent', 's', 'G', 'get'],
|
||||
alias: {
|
||||
H: 'header',
|
||||
A: 'user-agent'
|
||||
@@ -153,7 +153,10 @@ const parseCurlCommand = (curlCommand) => {
|
||||
// NB: the -G flag does not change the http verb. It just moves the data into the url.
|
||||
if (parsedArguments.G || parsedArguments.get) {
|
||||
urlObject.query = urlObject.query ? urlObject.query : '';
|
||||
const option = 'd' in parsedArguments ? 'd' : 'data' in parsedArguments ? 'data' : null;
|
||||
let option = null;
|
||||
if ('d' in parsedArguments) option = 'd';
|
||||
if ('data' in parsedArguments) option = 'data';
|
||||
if ('data-urlencode' in parsedArguments) option = 'data-urlencode';
|
||||
if (option) {
|
||||
let urlQueryString = '';
|
||||
|
||||
@@ -219,6 +222,8 @@ const parseCurlCommand = (curlCommand) => {
|
||||
} else if (parsedArguments['data-raw']) {
|
||||
request.data = parsedArguments['data-raw'];
|
||||
request.isDataRaw = true;
|
||||
} else if (parsedArguments['data-urlencode']) {
|
||||
request.data = parsedArguments['data-urlencode'];
|
||||
}
|
||||
|
||||
if (parsedArguments.u) {
|
||||
|
||||
@@ -49,7 +49,7 @@ export const exportCollection = (collection) => {
|
||||
|
||||
const generateEventSection = (item) => {
|
||||
const eventArray = [];
|
||||
if (item.request.tests.length) {
|
||||
if (item?.request?.tests?.length) {
|
||||
eventArray.push({
|
||||
listen: 'test',
|
||||
script: {
|
||||
@@ -58,7 +58,7 @@ export const exportCollection = (collection) => {
|
||||
}
|
||||
});
|
||||
}
|
||||
if (item.request.script.req) {
|
||||
if (item?.request?.script?.req) {
|
||||
eventArray.push({
|
||||
listen: 'prerequest',
|
||||
script: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import fileDialog from 'file-dialog';
|
||||
import { uuid } from 'utils/common';
|
||||
import { BrunoError } from 'utils/common/error';
|
||||
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
|
||||
import { postmanTranslation } from 'utils/importers/translators/postman_translation';
|
||||
|
||||
const readFile = (files) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -53,7 +54,7 @@ const convertV21Auth = (array) => {
|
||||
}, {});
|
||||
};
|
||||
|
||||
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => {
|
||||
brunoParent.items = brunoParent.items || [];
|
||||
const folderMap = {};
|
||||
|
||||
@@ -77,7 +78,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
brunoParent.items.push(brunoFolderItem);
|
||||
folderMap[folderName] = brunoFolderItem;
|
||||
if (i.item && i.item.length) {
|
||||
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth);
|
||||
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth, options);
|
||||
}
|
||||
} else {
|
||||
if (i.request) {
|
||||
@@ -121,9 +122,13 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
brunoRequestItem.request.script = {};
|
||||
}
|
||||
if (Array.isArray(event.script.exec)) {
|
||||
brunoRequestItem.request.script.req = event.script.exec.map((line) => `// ${line}`).join('\n');
|
||||
brunoRequestItem.request.script.req = event.script.exec
|
||||
.map((line) => (options.enablePostmanTranslations.enabled ? postmanTranslation(line) : `// ${line}`))
|
||||
.join('\n');
|
||||
} else {
|
||||
brunoRequestItem.request.script.req = `// ${event.script.exec[0]} `;
|
||||
brunoRequestItem.request.script.req = options.enablePostmanTranslations.enabled
|
||||
? postmanTranslation(event.script.exec[0])
|
||||
: `// ${event.script.exec[0]} `;
|
||||
}
|
||||
}
|
||||
if (event.listen === 'test' && event.script && event.script.exec) {
|
||||
@@ -131,9 +136,13 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
brunoRequestItem.request.tests = {};
|
||||
}
|
||||
if (Array.isArray(event.script.exec)) {
|
||||
brunoRequestItem.request.tests = event.script.exec.map((line) => `// ${line}`).join('\n');
|
||||
brunoRequestItem.request.tests = event.script.exec
|
||||
.map((line) => (options.enablePostmanTranslations.enabled ? postmanTranslation(line) : `// ${line}`))
|
||||
.join('\n');
|
||||
} else {
|
||||
brunoRequestItem.request.tests = `// ${event.script.exec[0]} `;
|
||||
brunoRequestItem.request.tests = options.enablePostmanTranslations.enabled
|
||||
? postmanTranslation(event.script.exec[0])
|
||||
: `// ${event.script.exec[0]} `;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -263,7 +272,7 @@ const searchLanguageByHeader = (headers) => {
|
||||
return contentType;
|
||||
};
|
||||
|
||||
const importPostmanV2Collection = (collection) => {
|
||||
const importPostmanV2Collection = (collection, options) => {
|
||||
const brunoCollection = {
|
||||
name: collection.info.name,
|
||||
uid: uuid(),
|
||||
@@ -272,12 +281,12 @@ const importPostmanV2Collection = (collection) => {
|
||||
environments: []
|
||||
};
|
||||
|
||||
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth);
|
||||
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth, options);
|
||||
|
||||
return brunoCollection;
|
||||
};
|
||||
|
||||
const parsePostmanCollection = (str) => {
|
||||
const parsePostmanCollection = (str, options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
let collection = JSON.parse(str);
|
||||
@@ -289,7 +298,7 @@ const parsePostmanCollection = (str) => {
|
||||
];
|
||||
|
||||
if (v2Schemas.includes(schema)) {
|
||||
return resolve(importPostmanV2Collection(collection));
|
||||
return resolve(importPostmanV2Collection(collection, options));
|
||||
}
|
||||
|
||||
throw new BrunoError('Unknown postman schema');
|
||||
@@ -304,11 +313,11 @@ const parsePostmanCollection = (str) => {
|
||||
});
|
||||
};
|
||||
|
||||
const importCollection = () => {
|
||||
const importCollection = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileDialog({ accept: 'application/json' })
|
||||
.then(readFile)
|
||||
.then(parsePostmanCollection)
|
||||
.then((str) => parsePostmanCollection(str, options))
|
||||
.then(transformItemsInCollection)
|
||||
.then(hydrateSeqInCollection)
|
||||
.then(validateSchema)
|
||||
|
||||
134
packages/bruno-app/src/utils/importers/translators/index.spec.js
Normal file
134
packages/bruno-app/src/utils/importers/translators/index.spec.js
Normal file
@@ -0,0 +1,134 @@
|
||||
const { postmanTranslation } = require('./postman_translation'); // Adjust path as needed
|
||||
|
||||
describe('postmanTranslation function', () => {
|
||||
test('should translate pm commands correctly', () => {
|
||||
const inputScript = `
|
||||
pm.environment.get('key');
|
||||
pm.environment.set('key', 'value');
|
||||
pm.variables.get('key');
|
||||
pm.variables.set('key', 'value');
|
||||
pm.collectionVariables.get('key');
|
||||
pm.collectionVariables.set('key', 'value');
|
||||
`;
|
||||
const expectedOutput = `
|
||||
bru.getEnvVar('key');
|
||||
bru.setEnvVar('key', 'value');
|
||||
bru.getVar('key');
|
||||
bru.setVar('key', 'value');
|
||||
bru.getVar('key');
|
||||
bru.setVar('key', 'value');
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should not translate non-pm commands', () => {
|
||||
const inputScript = `
|
||||
console.log('This script does not contain pm commands.');
|
||||
const data = pm.environment.get('key');
|
||||
pm.collectionVariables.set('key', data);
|
||||
`;
|
||||
const expectedOutput = `
|
||||
console.log('This script does not contain pm commands.');
|
||||
const data = bru.getEnvVar('key');
|
||||
bru.setVar('key', data);
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should comment non-translated pm commands', () => {
|
||||
const inputScript = "pm.test('random test', () => pm.response.json());";
|
||||
const expectedOutput = "// test('random test', () => pm.response.json());";
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
test('should handle multiple pm commands on the same line', () => {
|
||||
const inputScript = "pm.environment.get('key'); pm.environment.set('key', 'value');";
|
||||
const expectedOutput = "bru.getEnv(var); bru.setEnvVar(var, 'value');";
|
||||
});
|
||||
test('should handle comments and other JavaScript code', () => {
|
||||
const inputScript = `
|
||||
// This is a comment
|
||||
const value = 'test';
|
||||
pm.environment.set('key', value);
|
||||
/*
|
||||
Multi-line comment
|
||||
*/
|
||||
const result = pm.environment.get('key');
|
||||
console.log('Result:', result);
|
||||
`;
|
||||
const expectedOutput = `
|
||||
// This is a comment
|
||||
const value = 'test';
|
||||
bru.setEnvVar('key', value);
|
||||
/*
|
||||
Multi-line comment
|
||||
*/
|
||||
const result = bru.getEnvVar('key');
|
||||
console.log('Result:', result);
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should handle nested commands and edge cases', () => {
|
||||
const inputScript = `
|
||||
const sampleObjects = [
|
||||
{
|
||||
key: pm.environment.get('key'),
|
||||
value: pm.variables.get('value')
|
||||
},
|
||||
{
|
||||
key: pm.collectionVariables.get('key'),
|
||||
value: pm.collectionVariables.get('value')
|
||||
}
|
||||
];
|
||||
const dataTesting = Object.entries(sampleObjects || {}).reduce((acc, [key, value]) => {
|
||||
// this is a comment
|
||||
acc[key] = pm.collectionVariables.get(pm.environment.get(value));
|
||||
return acc; // Return the accumulator
|
||||
}, {});
|
||||
Object.values(dataTesting).forEach((data) => {
|
||||
pm.environment.set(data.key, pm.variables.get(data.value));
|
||||
});
|
||||
`;
|
||||
const expectedOutput = `
|
||||
const sampleObjects = [
|
||||
{
|
||||
key: bru.getEnvVar('key'),
|
||||
value: bru.getVar('value')
|
||||
},
|
||||
{
|
||||
key: bru.getVar('key'),
|
||||
value: bru.getVar('value')
|
||||
}
|
||||
];
|
||||
const dataTesting = Object.entries(sampleObjects || {}).reduce((acc, [key, value]) => {
|
||||
// this is a comment
|
||||
acc[key] = bru.getVar(bru.getEnvVar(value));
|
||||
return acc; // Return the accumulator
|
||||
}, {});
|
||||
Object.values(dataTesting).forEach((data) => {
|
||||
bru.setEnvVar(data.key, bru.getVar(data.value));
|
||||
});
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
test('should handle test commands', () => {
|
||||
const inputScript = `
|
||||
pm.test('Status code is 200', () => {
|
||||
pm.response.to.have.status(200);
|
||||
});
|
||||
pm.test('this test will fail', () => {
|
||||
return false
|
||||
});
|
||||
`;
|
||||
const expectedOutput = `
|
||||
test('Status code is 200', () => {
|
||||
expect(res.getStatus()).to.equal(200);
|
||||
});
|
||||
test('this test will fail', () => {
|
||||
return false
|
||||
});
|
||||
`;
|
||||
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
const replacements = {
|
||||
'pm\\.environment\\.get\\(': 'bru.getEnvVar(',
|
||||
'pm\\.environment\\.set\\(': 'bru.setEnvVar(',
|
||||
'pm\\.variables\\.get\\(': 'bru.getVar(',
|
||||
'pm\\.variables\\.set\\(': 'bru.setVar(',
|
||||
'pm\\.collectionVariables\\.get\\(': 'bru.getVar(',
|
||||
'pm\\.collectionVariables\\.set\\(': 'bru.setVar(',
|
||||
'pm\\.setNextRequest\\(': 'bru.setNextRequest(',
|
||||
'pm\\.test\\(': 'test(',
|
||||
'pm.response.to.have\\.status\\(': 'expect(res.getStatus()).to.equal('
|
||||
};
|
||||
|
||||
const compiledReplacements = Object.entries(replacements).map(([pattern, replacement]) => ({
|
||||
regex: new RegExp(pattern, 'g'),
|
||||
replacement
|
||||
}));
|
||||
|
||||
export const postmanTranslation = (script) => {
|
||||
try {
|
||||
let modifiedScript = script;
|
||||
let modified = false;
|
||||
for (const { regex, replacement } of compiledReplacements) {
|
||||
if (regex.test(modifiedScript)) {
|
||||
modifiedScript = modifiedScript.replace(regex, replacement);
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
if (modified && modifiedScript.includes('pm.')) {
|
||||
modifiedScript = modifiedScript.replace(/^(.*pm\..*)$/gm, '// $1');
|
||||
}
|
||||
return modifiedScript;
|
||||
} catch (e) {
|
||||
return script;
|
||||
}
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
|
||||
darkMode: ['class'],
|
||||
content: ['./src/**/*.{js,jsx}'],
|
||||
prefix: '',
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/cli",
|
||||
"version": "1.9.2",
|
||||
"version": "1.11.0",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
@@ -26,8 +26,8 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.525.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/js": "0.10.1",
|
||||
"@usebruno/lang": "0.10.0",
|
||||
"@usebruno/js": "0.11.0",
|
||||
"@usebruno/lang": "0.11.0",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "^1.5.1",
|
||||
"chai": "^4.3.7",
|
||||
|
||||
@@ -240,7 +240,7 @@ const builder = async (yargs) => {
|
||||
'$0 run request.bru --output results.html --format html',
|
||||
'Run a request and write the results to results.html in html format in the current directory'
|
||||
)
|
||||
.example('$0 run request.bru --test-only', 'Run all requests that have a test');
|
||||
.example('$0 run request.bru --tests-only', 'Run all requests that have a test');
|
||||
};
|
||||
|
||||
const handler = async function (argv) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "v1.11.0",
|
||||
"version": "v1.12.3",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
@@ -21,9 +21,9 @@
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.525.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/js": "0.10.1",
|
||||
"@usebruno/lang": "0.10.0",
|
||||
"@usebruno/schema": "0.6.0",
|
||||
"@usebruno/js": "0.11.0",
|
||||
"@usebruno/lang": "0.11.0",
|
||||
"@usebruno/schema": "0.7.0",
|
||||
"about-window": "^1.15.2",
|
||||
"aws4-axios": "^3.3.0",
|
||||
"axios": "^1.5.1",
|
||||
|
||||
@@ -25,6 +25,7 @@ const contentSecurityPolicy = [
|
||||
// this has been commented out to make oauth2 work
|
||||
// "form-action 'none'",
|
||||
"img-src 'self' blob: data: https:",
|
||||
"media-src 'self' blob: data: https:",
|
||||
"style-src 'self' 'unsafe-inline' https:"
|
||||
];
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
|
||||
|
||||
function onWindowRedirect(url) {
|
||||
// check if the url contains an authorization code
|
||||
if (url.match(/(code=).*/)) {
|
||||
if (new URL(url).searchParams.has('code')) {
|
||||
finalUrl = url;
|
||||
if (!url || !finalUrl.includes(callbackUrl)) {
|
||||
reject(new Error('Invalid Callback Url'));
|
||||
@@ -75,6 +75,11 @@ const authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session }) => {
|
||||
try {
|
||||
await window.loadURL(authorizeUrl);
|
||||
} catch (error) {
|
||||
// If browser redirects before load finished, loadURL throws an error with code ERR_ABORTED. This should be ignored.
|
||||
if (error.code === 'ERR_ABORTED') {
|
||||
console.debug('Ignoring ERR_ABORTED during authorizeUserInWindow');
|
||||
return;
|
||||
}
|
||||
reject(error);
|
||||
window.close();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ const os = require('os');
|
||||
const fs = require('fs');
|
||||
const qs = require('qs');
|
||||
const https = require('https');
|
||||
const tls = require('tls');
|
||||
const axios = require('axios');
|
||||
const path = require('path');
|
||||
const decomment = require('decomment');
|
||||
@@ -105,7 +106,11 @@ const configureRequest = async (
|
||||
if (preferencesUtil.shouldUseCustomCaCertificate()) {
|
||||
const caCertFilePath = preferencesUtil.getCustomCaCertificateFilePath();
|
||||
if (caCertFilePath) {
|
||||
httpsAgentRequestFields['ca'] = fs.readFileSync(caCertFilePath);
|
||||
let caCertBuffer = fs.readFileSync(caCertFilePath);
|
||||
if (preferencesUtil.shouldKeepDefaultCaCertificates()) {
|
||||
caCertBuffer += '\n' + tls.rootCertificates.join('\n'); // Augment default truststore with custom CA certificates
|
||||
}
|
||||
httpsAgentRequestFields['ca'] = caCertBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +208,7 @@ const configureRequest = async (
|
||||
interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars);
|
||||
const { data: authorizationCodeData, url: authorizationCodeAccessTokenUrl } =
|
||||
await resolveOAuth2AuthorizationCodeAccessToken(requestCopy, collectionUid);
|
||||
request.method = 'POST';
|
||||
request.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
request.data = authorizationCodeData;
|
||||
request.url = authorizationCodeAccessTokenUrl;
|
||||
@@ -211,6 +217,8 @@ const configureRequest = async (
|
||||
interpolateVars(requestCopy, envVars, collectionVariables, processEnvVars);
|
||||
const { data: clientCredentialsData, url: clientCredentialsAccessTokenUrl } =
|
||||
await transformClientCredentialsRequest(requestCopy);
|
||||
request.method = 'POST';
|
||||
request.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||
request.data = clientCredentialsData;
|
||||
request.url = clientCredentialsAccessTokenUrl;
|
||||
break;
|
||||
@@ -219,6 +227,7 @@ const configureRequest = async (
|
||||
const { data: passwordData, url: passwordAccessTokenUrl } = await transformPasswordCredentialsRequest(
|
||||
requestCopy
|
||||
);
|
||||
request.method = 'POST';
|
||||
request.data = passwordData;
|
||||
request.url = passwordAccessTokenUrl;
|
||||
break;
|
||||
@@ -251,7 +260,7 @@ const parseDataFromResponse = (response) => {
|
||||
const dataBuffer = Buffer.from(response.data);
|
||||
// Parse the charset from content type: https://stackoverflow.com/a/33192813
|
||||
const charset = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['Content-Type'] || '');
|
||||
// Overwrite the original data for backwards compatability
|
||||
// Overwrite the original data for backwards compatibility
|
||||
let data = dataBuffer.toString(charset || 'utf-8');
|
||||
// Try to parse response to JSON, this can quietly fail
|
||||
try {
|
||||
@@ -913,7 +922,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
);
|
||||
|
||||
timeStart = Date.now();
|
||||
let response;
|
||||
let response, responseTime;
|
||||
try {
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
response = await axiosInstance(request);
|
||||
@@ -921,6 +930,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
const { data, dataBuffer } = parseDataFromResponse(response);
|
||||
response.data = data;
|
||||
response.responseTime = response.headers.get('request-duration');
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'response-received',
|
||||
@@ -931,7 +941,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
duration: timeEnd - timeStart,
|
||||
dataBuffer: dataBuffer.toString('base64'),
|
||||
size: Buffer.byteLength(dataBuffer),
|
||||
data: response.data
|
||||
data: response.data,
|
||||
responseTime: response.headers.get('request-duration')
|
||||
},
|
||||
...eventData
|
||||
});
|
||||
@@ -948,7 +959,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
duration: timeEnd - timeStart,
|
||||
dataBuffer: dataBuffer.toString('base64'),
|
||||
size: Buffer.byteLength(dataBuffer),
|
||||
data: error.response.data
|
||||
data: error.response.data,
|
||||
responseTime: error.response.headers.get('request-duration')
|
||||
};
|
||||
|
||||
// if we get a response from the server, we consider it as a success
|
||||
|
||||
@@ -4,7 +4,7 @@ const { authorizeUserInWindow } = require('./authorize-user-in-window');
|
||||
const Oauth2Store = require('../../store/oauth2');
|
||||
|
||||
const generateCodeVerifier = () => {
|
||||
return crypto.randomBytes(16).toString('hex');
|
||||
return crypto.randomBytes(22).toString('hex');
|
||||
};
|
||||
|
||||
const generateCodeChallenge = (codeVerifier) => {
|
||||
@@ -49,8 +49,13 @@ const getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {
|
||||
const { callbackUrl, clientId, authorizationUrl, scope, pkce } = oauth2;
|
||||
|
||||
let oauth2QueryParams =
|
||||
(authorizationUrl.indexOf('?') > -1 ? '&' : '?') +
|
||||
`client_id=${clientId}&redirect_uri=${callbackUrl}&response_type=code&scope=${scope}`;
|
||||
(authorizationUrl.indexOf('?') > -1 ? '&' : '?') + `client_id=${clientId}&response_type=code`;
|
||||
if (callbackUrl) {
|
||||
oauth2QueryParams += `&redirect_uri=${callbackUrl}`;
|
||||
}
|
||||
if (scope) {
|
||||
oauth2QueryParams += `&scope=${scope}`;
|
||||
}
|
||||
if (pkce) {
|
||||
oauth2QueryParams += `&code_challenge=${codeChallenge}&code_challenge_method=S256`;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ const defaultPreferences = {
|
||||
enabled: false,
|
||||
filePath: null
|
||||
},
|
||||
keepDefaultCaCertificates: {
|
||||
enabled: false
|
||||
},
|
||||
storeCookies: true,
|
||||
sendCookies: true,
|
||||
timeout: 0
|
||||
@@ -43,6 +46,9 @@ const preferencesSchema = Yup.object().shape({
|
||||
enabled: Yup.boolean(),
|
||||
filePath: Yup.string().nullable()
|
||||
}),
|
||||
keepDefaultCaCertificates: Yup.object({
|
||||
enabled: Yup.boolean()
|
||||
}),
|
||||
storeCookies: Yup.boolean(),
|
||||
sendCookies: Yup.boolean(),
|
||||
timeout: Yup.number()
|
||||
@@ -111,6 +117,9 @@ const preferencesUtil = {
|
||||
shouldUseCustomCaCertificate: () => {
|
||||
return get(getPreferences(), 'request.customCaCertificate.enabled', false);
|
||||
},
|
||||
shouldKeepDefaultCaCertificates: () => {
|
||||
return get(getPreferences(), 'request.keepDefaultCaCertificates.enabled', false);
|
||||
},
|
||||
getCustomCaCertificateFilePath: () => {
|
||||
return get(getPreferences(), 'request.customCaCertificate.filePath', null);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/js",
|
||||
"version": "0.10.1",
|
||||
"version": "0.11.0",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/lang",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.0",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/schema",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
|
||||
@@ -26,6 +26,7 @@ vars:pre-request {
|
||||
|
||||
assert {
|
||||
res.status: eq 200
|
||||
res.responseTime: lte 2000
|
||||
~res.body: eq {{pong}}
|
||||
}
|
||||
|
||||
|
||||
12
readme.md
12
readme.md
@@ -31,7 +31,7 @@ Bruno is offline-only. There are no plans to add cloud-sync to Bruno, ever. We v
|
||||
Majority of our features are free and open source.
|
||||
We strive to strike a harmonious balance between [open-source principles and sustainability](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
You can Pre-Orders the [Golden Edition](https://www.usebruno.com/pricing) at ~~$19~~ **$9** ! <br/>
|
||||
You can buy the [Golden Edition](https://www.usebruno.com/pricing) for a one-time payment of **$19** ! <br/>
|
||||
|
||||
### Installation
|
||||
|
||||
@@ -89,6 +89,12 @@ Or any version control system of your choice
|
||||
|
||||
<img src="assets/images/sponsors/commit-company.png" width="70"/>
|
||||
|
||||
#### Bronze Sponsors
|
||||
|
||||
<a href="https://zuplo.link/bruno">
|
||||
<img src="assets/images/sponsors/zuplo.png" width="120"/>
|
||||
</a>
|
||||
|
||||
### Important Links 📌
|
||||
|
||||
- [Our Long Term Vision](https://github.com/usebruno/bruno/discussions/269)
|
||||
@@ -98,7 +104,7 @@ Or any version control system of your choice
|
||||
- [Website](https://www.usebruno.com)
|
||||
- [Pricing](https://www.usebruno.com/pricing)
|
||||
- [Download](https://www.usebruno.com/downloads)
|
||||
- [Github Sponsors](https://github.com/sponsors/helloanoop).
|
||||
- [GitHub Sponsors](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### Showcase 🎥
|
||||
|
||||
@@ -108,7 +114,7 @@ Or any version control system of your choice
|
||||
|
||||
### Support ❤️
|
||||
|
||||
If you like Bruno and want to support our opensource work, consider sponsoring us via [Github Sponsors](https://github.com/sponsors/helloanoop).
|
||||
If you like Bruno and want to support our opensource work, consider sponsoring us via [GitHub Sponsors](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### Share Testimonials 📣
|
||||
|
||||
|
||||
Reference in New Issue
Block a user