Merge branch 'main' into feature/variable-name-validation

This commit is contained in:
Its-treason 2023-10-25 21:00:18 +02:00
commit 3a5a213242
64 changed files with 1600 additions and 484 deletions

View File

@ -11,3 +11,7 @@
- [ ] **Create an issue and link to the pull request.** - [ ] **Create an issue and link to the pull request.**
Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests. Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.
### Publishing to New Package Managers
Please see [here](../publishing.md) for more information.

View File

@ -1,12 +1,12 @@
**English** | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) **English** | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md) | [Français](/contributing_fr.md)
## Lets make bruno better, together !! ## Let's make bruno better, together !!
I am happy that you are looking to improve bruno. Below are the guidelines to get started bringing up bruno on your computer. I am happy that you are looking to improve bruno. Below are the guidelines to get started bringing up bruno on your computer.
### Technology Stack ### Technology Stack
Bruno is built using NextJs and React. We also use electron to ship a desktop version (that supports local collections) Bruno is built using Next.js and React. We also use electron to ship a desktop version (that supports local collections)
Libraries we use Libraries we use
@ -23,7 +23,7 @@ Libraries we use
You would need [Node v18.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project You would need [Node v18.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
### Lets start coding ### Let's start coding
Please reference [development.md](docs/development.md) for instructions on running the local development environment. Please reference [development.md](docs/development.md) for instructions on running the local development environment.
@ -33,5 +33,5 @@ Please reference [development.md](docs/development.md) for instructions on runni
- Please follow the format of creating branches - Please follow the format of creating branches
- feature/[feature name]: This branch should contain changes for a specific feature - feature/[feature name]: This branch should contain changes for a specific feature
- Example: feature/dark-mode - Example: feature/dark-mode
- bugfix/[bug name]: This branch should container only bug fixes for a specific bug - bugfix/[bug name]: This branch should contain only bug fixes for a specific bug
- Example bugfix/bug-1 - Example bugfix/bug-1

37
contributing_de.md Normal file
View File

@ -0,0 +1,37 @@
[English](/contributing.md) | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | **Deutsch** | [Français](/contributing_fr.md)
## Lass uns Bruno noch besser machen, gemeinsam !!
Ich freue mich, dass Du Bruno verbessern möchtest. Hier findest Du eine Anleitung, mit der Du Bruno auf Deinem Computer einrichten kannst.
### Technologie Stack
Bruno ist mit Next.js und React erstellt. Außerdem benötigen wir electron für die Desktop Version (die lokale Sammlungen unterstützt).
Bibliotheken die wir benutzen
- CSS - Tailwind
- Code Editoren - Codemirror
- State Management - Redux
- Icons - Tabler Icons
- Formulare - formik
- Schema Validierung - Yup
- Request Client - axios
- Dateisystem Watcher - chokidar
### Abhängigkeiten
Du benötigst [Node v18.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
### Lass uns coden
Eine Anleitung zum Ausführen einer lokalen Entwicklungsumgebung findest Du in [development.md](docs/development_de.md).
### Pull Request erstellen
- Bitte halte die PRs klein und begrenzt auf eine Sache
- Bitte halte Dich beim Erstellen eines Branches an das folgende Format
- feature/[feature name]: Dieser Branch soll Änderungen für ein bestimmtes Feature enthalten
- Beispiel: feature/dark-mode
- bugfix/[bug name]: Dieser Branch soll ausschließlich Bugfixes für einen bestimmten Bug enthalten
- Beispiel: bugfix/bug-1

37
contributing_fr.md Normal file
View File

@ -0,0 +1,37 @@
[English](/contributing.md) | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md) | **Français**
## Ensemble, améliorons Bruno !
Je suis content de voir que vous envisagez améliorer Bruno. Ci-dessous, vous trouverez les règles et guides pour récupérer Bruno sur votre ordinateur.
### Technologies utilisées
Bruno est construit en utilisant NextJs et React. Nous utilisons aussi Electron pour embarquer la version ordinateur (qui permet les collections locales).
Les bibliothèques que nous utilisons :
- CSS - Tailwind
- Code Editors - Codemirror
- State Management - Redux
- Icons - Tabler Icons
- Forms - formik
- Schema Validation - Yup
- Request Client - axios
- Filesystem Watcher - chokidar
### Dépendances
Vous aurez besoin de [Node v18.x ou la dernière version LTS](https://nodejs.org/en/) et npm 8.x. Nous utilisons aussi les espaces de travail npm (_npm workspaces_) dans ce projet.
### Commençons à coder
Veuillez vous référez à la [documentation de développement](docs/development_fr.md) pour les instructions de démarrage de l'environnement de développement local.
### Ouvrir une Pull Request
- Merci de conserver les PR petites et focalisées sur un seul objectif
- Merci de suivre le format de nom des branches
- feature/[feature name]: Cette branche devrait contenir une fonctionnalité spécifique
- Exemple: feature/dark-mode
- bugfix/[bug name]: Cette branche devrait contenir seulement une solution pour pour une bogue spécifique
- Exemple: bugfix/bug-1

View File

@ -1,4 +1,4 @@
[English](/contributing.md) | [Українська](/contributing_ua.md) | **Русский** [English](/contributing.md) | [Українська](/contributing_ua.md) | **Русский** | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md) | [Français](/contributing_fr.md)
## Давайте вместе сделаем Бруно лучше!!! ## Давайте вместе сделаем Бруно лучше!!!
@ -6,7 +6,7 @@
### Стек ### Стек
Bruno построен с использованием NextJs и React. Мы также используем electron для поставки десктопной версии ( которая поддерживает локальные коллекции ) Bruno построен с использованием Next.js и React. Мы также используем electron для поставки десктопной версии ( которая поддерживает локальные коллекции )
Библиотеки, которые мы используем Библиотеки, которые мы используем

37
contributing_tr.md Normal file
View File

@ -0,0 +1,37 @@
[English](/readme.md) | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | **Türkçe** | [Deutsch](/contributing_de.md) | [Français](/contributing_fr.md)
## Bruno'yu birlikte daha iyi hale getirelim !!
Bruno'yu geliştirmek istemenizden mutluluk duyuyorum. Aşağıda, bruno'yu bilgisayarınıza getirmeye başlamak için yönergeler bulunmaktadır.
### Kullanılan Teknolojiler
Bruno, Next.js ve React kullanılarak oluşturulmuştur. Ayrıca bir masaüstü sürümü (yerel koleksiyonları destekleyen) göndermek için electron kullanıyoruz
Kullandığımız kütüphaneler
- CSS - Tailwind
- Kod Düzenleyiciler - Codemirror
- Durum Yönetimi - Redux
- Iconlar - Tabler Simgeleri
- Formlar - formik
- Şema Doğrulama - Yup
- İstek İstemcisi - axios
- Dosya Sistemi İzleyicisi - chokidar
### Bağımlılıklar
[Node v18.x veya en son LTS sürümüne](https://nodejs.org/en/) ve npm 8.x'e ihtiyacınız olacaktır. Projede npm çalışma alanlarını kullanıyoruz
### Kodlamaya başlayalım
Yerel geliştirme ortamının çalıştırılmasına ilişkin talimatlar için lütfen [development.md](docs/development.md) adresine başvurun.
### Pull Request Oluşturma
- Lütfen PR'ları küçük tutun ve tek bir şeye odaklanın
- Lütfen şube oluşturma formatını takip edin
- feature/[özellik adı]: Bu dal belirli bir özellik için değişiklikler içermelidir
- Örnek: feature/dark-mode
- bugfix/[hata adı]: Bu dal yalnızca belirli bir hata için hata düzeltmelerini içermelidir
- Örnek bugfix/bug-1

View File

@ -1,4 +1,4 @@
[English](/contributing.md) | **Українська** | [Русский](/contributing_ru.md) [English](/contributing.md) | **Українська** | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md) | [Français](/contributing_fr.md)
## Давайте зробимо Bruno краще, разом !! ## Давайте зробимо Bruno краще, разом !!
@ -6,7 +6,7 @@
### Стек технологій ### Стек технологій
Bruno побудований на NextJs та React. Також для десктопної версії (яка підтримує локальні колекції) використовується Electron Bruno побудований на Next.js та React. Також для десктопної версії (яка підтримує локальні колекції) використовується Electron
Бібліотеки, які ми використовуємо Бібліотеки, які ми використовуємо

View File

@ -1,8 +1,8 @@
**English** | [Українська](/docs/development_ua.md) | [Русский](/docs/development_ru.md) **English** | [Українська](/docs/development_ua.md) | [Русский](/docs/development_ru.md) | [Deutsch](/docs/development_de.md) | [Français](/docs/development_fr.md)
## Development ## Development
Bruno is being developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal. Bruno is being developed as a desktop app. You need to load the app by running the Next.js app in one terminal and then run the electron app in another terminal.
### Dependencies ### Dependencies

55
docs/development_de.md Normal file
View File

@ -0,0 +1,55 @@
[English](/docs/development.md) | [Українська](/docs/development_ua.md) | [Русский](/docs/development_ru.md) | **Deutsch** | [Français](/docs/development_fr.md)
## Entwicklung
Bruno wird als Desktop-Anwendung entwickelt. Um die App zu starten, musst Du zuerst die Next.js App in einem Terminal ausführen und anschließend in einem anderen Terminal die Electron-App.
### Abhängigkeiten
- NodeJS v18
### Lokales Entwickeln
```bash
# use nodejs 18 version
nvm use
# install deps
npm i --legacy-peer-deps
# build graphql docs
npm run build:graphql-docs
# build bruno query
npm run build:bruno-query
# run next app (terminal 1)
npm run dev:web
# run electron app (terminal 2)
npm run dev:electron
```
### Troubleshooting
Es kann sein, dass Du einen `Unsupported platform`-Fehler bekommst, wenn Du `npm install` ausführst. Um dies zu beheben, musst Du `node_modules` und `package-lock.json` löschen und `npm install` erneut ausführen. Dies sollte alle notwendigen Pakete installieren, die zum Ausführen der Anwendung benötigt werden.
```shell
# Delete node_modules in sub-directories
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# Delete package-lock in sub-directories
find . -type f -name "package-lock.json" -delete
```
### Testen
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```

55
docs/development_fr.md Normal file
View File

@ -0,0 +1,55 @@
[English](/docs/development.md) | [Українська](/docs/development_ua.md) | [Русский](/docs/development_ru.md) | [Deutsch](/docs/development_de.md) | **Français**
## Développement
Bruno est développé comme une application de _lourde_. Vous devez charger l'application en démarrant nextjs dans un terminal, puis démarre l'application Electron dans un autre terminal.
### Dépendances
- NodeJS v18
### Développement local
```bash
# use nodejs 18 version
nvm use
# install deps
npm i --legacy-peer-deps
# build graphql docs
npm run build:graphql-docs
# build bruno query
npm run build:bruno-query
# run next app (terminal 1)
npm run dev:web
# run electron app (terminal 2)
npm run dev:electron
```
### Dépannage
Vous pourriez rencontrer une error `Unsupported platform` pendant le lancement de `npm install`. Pour résoudre cela, veuillez supprimer le répertoire `node_modules`, le fichier `package-lock.json` et lancer à nouveau `npm install`. Cela devrait isntaller tous les paquets nécessaires pour lancer l'application.
```shell
# Delete node_modules in sub-directories
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# Delete package-lock in sub-directories
find . -type f -name "package-lock.json" -delete
```
### Tests
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```

View File

@ -1,8 +1,8 @@
[English](/docs/development.md) | [Українська](/docs/development_ua.md) | **Русский** [English](/docs/development.md) | [Українська](/docs/development_ua.md) | **Русский** | [Deutsch](/docs/development_de.md) | [Français](/docs/development_fr.md)
## Разработка ## Разработка
Bruno разрабатывается как десктопное приложение. Необходимо загрузить приложение, запустив приложение nextjs в одном терминале, а затем запустить приложение electron в другом терминале. Bruno разрабатывается как десктопное приложение. Необходимо загрузить приложение, запустив приложение Next.js в одном терминале, а затем запустить приложение electron в другом терминале.
### Зависимости ### Зависимости

View File

@ -1,8 +1,8 @@
[English](/docs/development.md) | **Українська** | [Русский](/docs/development_ru.md) [English](/docs/development.md) | **Українська** | [Русский](/docs/development_ru.md) | [Deutsch](/docs/development_de.md) | [Français](/docs/development_fr.md)
## Розробка ## Розробка
Bruno розробляється як декстопний застосунок. Вам потрібно запустити nextjs в одній сесії терміналу, та запустити застосунок Electron в іншій сесії терміналу. Bruno розробляється як декстопний застосунок. Вам потрібно запустити Next.js в одній сесії терміналу, та запустити застосунок Electron в іншій сесії терміналу.
### Залежності ### Залежності

220
package-lock.json generated
View File

@ -5534,75 +5534,6 @@
"node": ">=10.12.0" "node": ">=10.12.0"
} }
}, },
"node_modules/autoprefixer": {
"version": "10.4.16",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz",
"integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"browserslist": "^4.21.10",
"caniuse-lite": "^1.0.30001538",
"fraction.js": "^4.3.6",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/autoprefixer/node_modules/browserslist": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
"integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001541",
"electron-to-chromium": "^1.4.535",
"node-releases": "^2.0.13",
"update-browserslist-db": "^1.0.13"
},
"bin": {
"browserslist": "cli.js"
},
"engines": {
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/aws-sign2": { "node_modules/aws-sign2": {
"version": "0.7.0", "version": "0.7.0",
"dev": true, "dev": true,
@ -5998,6 +5929,7 @@
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.21.4", "version": "4.21.4",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -7919,7 +7851,8 @@
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.554", "version": "1.4.554",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.554.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.554.tgz",
"integrity": "sha512-Q0umzPJjfBrrj8unkONTgbKQXzXRrH7sVV7D9ea2yBV3Oaogz991yhbpfvo2LMNkJItmruXTEzVpP9cp7vaIiQ==" "integrity": "sha512-Q0umzPJjfBrrj8unkONTgbKQXzXRrH7sVV7D9ea2yBV3Oaogz991yhbpfvo2LMNkJItmruXTEzVpP9cp7vaIiQ==",
"dev": true
}, },
"node_modules/electron-util": { "node_modules/electron-util": {
"version": "0.17.2", "version": "0.17.2",
@ -8664,19 +8597,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
"peer": true,
"engines": {
"node": "*"
},
"funding": {
"type": "patreon",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fresh": { "node_modules/fresh": {
"version": "0.5.2", "version": "0.5.2",
"license": "MIT", "license": "MIT",
@ -11856,7 +11776,8 @@
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.13", "version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
"dev": true
}, },
"node_modules/normalize-package-data": { "node_modules/normalize-package-data": {
"version": "2.5.0", "version": "2.5.0",
@ -11889,15 +11810,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/normalize-url": { "node_modules/normalize-url": {
"version": "6.1.0", "version": "6.1.0",
"dev": true, "dev": true,
@ -15843,6 +15755,7 @@
"version": "1.0.13", "version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
"integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -16534,7 +16447,7 @@
"@tabler/icons": "^1.46.0", "@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"@usebruno/graphql-docs": "0.1.0", "@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.5.0", "@usebruno/schema": "0.6.0",
"axios": "^0.26.0", "axios": "^0.26.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"codemirror": "^5.65.2", "codemirror": "^5.65.2",
@ -16625,11 +16538,11 @@
}, },
"packages/bruno-cli": { "packages/bruno-cli": {
"name": "@usebruno/cli", "name": "@usebruno/cli",
"version": "0.14.0", "version": "0.15.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@usebruno/js": "0.8.0", "@usebruno/js": "0.9.1",
"@usebruno/lang": "0.8.0", "@usebruno/lang": "0.9.0",
"axios": "^1.5.1", "axios": "^1.5.1",
"chai": "^4.3.7", "chai": "^4.3.7",
"chalk": "^3.0.0", "chalk": "^3.0.0",
@ -16717,18 +16630,18 @@
}, },
"packages/bruno-electron": { "packages/bruno-electron": {
"name": "bruno", "name": "bruno",
"version": "v0.25.0", "version": "v0.27.2",
"dependencies": { "dependencies": {
"@aws-sdk/credential-providers": "^3.425.0", "@aws-sdk/credential-providers": "^3.425.0",
"@usebruno/js": "0.8.0", "@usebruno/js": "0.9.1",
"@usebruno/lang": "0.8.0", "@usebruno/lang": "0.9.0",
"@usebruno/schema": "0.5.0", "@usebruno/schema": "0.6.0",
"about-window": "^1.15.2", "about-window": "^1.15.2",
"aws4-axios": "^3.3.0", "aws4-axios": "^3.3.0",
"axios": "^1.5.1", "axios": "^1.5.1",
"chai": "^4.3.7", "chai": "^4.3.7",
"chai-string": "^1.5.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"content-disposition": "^0.5.4",
"decomment": "^0.9.5", "decomment": "^0.9.5",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"electron-is-dev": "^2.0.0", "electron-is-dev": "^2.0.0",
@ -16742,7 +16655,9 @@
"http-proxy-agent": "^7.0.0", "http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.2",
"is-valid-path": "^0.1.1", "is-valid-path": "^0.1.1",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mime-types": "^2.1.35",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"node-machine-id": "^1.1.12", "node-machine-id": "^1.1.12",
@ -16776,6 +16691,11 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"packages/bruno-electron/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"packages/bruno-electron/node_modules/aws4-axios": { "packages/bruno-electron/node_modules/aws4-axios": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/aws4-axios/-/aws4-axios-3.3.0.tgz", "resolved": "https://registry.npmjs.org/aws4-axios/-/aws4-axios-3.3.0.tgz",
@ -16892,6 +16812,17 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"packages/bruno-electron/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"packages/bruno-electron/node_modules/uuid": { "packages/bruno-electron/node_modules/uuid": {
"version": "9.0.0", "version": "9.0.0",
"license": "MIT", "license": "MIT",
@ -16928,7 +16859,7 @@
}, },
"packages/bruno-js": { "packages/bruno-js": {
"name": "@usebruno/js", "name": "@usebruno/js",
"version": "0.8.0", "version": "0.9.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@usebruno/query": "0.1.0", "@usebruno/query": "0.1.0",
@ -16937,6 +16868,7 @@
"axios": "^0.26.0", "axios": "^0.26.0",
"btoa": "^1.2.1", "btoa": "^1.2.1",
"chai": "^4.3.7", "chai": "^4.3.7",
"chai-string": "^1.5.0",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"json-query": "^2.2.2", "json-query": "^2.2.2",
@ -16977,7 +16909,7 @@
}, },
"packages/bruno-lang": { "packages/bruno-lang": {
"name": "@usebruno/lang", "name": "@usebruno/lang",
"version": "0.8.0", "version": "0.9.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"arcsecond": "^5.0.0", "arcsecond": "^5.0.0",
@ -17014,7 +16946,7 @@
}, },
"packages/bruno-schema": { "packages/bruno-schema": {
"name": "@usebruno/schema", "name": "@usebruno/schema",
"version": "0.5.0", "version": "0.6.0",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"yup": "^0.32.11" "yup": "^0.32.11"
@ -20578,7 +20510,7 @@
"@tabler/icons": "^1.46.0", "@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"@usebruno/graphql-docs": "0.1.0", "@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.5.0", "@usebruno/schema": "0.6.0",
"axios": "^0.26.0", "axios": "^0.26.0",
"babel-loader": "^8.2.3", "babel-loader": "^8.2.3",
"classnames": "^2.3.1", "classnames": "^2.3.1",
@ -20660,8 +20592,8 @@
"@usebruno/cli": { "@usebruno/cli": {
"version": "file:packages/bruno-cli", "version": "file:packages/bruno-cli",
"requires": { "requires": {
"@usebruno/js": "0.8.0", "@usebruno/js": "0.9.1",
"@usebruno/lang": "0.8.0", "@usebruno/lang": "0.9.0",
"axios": "^1.5.1", "axios": "^1.5.1",
"chai": "^4.3.7", "chai": "^4.3.7",
"chalk": "^3.0.0", "chalk": "^3.0.0",
@ -20756,6 +20688,7 @@
"axios": "^0.26.0", "axios": "^0.26.0",
"btoa": "^1.2.1", "btoa": "^1.2.1",
"chai": "^4.3.7", "chai": "^4.3.7",
"chai-string": "^1.5.0",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"json-query": "^2.2.2", "json-query": "^2.2.2",
@ -21307,34 +21240,6 @@
"atomically": { "atomically": {
"version": "1.7.0" "version": "1.7.0"
}, },
"autoprefixer": {
"version": "10.4.16",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz",
"integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==",
"peer": true,
"requires": {
"browserslist": "^4.21.10",
"caniuse-lite": "^1.0.30001538",
"fraction.js": "^4.3.6",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"postcss-value-parser": "^4.2.0"
},
"dependencies": {
"browserslist": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
"integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
"peer": true,
"requires": {
"caniuse-lite": "^1.0.30001541",
"electron-to-chromium": "^1.4.535",
"node-releases": "^2.0.13",
"update-browserslist-db": "^1.0.13"
}
}
}
},
"aws-sign2": { "aws-sign2": {
"version": "0.7.0", "version": "0.7.0",
"dev": true "dev": true
@ -21596,6 +21501,7 @@
}, },
"browserslist": { "browserslist": {
"version": "4.21.4", "version": "4.21.4",
"dev": true,
"requires": { "requires": {
"caniuse-lite": "^1.0.30001400", "caniuse-lite": "^1.0.30001400",
"electron-to-chromium": "^1.4.251", "electron-to-chromium": "^1.4.251",
@ -21607,15 +21513,15 @@
"version": "file:packages/bruno-electron", "version": "file:packages/bruno-electron",
"requires": { "requires": {
"@aws-sdk/credential-providers": "^3.425.0", "@aws-sdk/credential-providers": "^3.425.0",
"@usebruno/js": "0.8.0", "@usebruno/js": "0.9.1",
"@usebruno/lang": "0.8.0", "@usebruno/lang": "0.9.0",
"@usebruno/schema": "0.5.0", "@usebruno/schema": "0.6.0",
"about-window": "^1.15.2", "about-window": "^1.15.2",
"aws4-axios": "^3.3.0", "aws4-axios": "^3.3.0",
"axios": "^1.5.1", "axios": "^1.5.1",
"chai": "^4.3.7", "chai": "^4.3.7",
"chai-string": "^1.5.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"content-disposition": "^0.5.4",
"decomment": "^0.9.5", "decomment": "^0.9.5",
"dmg-license": "^1.0.11", "dmg-license": "^1.0.11",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
@ -21633,7 +21539,9 @@
"http-proxy-agent": "^7.0.0", "http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.2",
"is-valid-path": "^0.1.1", "is-valid-path": "^0.1.1",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mime-types": "^2.1.35",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"node-machine-id": "^1.1.12", "node-machine-id": "^1.1.12",
@ -21654,6 +21562,11 @@
"debug": "^4.3.4" "debug": "^4.3.4"
} }
}, },
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"aws4-axios": { "aws4-axios": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/aws4-axios/-/aws4-axios-3.3.0.tgz", "resolved": "https://registry.npmjs.org/aws4-axios/-/aws4-axios-3.3.0.tgz",
@ -21724,6 +21637,14 @@
"debug": "4" "debug": "4"
} }
}, },
"js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"requires": {
"argparse": "^2.0.1"
}
},
"uuid": { "uuid": {
"version": "9.0.0" "version": "9.0.0"
} }
@ -22948,7 +22869,8 @@
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.4.554", "version": "1.4.554",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.554.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.554.tgz",
"integrity": "sha512-Q0umzPJjfBrrj8unkONTgbKQXzXRrH7sVV7D9ea2yBV3Oaogz991yhbpfvo2LMNkJItmruXTEzVpP9cp7vaIiQ==" "integrity": "sha512-Q0umzPJjfBrrj8unkONTgbKQXzXRrH7sVV7D9ea2yBV3Oaogz991yhbpfvo2LMNkJItmruXTEzVpP9cp7vaIiQ==",
"dev": true
}, },
"electron-util": { "electron-util": {
"version": "0.17.2", "version": "0.17.2",
@ -23429,12 +23351,6 @@
"forwarded": { "forwarded": {
"version": "0.2.0" "version": "0.2.0"
}, },
"fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
"integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
"peer": true
},
"fresh": { "fresh": {
"version": "0.5.2" "version": "0.5.2"
}, },
@ -25443,7 +25359,8 @@
"node-releases": { "node-releases": {
"version": "2.0.13", "version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
"dev": true
}, },
"normalize-package-data": { "normalize-package-data": {
"version": "2.5.0", "version": "2.5.0",
@ -25468,12 +25385,6 @@
"normalize-path": { "normalize-path": {
"version": "3.0.0" "version": "3.0.0"
}, },
"normalize-range": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
"integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
"peer": true
},
"normalize-url": { "normalize-url": {
"version": "6.1.0", "version": "6.1.0",
"dev": true "dev": true
@ -27920,6 +27831,7 @@
"version": "1.0.13", "version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
"integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
"dev": true,
"requires": { "requires": {
"escalade": "^3.1.1", "escalade": "^3.1.1",
"picocolors": "^1.0.0" "picocolors": "^1.0.0"

View File

@ -3,7 +3,7 @@
"version": "0.3.0", "version": "0.3.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "cross-env ENV=dev next dev", "dev": "cross-env ENV=dev next dev -p 3000",
"build": "next build && next export", "build": "next build && next export",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
@ -19,7 +19,7 @@
"@tabler/icons": "^1.46.0", "@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6", "@tippyjs/react": "^4.2.6",
"@usebruno/graphql-docs": "0.1.0", "@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.5.0", "@usebruno/schema": "0.6.0",
"axios": "^0.26.0", "axios": "^0.26.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"codemirror": "^5.65.2", "codemirror": "^5.65.2",

View File

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

View File

@ -70,6 +70,35 @@ export default class CodeEditor extends React.Component {
'Ctrl-F': 'findPersistent', 'Ctrl-F': 'findPersistent',
Tab: function (cm) { Tab: function (cm) {
cm.replaceSelection(' ', 'end'); cm.replaceSelection(' ', 'end');
},
'Ctrl-Y': 'foldAll',
'Cmd-Y': 'foldAll',
'Ctrl-I': 'unfoldAll',
'Cmd-I': 'unfoldAll'
},
foldOptions: {
widget: (from, to) => {
var count = undefined;
var internal = this.editor.getRange(from, to);
if (this.props.mode == 'application/ld+json') {
if (this.editor.getLine(from.line).endsWith('[')) {
var toParse = '[' + internal + ']';
} else var toParse = '{' + internal + '}';
try {
count = Object.keys(JSON.parse(toParse)).length;
} catch (e) {}
} else if (this.props.mode == 'application/xml') {
var doc = new DOMParser();
try {
//add header element and remove prefix namespaces for DOMParser
var dcm = doc.parseFromString(
'<a> ' + internal.replace(/(?<=\<|<\/)\w+:/g, '') + '</a>',
'application/xml'
);
count = dcm.documentElement.children.length;
} catch (e) {}
}
return count ? `\u21A4${count}\u21A6` : '\u2194';
} }
} }
})); }));

View File

@ -7,17 +7,17 @@ import toast from 'react-hot-toast';
const ProxySettings = ({ proxyConfig, onUpdate }) => { const ProxySettings = ({ proxyConfig, onUpdate }) => {
const proxySchema = Yup.object({ const proxySchema = Yup.object({
use: Yup.string().oneOf(['global', 'true', 'false']), enabled: Yup.string().oneOf(['global', 'true', 'false']),
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']), protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
hostname: Yup.string() hostname: Yup.string()
.when('use', { .when('enabled', {
is: true, is: true,
then: (hostname) => hostname.required('Specify the hostname for your proxy.'), then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
otherwise: (hostname) => hostname.nullable() otherwise: (hostname) => hostname.nullable()
}) })
.max(1024), .max(1024),
port: Yup.number() port: Yup.number()
.when('use', { .when('enabled', {
is: true, is: true,
then: (port) => port.required('Specify port between 1 and 65535').typeError('Specify port between 1 and 65535'), then: (port) => port.required('Specify port between 1 and 65535').typeError('Specify port between 1 and 65535'),
otherwise: (port) => port.nullable().transform((_, val) => (val ? Number(val) : null)) otherwise: (port) => port.nullable().transform((_, val) => (val ? Number(val) : null))
@ -49,7 +49,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
const formik = useFormik({ const formik = useFormik({
initialValues: { initialValues: {
use: proxyConfig.use || 'global', enabled: proxyConfig.enabled || 'global',
protocol: proxyConfig.protocol || 'http', protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '', hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '', port: proxyConfig.port || '',
@ -65,11 +65,11 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
proxySchema proxySchema
.validate(values, { abortEarly: true }) .validate(values, { abortEarly: true })
.then((validatedProxy) => { .then((validatedProxy) => {
// serialize 'use' to boolean // serialize 'enabled' to boolean
if (validatedProxy.use === 'true') { if (validatedProxy.enabled === 'true') {
validatedProxy.use = true; validatedProxy.enabled = true;
} else if (validatedProxy.use === 'false') { } else if (validatedProxy.enabled === 'false') {
validatedProxy.use = false; validatedProxy.enabled = false;
} }
onUpdate(validatedProxy); onUpdate(validatedProxy);
@ -83,7 +83,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
useEffect(() => { useEffect(() => {
formik.setValues({ formik.setValues({
use: proxyConfig.use === true ? 'true' : proxyConfig.use === false ? 'false' : 'global', enabled: proxyConfig.enabled === true ? 'true' : proxyConfig.enabled === false ? 'false' : 'global',
protocol: proxyConfig.protocol || 'http', protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '', hostname: proxyConfig.hostname || '',
port: proxyConfig.port || '', port: proxyConfig.port || '',
@ -120,9 +120,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<label className="flex items-center"> <label className="flex items-center">
<input <input
type="radio" type="radio"
name="use" name="enabled"
value="global" value="global"
checked={formik.values.use === 'global'} checked={formik.values.enabled === 'global'}
onChange={formik.handleChange} onChange={formik.handleChange}
className="mr-1" className="mr-1"
/> />
@ -131,9 +131,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<label className="flex items-center ml-4"> <label className="flex items-center ml-4">
<input <input
type="radio" type="radio"
name="use" name="enabled"
value={'true'} value={'true'}
checked={formik.values.use === 'true'} checked={formik.values.enabled === 'true'}
onChange={formik.handleChange} onChange={formik.handleChange}
className="mr-1" className="mr-1"
/> />
@ -142,9 +142,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
<label className="flex items-center ml-4"> <label className="flex items-center ml-4">
<input <input
type="radio" type="radio"
name="use" name="enabled"
value={'false'} value={'false'}
checked={formik.values.use === 'false'} checked={formik.values.enabled === 'false'}
onChange={formik.handleChange} onChange={formik.handleChange}
className="mr-1" className="mr-1"
/> />

View File

@ -14,6 +14,8 @@ const Wrapper = styled.div`
background-color: ${(props) => props.theme.dropdown.bg}; background-color: ${(props) => props.theme.dropdown.bg};
box-shadow: ${(props) => props.theme.dropdown.shadow}; box-shadow: ${(props) => props.theme.dropdown.shadow};
border-radius: 3px; border-radius: 3px;
max-height: 90vh;
overflow-y: auto;
.tippy-content { .tippy-content {
padding-left: 0; padding-left: 0;

View File

@ -10,7 +10,8 @@ const StyledWrapper = styled.div`
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg}; background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight}; border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
min-height: 400px; min-height: 400px;
height: 100%; max-height: 85vh;
overflow-y: auto;
} }
.environment-item { .environment-item {

View File

@ -1,7 +1,5 @@
import React, { useEffect, useState, forwardRef, useRef } from 'react'; import React, { useEffect, useState, forwardRef, useRef } from 'react';
import { findEnvironmentInCollection } from 'utils/collections'; import { findEnvironmentInCollection } from 'utils/collections';
import toast from 'react-hot-toast';
import { toastError } from 'utils/common/error';
import usePrevious from 'hooks/usePrevious'; import usePrevious from 'hooks/usePrevious';
import EnvironmentDetails from './EnvironmentDetails'; import EnvironmentDetails from './EnvironmentDetails';
import CreateEnvironment from '../CreateEnvironment'; import CreateEnvironment from '../CreateEnvironment';

View File

@ -107,6 +107,7 @@ const FormUrlEncodedParams = ({ item, collection }) => {
'value' 'value'
) )
} }
allowNewlines={true}
onRun={handleRun} onRun={handleRun}
collection={collection} collection={collection}
/> />

View File

@ -4,7 +4,7 @@ import filter from 'lodash/filter';
import classnames from 'classnames'; import classnames from 'classnames';
import { IconChevronRight, IconChevronLeft } from '@tabler/icons'; import { IconChevronRight, IconChevronLeft } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { focusTab } from 'providers/ReduxStore/slices/tabs'; import { closeTabs, focusTab } from 'providers/ReduxStore/slices/tabs';
import NewRequest from 'components/Sidebar/NewRequest'; import NewRequest from 'components/Sidebar/NewRequest';
import CollectionToolBar from './CollectionToolBar'; import CollectionToolBar from './CollectionToolBar';
import RequestTab from './RequestTab'; import RequestTab from './RequestTab';
@ -35,6 +35,14 @@ const RequestTabs = () => {
); );
}; };
const handleCloseClick = (tab) => {
dispatch(
closeTabs({
tabUids: [tab.uid]
})
);
};
const createNewTab = () => setNewRequestModalOpen(true); const createNewTab = () => setNewRequestModalOpen(true);
if (!activeTabUid) { if (!activeTabUid) {
@ -110,6 +118,7 @@ const RequestTabs = () => {
className={getTabClassname(tab, index)} className={getTabClassname(tab, index)}
role="tab" role="tab"
onClick={() => handleClick(tab)} onClick={() => handleClick(tab)}
onAuxClick={() => handleCloseClick(tab)}
> >
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} /> <RequestTab key={tab.uid} tab={tab} collection={activeCollection} />
</li> </li>

View File

@ -23,6 +23,10 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.colors.text.yellow}; color: ${(props) => props.theme.colors.text.yellow};
} }
} }
.muted {
color: ${(props) => props.theme.colors.text.muted};
}
`; `;
export default StyledWrapper; export default StyledWrapper;

View File

@ -35,7 +35,7 @@ const formatResponse = (data, mode) => {
return safeStringifyJSON(data); return safeStringifyJSON(data);
}; };
const QueryResult = ({ item, collection, data, width, disableRunEventListener, headers, error }) => { const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEventListener, headers, error }) => {
const contentType = getContentType(headers); const contentType = getContentType(headers);
const mode = getCodeMirrorModeBasedOnContentType(contentType); const mode = getCodeMirrorModeBasedOnContentType(contentType);
const formattedData = formatResponse(data, mode); const formattedData = formatResponse(data, mode);
@ -85,12 +85,21 @@ const QueryResult = ({ item, collection, data, width, disableRunEventListener, h
{tabs} {tabs}
</div> </div>
{error ? ( {error ? (
<span className="text-red-500">{error}</span> <div>
<div className="text-red-500">{error}</div>
{error && typeof error === 'string' && error.toLowerCase().includes('self signed certificate') ? (
<div className="mt-6 muted text-xs">
You can disable SSL verification in the Preferences. <br />
To open the Preferences, click on the gear icon in the bottom left corner.
</div>
) : null}
</div>
) : ( ) : (
<QueryResultPreview <QueryResultPreview
previewTab={previewTab} previewTab={previewTab}
data={data} data={data}
dataBuffer={item.response.dataBuffer} dataBuffer={dataBuffer}
formattedData={formattedData} formattedData={formattedData}
item={item} item={item}
contentType={contentType} contentType={contentType}

View File

@ -0,0 +1,8 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
font-size: 0.8125rem;
color: ${(props) => props.theme.requestTabPanel.responseStatus};
`;
export default StyledWrapper;

View File

@ -0,0 +1,32 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import get from 'lodash/get';
import { IconDownload } from '@tabler/icons';
const ResponseSave = ({ item }) => {
const { ipcRenderer } = window;
const response = item.response || {};
const saveResponseToFile = () => {
return new Promise((resolve, reject) => {
console.log(item);
ipcRenderer
.invoke('renderer:save-response-to-file', response, item.requestSent.url)
.then(resolve)
.catch((err) => {
toast.error(get(err, 'error.message') || 'Something went wrong!');
reject(err);
});
});
};
return (
<StyledWrapper className="ml-4 flex items-center">
<button onClick={saveResponseToFile} disabled={!response.dataBuffer} title="Save response to file">
<IconDownload size={16} strokeWidth={1.5} />
</button>
</StyledWrapper>
);
};
export default ResponseSave;

View File

@ -6,7 +6,7 @@ import StyledWrapper from './StyledWrapper';
// Todo: text-error class is not getting pulled in for 500 errors // Todo: text-error class is not getting pulled in for 500 errors
const StatusCode = ({ status }) => { const StatusCode = ({ status }) => {
const getTabClassname = (status) => { const getTabClassname = (status) => {
return classnames('', { return classnames('ml-2', {
'text-ok': status >= 100 && status < 200, 'text-ok': status >= 100 && status < 200,
'text-ok': status >= 200 && status < 300, 'text-ok': status >= 200 && status < 300,
'text-error': status >= 300 && status < 400, 'text-error': status >= 300 && status < 400,

View File

@ -14,6 +14,7 @@ import Timeline from './Timeline';
import TestResults from './TestResults'; import TestResults from './TestResults';
import TestResultsLabel from './TestResultsLabel'; import TestResultsLabel from './TestResultsLabel';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import ResponseSave from 'src/components/ResponsePane/ResponseSave';
const ResponsePane = ({ rightPaneWidth, item, collection }) => { const ResponsePane = ({ rightPaneWidth, item, collection }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -41,6 +42,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
collection={collection} collection={collection}
width={rightPaneWidth} width={rightPaneWidth}
data={response.data} data={response.data}
dataBuffer={response.dataBuffer}
headers={response.headers} headers={response.headers}
error={response.error} error={response.error}
key={item.filename} key={item.filename}
@ -111,6 +113,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
</div> </div>
{!isLoading ? ( {!isLoading ? (
<div className="flex flex-grow justify-end items-center"> <div className="flex flex-grow justify-end items-center">
<ResponseSave item={item} />
<StatusCode status={response.status} /> <StatusCode status={response.status} />
<ResponseTime duration={response.duration} /> <ResponseTime duration={response.duration} />
<ResponseSize size={response.size} /> <ResponseSize size={response.size} />

View File

@ -34,6 +34,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
width={rightPaneWidth} width={rightPaneWidth}
disableRunEventListener={true} disableRunEventListener={true}
data={responseReceived.data} data={responseReceived.data}
dataBuffer={responseReceived.dataBuffer}
headers={responseReceived.headers} headers={responseReceived.headers}
key={item.filename} key={item.filename}
/> />

View File

@ -18,7 +18,7 @@ const RemoveCollection = ({ onClose, collection }) => {
return ( return (
<Modal size="sm" title="Remove Collection" confirmText="Remove" handleConfirm={onConfirm} handleCancel={onClose}> <Modal size="sm" title="Remove Collection" confirmText="Remove" handleConfirm={onConfirm} handleCancel={onClose}>
Are you sure you want to delete collection <span className="font-semibold">{collection.name}</span> ? Are you sure you want to remove collection <span className="font-semibold">{collection.name}</span> ?
</Modal> </Modal>
); );
}; };

View File

@ -2,6 +2,7 @@ import React from 'react';
import importBrunoCollection from 'utils/importers/bruno-collection'; import importBrunoCollection from 'utils/importers/bruno-collection';
import importPostmanCollection from 'utils/importers/postman-collection'; import importPostmanCollection from 'utils/importers/postman-collection';
import importInsomniaCollection from 'utils/importers/insomnia-collection'; import importInsomniaCollection from 'utils/importers/insomnia-collection';
import importOpenapiCollection from 'utils/importers/openapi-collection';
import { toastError } from 'utils/common/error'; import { toastError } from 'utils/common/error';
import Modal from 'components/Modal'; import Modal from 'components/Modal';
@ -30,6 +31,14 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
.catch((err) => toastError(err, 'Insomnia Import collection failed')); .catch((err) => toastError(err, 'Insomnia Import collection failed'));
}; };
const handleImportOpenapiCollection = () => {
importOpenapiCollection()
.then((collection) => {
handleSubmit(collection);
})
.catch((err) => toastError(err, 'OpenAPI v3 Import collection failed'));
};
return ( return (
<Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}> <Modal size="sm" title="Import Collection" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
<div> <div>
@ -42,6 +51,9 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportInsomniaCollection}> <div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportInsomniaCollection}>
Insomnia Collection Insomnia Collection
</div> </div>
<div className="text-link hover:underline cursor-pointer mt-2" onClick={handleImportOpenapiCollection}>
OpenAPI V3 Spec
</div>
</div> </div>
</Modal> </Modal>
); );

View File

@ -7,7 +7,7 @@ import Preferences from 'components/Preferences';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { IconSettings } from '@tabler/icons'; import { IconSettings } from '@tabler/icons';
import { updateLeftSidebarWidth, updateIsDragging } from 'providers/ReduxStore/slices/app'; import { updateLeftSidebarWidth, updateIsDragging, showPreferences } from 'providers/ReduxStore/slices/app';
import { useTheme } from 'providers/Theme'; import { useTheme } from 'providers/Theme';
const MIN_LEFT_SIDEBAR_WIDTH = 222; const MIN_LEFT_SIDEBAR_WIDTH = 222;
@ -15,7 +15,7 @@ const MAX_LEFT_SIDEBAR_WIDTH = 600;
const Sidebar = () => { const Sidebar = () => {
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth); const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
const [preferencesOpen, setPreferencesOpen] = useState(false); const preferencesOpen = useSelector((state) => state.app.showPreferences);
const [asideWidth, setAsideWidth] = useState(leftSidebarWidth); const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);
@ -78,7 +78,7 @@ const Sidebar = () => {
<StyledWrapper className="flex relative h-screen"> <StyledWrapper className="flex relative h-screen">
<aside> <aside>
<div className="flex flex-row h-screen w-full"> <div className="flex flex-row h-screen w-full">
{preferencesOpen && <Preferences onClose={() => setPreferencesOpen(false)} />} {preferencesOpen && <Preferences onClose={() => dispatch(showPreferences(false))} />}
<div className="flex flex-col w-full" style={{ width: asideWidth }}> <div className="flex flex-col w-full" style={{ width: asideWidth }}>
<div className="flex flex-col flex-grow"> <div className="flex flex-col flex-grow">
@ -92,7 +92,7 @@ const Sidebar = () => {
size={18} size={18}
strokeWidth={1.5} strokeWidth={1.5}
className="mr-2 hover:text-gray-700" className="mr-2 hover:text-gray-700"
onClick={() => setPreferencesOpen(true)} onClick={() => dispatch(showPreferences(true))}
/> />
</div> </div>
<div className="pl-1" style={{ position: 'relative', top: '3px' }}> <div className="pl-1" style={{ position: 'relative', top: '3px' }}>
@ -105,7 +105,7 @@ const Sidebar = () => {
Star Star
</GitHubButton> </GitHubButton>
</div> </div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v0.26.0</div> <div className="flex flex-grow items-center justify-end text-xs mr-2">v0.27.2</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -57,6 +57,7 @@ class SingleLineEditor extends Component {
} }
componentDidMount() { componentDidMount() {
// Initialize CodeMirror as a single line editor // Initialize CodeMirror as a single line editor
/** @type {import("codemirror").Editor} */
this.editor = CodeMirror(this.editorRef.current, { this.editor = CodeMirror(this.editorRef.current, {
lineWrapping: false, lineWrapping: false,
lineNumbers: false, lineNumbers: false,
@ -84,7 +85,10 @@ class SingleLineEditor extends Component {
} }
}, },
'Alt-Enter': () => { 'Alt-Enter': () => {
if (this.props.onRun) { if (this.props.allowNewlines) {
this.editor.setValue(this.editor.getValue() + '\n');
this.editor.setCursor({ line: this.editor.lineCount(), ch: 0 });
} else if (this.props.onRun) {
this.props.onRun(); this.props.onRun();
} }
}, },

View File

@ -21,6 +21,7 @@ if (!SERVER_RENDERED) {
require('codemirror/addon/edit/matchbrackets'); require('codemirror/addon/edit/matchbrackets');
require('codemirror/addon/fold/brace-fold'); require('codemirror/addon/fold/brace-fold');
require('codemirror/addon/fold/foldgutter'); require('codemirror/addon/fold/foldgutter');
require('codemirror/addon/fold/xml-fold');
require('codemirror/addon/hint/show-hint'); require('codemirror/addon/hint/show-hint');
require('codemirror/addon/lint/lint'); require('codemirror/addon/lint/lint');
require('codemirror/addon/mode/overlay'); require('codemirror/addon/mode/overlay');

View File

@ -41,6 +41,19 @@ function MyApp({ Component, pageProps }) {
return null; return null;
} }
if (!window.ipcRenderer) {
return (
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 mx-10 my-10 rounded relative" role="alert">
<strong class="font-bold">ERROR:</strong>
<span className="block inline ml-1">"ipcRenderer" not found in window object.</span>
<div>
You most likely opened Bruno inside your web browser. Bruno only works within Electron, you can start Electron
in an adjacent terminal using "npm run dev:electron".
</div>
</div>
);
}
return ( return (
<ErrorBoundary> <ErrorBoundary>
<SafeHydrate> <SafeHydrate>

View File

@ -14,7 +14,7 @@ import {
runFolderEvent, runFolderEvent,
brunoConfigUpdateEvent brunoConfigUpdateEvent
} from 'providers/ReduxStore/slices/collections'; } from 'providers/ReduxStore/slices/collections';
import { updatePreferences } from 'providers/ReduxStore/slices/app'; import { showPreferences, updatePreferences } from 'providers/ReduxStore/slices/app';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { openCollectionEvent, collectionAddEnvFileEvent } from 'providers/ReduxStore/slices/collections/actions'; import { openCollectionEvent, collectionAddEnvFileEvent } from 'providers/ReduxStore/slices/collections/actions';
import { isElectron } from 'utils/common/platform'; import { isElectron } from 'utils/common/platform';
@ -127,6 +127,10 @@ const useIpcEvents = () => {
dispatch(brunoConfigUpdateEvent(val)) dispatch(brunoConfigUpdateEvent(val))
); );
const showPreferencesListener = ipcRenderer.on('main:open-preferences', () => {
dispatch(showPreferences(true));
});
const removePreferencesUpdatesListener = ipcRenderer.on('main:load-preferences', (val) => { const removePreferencesUpdatesListener = ipcRenderer.on('main:load-preferences', (val) => {
dispatch(updatePreferences(val)); dispatch(updatePreferences(val));
}); });
@ -143,6 +147,7 @@ const useIpcEvents = () => {
removeProcessEnvUpdatesListener(); removeProcessEnvUpdatesListener();
removeConsoleLogListener(); removeConsoleLogListener();
removeConfigUpdatesListener(); removeConfigUpdatesListener();
showPreferencesListener();
removePreferencesUpdatesListener(); removePreferencesUpdatesListener();
}; };
}, [isElectron]); }, [isElectron]);

View File

@ -7,6 +7,7 @@ const initialState = {
leftSidebarWidth: 222, leftSidebarWidth: 222,
screenWidth: 500, screenWidth: 500,
showHomePage: false, showHomePage: false,
showPreferences: false,
preferences: { preferences: {
request: { request: {
sslVerification: true, sslVerification: true,
@ -40,6 +41,9 @@ export const appSlice = createSlice({
hideHomePage: (state) => { hideHomePage: (state) => {
state.showHomePage = false; state.showHomePage = false;
}, },
showPreferences: (state, action) => {
state.showPreferences = action.payload;
},
updatePreferences: (state, action) => { updatePreferences: (state, action) => {
state.preferences = action.payload; state.preferences = action.payload;
} }
@ -53,6 +57,7 @@ export const {
updateIsDragging, updateIsDragging,
showHomePage, showHomePage,
hideHomePage, hideHomePage,
showPreferences,
updatePreferences updatePreferences
} = appSlice.actions; } = appSlice.actions;

View File

@ -1,3 +1,4 @@
import jsyaml from 'js-yaml';
import each from 'lodash/each'; import each from 'lodash/each';
import get from 'lodash/get'; import get from 'lodash/get';
import fileDialog from 'file-dialog'; import fileDialog from 'file-dialog';
@ -8,7 +9,22 @@ import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } fr
const readFile = (files) => { const readFile = (files) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const fileReader = new FileReader(); const fileReader = new FileReader();
fileReader.onload = (e) => resolve(e.target.result); fileReader.onload = (e) => {
try {
// try to load JSON
const parsedData = JSON.parse(e.target.result);
resolve(parsedData);
} catch (jsonError) {
// not a valid JSOn, try yaml
try {
const parsedData = jsyaml.load(e.target.result);
resolve(parsedData);
} catch (yamlError) {
console.error('Error parsing the file :', jsonError, yamlError);
reject(new BrunoError('Import collection failed'));
}
}
};
fileReader.onerror = (err) => reject(err); fileReader.onerror = (err) => reject(err);
fileReader.readAsText(files[0]); fileReader.readAsText(files[0]);
}); });
@ -167,7 +183,7 @@ const parseInsomniaCollection = (data) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const insomniaExport = JSON.parse(data); const insomniaExport = data;
const insomniaResources = get(insomniaExport, 'resources', []); const insomniaResources = get(insomniaExport, 'resources', []);
const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace'); const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace');
@ -213,7 +229,7 @@ const parseInsomniaCollection = (data) => {
const importCollection = () => { const importCollection = () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fileDialog({ accept: 'application/json' }) fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' })
.then(readFile) .then(readFile)
.then(parseInsomniaCollection) .then(parseInsomniaCollection)
.then(transformItemsInCollection) .then(transformItemsInCollection)
@ -221,8 +237,8 @@ const importCollection = () => {
.then(validateSchema) .then(validateSchema)
.then((collection) => resolve(collection)) .then((collection) => resolve(collection))
.catch((err) => { .catch((err) => {
console.log(err); console.error(err);
reject(new BrunoError('Import collection failed')); reject(new BrunoError('Import collection failed: ' + err.message));
}); });
}); });
}; };

View File

@ -0,0 +1,386 @@
import jsyaml from 'js-yaml';
import each from 'lodash/each';
import get from 'lodash/get';
import fileDialog from 'file-dialog';
import { uuid } from 'utils/common';
import { BrunoError } from 'utils/common/error';
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
const readFile = (files) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (e) => {
try {
// try to load JSON
const parsedData = JSON.parse(e.target.result);
resolve(parsedData);
} catch (jsonError) {
// not a valid JSOn, try yaml
try {
const parsedData = jsyaml.load(e.target.result);
resolve(parsedData);
} catch (yamlError) {
console.error('Error parsing the file :', jsonError, yamlError);
reject(new BrunoError('Import collection failed'));
}
}
};
fileReader.onerror = (err) => reject(err);
fileReader.readAsText(files[0]);
});
};
const ensureUrl = (url) => {
let protUrl = url.startsWith('http') ? url : `http://${url}`;
// replace any double or triple slashes
return protUrl.replace(/([^:]\/)\/+/g, '$1');
};
const buildEmptyJsonBody = (bodySchema) => {
let _jsonBody = {};
each(bodySchema.properties || {}, (prop, name) => {
if (prop.type === 'object') {
_jsonBody[name] = buildEmptyJsonBody(prop);
// handle arrays
} else if (prop.type === 'array') {
_jsonBody[name] = [];
} else {
_jsonBody[name] = '';
}
});
return _jsonBody;
};
const transformOpenapiRequestItem = (request) => {
let _operationObject = request.operationObject;
let operationName = _operationObject.operationId || _operationObject.summary || _operationObject.description;
if (!operationName) {
operationName = `${request.method} ${request.path}`;
}
const brunoRequestItem = {
uid: uuid(),
name: operationName,
type: 'http-request',
request: {
url: ensureUrl(request.global.server + '/' + request.path),
method: request.method.toUpperCase(),
auth: {
mode: 'none',
basic: null,
bearer: null
},
headers: [],
params: [],
body: {
mode: 'none',
json: null,
text: null,
xml: null,
formUrlEncoded: [],
multipartForm: []
}
}
};
each(_operationObject.parameters || [], (param) => {
if (param.in === 'query') {
brunoRequestItem.request.params.push({
uid: uuid(),
name: param.name,
value: '',
description: param.description || '',
enabled: param.required
});
} else if (param.in === 'header') {
brunoRequestItem.request.headers.push({
uid: uuid(),
name: param.name,
value: '',
description: param.description || '',
enabled: param.required
});
}
});
let auth;
// allow operation override
if (_operationObject.security && _operationObject.security.length > 0) {
let schemeName = Object.keys(_operationObject.security[0])[0];
auth = request.global.security.getScheme(schemeName);
} else if (request.global.security.supported.length > 0) {
auth = request.global.security.supported[0];
}
if (auth) {
if (auth.type === 'http' && auth.scheme === 'basic') {
brunoRequestItem.request.auth.mode = 'basic';
brunoRequestItem.request.auth.basic = {
username: '{{username}}',
password: '{{password}}'
};
} else if (auth.type === 'http' && auth.scheme === 'bearer') {
brunoRequestItem.request.auth.mode = 'bearer';
brunoRequestItem.request.auth.bearer = {
token: '{{token}}'
};
} else if (auth.type === 'apiKey' && auth.in === 'header') {
brunoRequestItem.request.headers.push({
uid: uuid(),
name: auth.name,
value: '{{apiKey}}',
description: 'Authentication header',
enabled: true
});
}
}
// TODO: handle allOf/anyOf/oneOf
if (_operationObject.requestBody) {
let content = get(_operationObject, 'requestBody.content', {});
let mimeType = Object.keys(content)[0];
let body = content[mimeType] || {};
let bodySchema = body.schema;
if (mimeType === 'application/json') {
brunoRequestItem.request.body.mode = 'json';
if (bodySchema && bodySchema.type === 'object') {
let _jsonBody = buildEmptyJsonBody(bodySchema);
brunoRequestItem.request.body.json = JSON.stringify(_jsonBody, null, 2);
}
} else if (mimeType === 'application/x-www-form-urlencoded') {
brunoRequestItem.request.body.mode = 'formUrlEncoded';
if (bodySchema && bodySchema.type === 'object') {
each(bodySchema.properties || {}, (prop, name) => {
brunoRequestItem.request.body.formUrlEncoded.push({
uid: uuid(),
name: name,
value: '',
description: prop.description || '',
enabled: true
});
});
}
} else if (mimeType === 'multipart/form-data') {
brunoRequestItem.request.body.mode = 'multipartForm';
if (bodySchema && bodySchema.type === 'object') {
each(bodySchema.properties || {}, (prop, name) => {
brunoRequestItem.request.body.multipartForm.push({
uid: uuid(),
name: name,
value: '',
description: prop.description || '',
enabled: true
});
});
}
} else if (mimeType === 'text/plain') {
brunoRequestItem.request.body.mode = 'text';
brunoRequestItem.request.body.text = '';
} else if (mimeType === 'text/xml') {
brunoRequestItem.request.body.mode = 'xml';
brunoRequestItem.request.body.xml = '';
}
}
return brunoRequestItem;
};
const resolveRefs = (spec, components = spec.components) => {
if (!spec || typeof spec !== 'object') {
return spec;
}
if (Array.isArray(spec)) {
return spec.map((item) => resolveRefs(item, components));
}
if ('$ref' in spec) {
const refPath = spec.$ref;
if (refPath.startsWith('#/components/')) {
// Local reference within components
const refKeys = refPath.replace('#/components/', '').split('/');
let ref = components;
for (const key of refKeys) {
if (ref[key]) {
ref = ref[key];
} else {
// Handle invalid references gracefully?
return spec;
}
}
return resolveRefs(ref, components);
} else {
// Handle external references (not implemented here)
// You would need to fetch the external reference and resolve it.
// Example: Fetch and resolve an external reference from a URL.
}
}
// Recursively resolve references in nested objects
for (const prop in spec) {
spec[prop] = resolveRefs(spec[prop], components);
}
return spec;
};
const groupRequestsByTags = (requests) => {
let _groups = {};
let ungrouped = [];
each(requests, (request) => {
let tags = request.operationObject.tags || [];
if (tags.length > 0) {
let tag = tags[0]; // take first tag
if (!_groups[tag]) {
_groups[tag] = [];
}
_groups[tag].push(request);
} else {
ungrouped.push(request);
}
});
let groups = Object.keys(_groups).map((groupName) => {
return {
name: groupName,
requests: _groups[groupName]
};
});
return [groups, ungrouped];
};
const getDefaultUrl = (serverObject) => {
let url = serverObject.url;
if (serverObject.variables) {
each(serverObject.variables, (variable, variableName) => {
let sub = variable.default || (variable.enum ? variable.enum[0] : `{{${variableName}}}`);
url = url.replace(`{${variableName}}`, sub);
});
}
return url;
};
const getSecurity = (apiSpec) => {
let supportedSchemes = apiSpec.security || [];
if (supportedSchemes.length === 0) {
return {
supported: []
};
}
let securitySchemes = get(apiSpec, 'components.securitySchemes', {});
if (Object.keys(securitySchemes) === 0) {
return {
supported: []
};
}
return {
supported: supportedSchemes.map((scheme) => {
var schemeName = Object.keys(scheme)[0];
return securitySchemes[schemeName];
}),
schemes: securitySchemes,
getScheme: (schemeName) => {
return securitySchemes[schemeName];
}
};
};
const parseOpenApiCollection = (data) => {
const brunoCollection = {
name: '',
uid: uuid(),
version: '1',
items: [],
environments: []
};
return new Promise((resolve, reject) => {
try {
const collectionData = resolveRefs(data);
if (!collectionData) {
reject(new BrunoError('Invalid OpenAPI collection. Failed to resolve refs.'));
return;
}
// Currently parsing of openapi spec is "do your best", that is
// allows "invalid" openapi spec
// assumes v3 if not defined. v2 no supported yet
if (collectionData.openapi && !collectionData.openapi.startsWith('3')) {
reject(new BrunoError('Only OpenAPI v3 is supported currently.'));
return;
}
// TODO what if info.title not defined?
brunoCollection.name = collectionData.info.title;
let servers = collectionData.servers || [];
let baseUrl = servers[0] ? getDefaultUrl(servers[0]) : '';
let securityConfig = getSecurity(collectionData);
let allRequests = Object.entries(collectionData.paths)
.map(([path, methods]) => {
return Object.entries(methods)
.filter(([method, op]) => {
return ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].includes(
method.toLowerCase()
);
})
.map(([method, operationObject]) => {
return {
method: method,
path: path,
operationObject: operationObject,
global: {
server: baseUrl,
security: securityConfig
}
};
});
})
.reduce((acc, val) => acc.concat(val), []); // flatten
let [groups, ungroupedRequests] = groupRequestsByTags(allRequests);
let brunoFolders = groups.map((group) => {
return {
uid: uuid(),
name: group.name,
type: 'folder',
items: group.requests.map(transformOpenapiRequestItem)
};
});
let ungroupedItems = ungroupedRequests.map(transformOpenapiRequestItem);
let brunoCollectionItems = brunoFolders.concat(ungroupedItems);
brunoCollection.items = brunoCollectionItems;
resolve(brunoCollection);
} catch (err) {
console.error(err);
reject(new BrunoError('An error occurred while parsing the OpenAPI collection'));
}
});
};
const importCollection = () => {
return new Promise((resolve, reject) => {
fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' })
.then(readFile)
.then(parseOpenApiCollection)
.then(transformItemsInCollection)
.then(hydrateSeqInCollection)
.then(validateSchema)
.then((collection) => resolve(collection))
.catch((err) => {
console.error(err);
reject(new BrunoError('Import collection failed: ' + err.message));
});
});
};
export default importCollection;

View File

@ -68,13 +68,21 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
if (!brunoRequestItem.request.script) { if (!brunoRequestItem.request.script) {
brunoRequestItem.request.script = {}; brunoRequestItem.request.script = {};
} }
brunoRequestItem.request.script.req = event.script.exec[0]; if (Array.isArray(event.script.exec[0])) {
brunoRequestItem.request.script.req = event.script.exec[0].map((line) => `// ${line}`).join('\n');
} else {
brunoRequestItem.request.script.req = `// ${event.script.exec[0]} `;
}
} }
if (event.listen === 'test' && event.script && event.script.exec) { if (event.listen === 'test' && event.script && event.script.exec) {
if (!brunoRequestItem.request.tests) { if (!brunoRequestItem.request.tests) {
brunoRequestItem.request.tests = {}; brunoRequestItem.request.tests = {};
} }
brunoRequestItem.request.tests = event.script.exec[0]; if (Array.isArray(event.script.exec[0])) {
brunoRequestItem.request.tests = event.script.exec[0].map((line) => `// ${line}`).join('\n');
} else {
brunoRequestItem.request.tests = `// ${event.script.exec[0]} `;
}
} }
}); });
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@usebruno/cli", "name": "@usebruno/cli",
"version": "0.14.0", "version": "0.15.1",
"license": "MIT", "license": "MIT",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {
@ -24,8 +24,8 @@
"package.json" "package.json"
], ],
"dependencies": { "dependencies": {
"@usebruno/js": "0.8.0", "@usebruno/js": "0.9.1",
"@usebruno/lang": "0.8.0", "@usebruno/lang": "0.9.0",
"axios": "^1.5.1", "axios": "^1.5.1",
"chai": "^4.3.7", "chai": "^4.3.7",
"chalk": "^3.0.0", "chalk": "^3.0.0",

View File

@ -12,11 +12,10 @@ const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime } = require('@use
const { stripExtension } = require('../utils/filesystem'); const { stripExtension } = require('../utils/filesystem');
const { getOptions } = require('../utils/bru'); const { getOptions } = require('../utils/bru');
const https = require('https'); const https = require('https');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { HttpProxyAgent } = require('http-proxy-agent'); const { HttpProxyAgent } = require('http-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent'); const { SocksProxyAgent } = require('socks-proxy-agent');
const { makeAxiosInstance } = require('../utils/axios-instance'); const { makeAxiosInstance } = require('../utils/axios-instance');
const { shouldUseProxy } = require('../utils/proxy-util'); const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
const runSingleRequest = async function ( const runSingleRequest = async function (
filename, filename,
@ -152,7 +151,7 @@ const runSingleRequest = async function (
request.httpsAgent = socksProxyAgent; request.httpsAgent = socksProxyAgent;
request.httpAgent = socksProxyAgent; request.httpAgent = socksProxyAgent;
} else { } else {
request.httpsAgent = new HttpsProxyAgent( request.httpsAgent = new PatchedHttpsProxyAgent(
proxyUri, proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
); );

View File

@ -1,4 +1,6 @@
const parseUrl = require('url').parse; const parseUrl = require('url').parse;
const { isEmpty } = require('lodash');
const { HttpsProxyAgent } = require('https-proxy-agent');
const DEFAULT_PORTS = { const DEFAULT_PORTS = {
ftp: 21, ftp: 21,
@ -9,7 +11,7 @@ const DEFAULT_PORTS = {
wss: 443 wss: 443
}; };
/** /**
* check for proxy bypass, Copied form 'proxy-from-env' * check for proxy bypass, copied form 'proxy-from-env'
*/ */
const shouldUseProxy = (url, proxyBypass) => { const shouldUseProxy = (url, proxyBypass) => {
if (proxyBypass === '*') { if (proxyBypass === '*') {
@ -39,7 +41,6 @@ const shouldUseProxy = (url, proxyBypass) => {
if (!dontProxyFor) { if (!dontProxyFor) {
return true; // Skip zero-length hosts. return true; // Skip zero-length hosts.
} }
const parsedProxy = dontProxyFor.match(/^(.+):(\d+)$/); const parsedProxy = dontProxyFor.match(/^(.+):(\d+)$/);
let parsedProxyHostname = parsedProxy ? parsedProxy[1] : dontProxyFor; let parsedProxyHostname = parsedProxy ? parsedProxy[1] : dontProxyFor;
const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2]) : 0; const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2]) : 0;
@ -61,6 +62,24 @@ const shouldUseProxy = (url, proxyBypass) => {
}); });
}; };
/**
* Patched version of HttpsProxyAgent to get around a bug that ignores
* options like ca and rejectUnauthorized when upgrading the socket to TLS:
* https://github.com/TooTallNate/proxy-agents/issues/194
*/
class PatchedHttpsProxyAgent extends HttpsProxyAgent {
constructor(proxy, opts) {
super(proxy, opts);
this.constructorOpts = opts;
}
async connect(req, opts) {
const combinedOpts = { ...this.constructorOpts, ...opts };
return super.connect(req, combinedOpts);
}
}
module.exports = { module.exports = {
shouldUseProxy shouldUseProxy,
PatchedHttpsProxyAgent
}; };

View File

@ -1,5 +1,5 @@
{ {
"version": "v0.26.0", "version": "v0.27.2",
"name": "bruno", "name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs", "description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com", "homepage": "https://www.usebruno.com",
@ -20,15 +20,15 @@
}, },
"dependencies": { "dependencies": {
"@aws-sdk/credential-providers": "^3.425.0", "@aws-sdk/credential-providers": "^3.425.0",
"@usebruno/js": "0.8.0", "@usebruno/js": "0.9.1",
"@usebruno/lang": "0.8.0", "@usebruno/lang": "0.9.0",
"@usebruno/schema": "0.5.0", "@usebruno/schema": "0.6.0",
"about-window": "^1.15.2", "about-window": "^1.15.2",
"aws4-axios": "^3.3.0", "aws4-axios": "^3.3.0",
"axios": "^1.5.1", "axios": "^1.5.1",
"chai": "^4.3.7", "chai": "^4.3.7",
"chai-string": "^1.5.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"content-disposition": "^0.5.4",
"decomment": "^0.9.5", "decomment": "^0.9.5",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"electron-is-dev": "^2.0.0", "electron-is-dev": "^2.0.0",
@ -42,7 +42,9 @@
"http-proxy-agent": "^7.0.0", "http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.2",
"is-valid-path": "^0.1.1", "is-valid-path": "^0.1.1",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mime-types": "^2.1.35",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"node-machine-id": "^1.1.12", "node-machine-id": "^1.1.12",

View File

@ -12,6 +12,14 @@ const template = [
ipcMain.emit('main:open-collection'); ipcMain.emit('main:open-collection');
} }
}, },
{
label: 'Preferences',
accelerator: 'CommandOrControl+,',
click() {
ipcMain.emit('main:open-preferences');
}
},
{ type: 'separator' },
{ role: 'quit' } { role: 'quit' }
] ]
}, },

View File

@ -67,7 +67,21 @@ app.on('ready', async () => {
slashes: true slashes: true
}); });
mainWindow.loadURL(url); mainWindow.loadURL(url).catch((reason) => {
console.error(`Error: Failed to load URL: "${url}" (Electron shows a blank screen because of this).`);
console.error('Original message:', reason);
if (isDev) {
console.error(
'Could not connect to Next.Js dev server, is it running?' +
' Start the dev server using "npm run dev:web" and restart electron'
);
} else {
console.error(
'If you are using an official production build: the above error is most likely a bug! ' +
' Please report this under: https://github.com/usebruno/bruno/issues'
);
}
});
watcher = new Watcher(); watcher = new Watcher();
const handleBoundsChange = () => { const handleBoundsChange = () => {

View File

@ -3,9 +3,12 @@ const fs = require('fs');
const qs = require('qs'); const qs = require('qs');
const https = require('https'); const https = require('https');
const axios = require('axios'); const axios = require('axios');
const path = require('path');
const decomment = require('decomment'); const decomment = require('decomment');
const Mustache = require('mustache'); const Mustache = require('mustache');
const FormData = require('form-data'); const FormData = require('form-data');
const contentDispositionParser = require('content-disposition');
const mime = require('mime-types');
const { ipcMain } = require('electron'); const { ipcMain } = require('electron');
const { forOwn, extend, each, get, compact } = require('lodash'); const { forOwn, extend, each, get, compact } = require('lodash');
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js'); const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
@ -19,12 +22,12 @@ const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper');
const { preferencesUtil } = require('../../store/preferences'); const { preferencesUtil } = require('../../store/preferences');
const { getProcessEnvVars } = require('../../store/process-env'); const { getProcessEnvVars } = require('../../store/process-env');
const { getBrunoConfig } = require('../../store/bruno-config'); const { getBrunoConfig } = require('../../store/bruno-config');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { HttpProxyAgent } = require('http-proxy-agent'); const { HttpProxyAgent } = require('http-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent'); const { SocksProxyAgent } = require('socks-proxy-agent');
const { makeAxiosInstance } = require('./axios-instance'); const { makeAxiosInstance } = require('./axios-instance');
const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper'); const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');
const { shouldUseProxy } = require('../../utils/proxy-util'); const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../../utils/proxy-util');
const { chooseFileToSave, writeBinaryFile } = require('../../utils/filesystem');
// override the default escape function to prevent escaping // override the default escape function to prevent escaping
Mustache.escape = function (value) { Mustache.escape = function (value) {
@ -84,7 +87,14 @@ const getSize = (data) => {
return 0; return 0;
}; };
const configureRequest = async (collectionUid, request, envVars, collectionVariables, processEnvVars) => { const configureRequest = async (
collectionUid,
request,
envVars,
collectionVariables,
processEnvVars,
collectionPath
) => {
const httpsAgentRequestFields = {}; const httpsAgentRequestFields = {};
if (!preferencesUtil.shouldVerifyTls()) { if (!preferencesUtil.shouldVerifyTls()) {
httpsAgentRequestFields['rejectUnauthorized'] = false; httpsAgentRequestFields['rejectUnauthorized'] = false;
@ -101,18 +111,29 @@ const configureRequest = async (collectionUid, request, envVars, collectionVaria
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []); const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
for (let clientCert of clientCertConfig) { for (let clientCert of clientCertConfig) {
const domain = interpolateString(clientCert.domain, interpolationOptions); const domain = interpolateString(clientCert.domain, interpolationOptions);
const certFilePath = interpolateString(clientCert.certFilePath, interpolationOptions);
const keyFilePath = interpolateString(clientCert.keyFilePath, interpolationOptions); let certFilePath = interpolateString(clientCert.certFilePath, interpolationOptions);
certFilePath = path.isAbsolute(certFilePath) ? certFilePath : path.join(collectionPath, certFilePath);
let keyFilePath = interpolateString(clientCert.keyFilePath, interpolationOptions);
keyFilePath = path.isAbsolute(keyFilePath) ? keyFilePath : path.join(collectionPath, keyFilePath);
if (domain && certFilePath && keyFilePath) { if (domain && certFilePath && keyFilePath) {
const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*'); const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
if (request.url.match(hostRegex)) { if (request.url.match(hostRegex)) {
try { try {
httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath); httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);
} catch (err) {
console.log('Error reading cert file', err);
}
try {
httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath); httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);
} catch (err) { } catch (err) {
console.log('Error reading cert/key file', err); console.log('Error reading key file', err);
} }
httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions); httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions);
break; break;
} }
@ -121,7 +142,7 @@ const configureRequest = async (collectionUid, request, envVars, collectionVaria
// proxy configuration // proxy configuration
let proxyConfig = get(brunoConfig, 'proxy', {}); let proxyConfig = get(brunoConfig, 'proxy', {});
let proxyEnabled = get(proxyConfig, 'use', false); let proxyEnabled = get(proxyConfig, 'enabled', false);
if (proxyEnabled === 'global') { if (proxyEnabled === 'global') {
proxyConfig = preferencesUtil.getGlobalProxyConfig(); proxyConfig = preferencesUtil.getGlobalProxyConfig();
proxyEnabled = get(proxyConfig, 'enabled', false); proxyEnabled = get(proxyConfig, 'enabled', false);
@ -149,7 +170,7 @@ const configureRequest = async (collectionUid, request, envVars, collectionVaria
request.httpsAgent = socksProxyAgent; request.httpsAgent = socksProxyAgent;
request.httpAgent = socksProxyAgent; request.httpAgent = socksProxyAgent;
} else { } else {
request.httpsAgent = new HttpsProxyAgent( request.httpsAgent = new PatchedHttpsProxyAgent(
proxyUri, proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
); );
@ -189,13 +210,6 @@ const parseDataFromResponse = (response) => {
}; };
const registerNetworkIpc = (mainWindow) => { const registerNetworkIpc = (mainWindow) => {
// handler for sending http request
ipcMain.handle('send-http-request', async (event, item, collection, environment, collectionVariables) => {
const collectionUid = collection.uid;
const collectionPath = collection.pathname;
const cancelTokenUid = uuid();
const requestUid = uuid();
const onConsoleLog = (type, args) => { const onConsoleLog = (type, args) => {
console[type](...args); console[type](...args);
@ -205,38 +219,17 @@ const registerNetworkIpc = (mainWindow) => {
}); });
}; };
mainWindow.webContents.send('main:run-request-event', { const runPreRequest = async (
type: 'request-queued', request,
requestUid, requestUid,
envVars,
collectionPath,
collectionRoot,
collectionUid, collectionUid,
itemUid: item.uid, collectionVariables,
cancelTokenUid processEnvVars,
}); scriptingConfig
) => {
const collectionRoot = get(collection, 'root', {});
const _request = item.draft ? item.draft.request : item.request;
const request = prepareRequest(_request, collectionRoot);
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
try {
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
if (request.headers && request.headers['content-type'] === 'multipart/form-data') {
const form = new FormData();
forOwn(request.data, (value, key) => {
form.append(key, value);
});
extend(request.headers, form.getHeaders());
request.data = form;
}
const cancelToken = axios.CancelToken.source();
request.cancelToken = cancelToken.token;
saveCancelToken(cancelTokenUid, cancelToken);
// run pre-request vars // run pre-request vars
const preRequestVars = get(request, 'vars.req', []); const preRequestVars = get(request, 'vars.req', []);
if (preRequestVars?.length) { if (preRequestVars?.length) {
@ -261,9 +254,7 @@ const registerNetworkIpc = (mainWindow) => {
} }
// run pre-request script // run pre-request script
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join( const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(os.EOL);
os.EOL
);
if (requestScript?.length) { if (requestScript?.length) {
const scriptRuntime = new ScriptRuntime(); const scriptRuntime = new ScriptRuntime();
const result = await scriptRuntime.runRequestScript( const result = await scriptRuntime.runRequestScript(
@ -285,74 +276,27 @@ const registerNetworkIpc = (mainWindow) => {
}); });
} }
// interpolate variables inside request
interpolateVars(request, envVars, collectionVariables, processEnvVars); interpolateVars(request, envVars, collectionVariables, processEnvVars);
// stringify the request url encoded params // stringify the request url encoded params
if (request.headers['content-type'] === 'application/x-www-form-urlencoded') { if (request.headers['content-type'] === 'application/x-www-form-urlencoded') {
request.data = qs.stringify(request.data); request.data = qs.stringify(request.data);
} }
};
// todo: const runPostResponse = async (
// i have no clue why electron can't send the request object
// without safeParseJSON(safeStringifyJSON(request.data))
mainWindow.webContents.send('main:run-request-event', {
type: 'request-sent',
requestSent: {
url: request.url,
method: request.method,
headers: request.headers,
data: safeParseJSON(safeStringifyJSON(request.data)),
timestamp: Date.now()
},
collectionUid,
itemUid: item.uid,
requestUid,
cancelTokenUid
});
const axiosInstance = await configureRequest(
collectionUid,
request, request,
response,
requestUid,
envVars, envVars,
collectionPath,
collectionRoot,
collectionUid,
collectionVariables, collectionVariables,
processEnvVars processEnvVars,
); scriptingConfig
) => {
let response, responseTime;
try {
/** @type {import('axios').AxiosResponse} */
response = await axiosInstance(request);
// Prevents the duration on leaking to the actual result
responseTime = response.headers.get('request-duration');
response.headers.delete('request-duration');
} catch (error) {
deleteCancelToken(cancelTokenUid);
// if it's a cancel request, don't continue
if (axios.isCancel(error)) {
let error = new Error('Request cancelled');
error.isCancel = true;
return Promise.reject(error);
}
if (error?.response) {
response = error.response;
// Prevents the duration on leaking to the actual result
responseTime = response.headers.get('request-duration');
response.headers.delete('request-duration');
} else {
// if it's not a network error, don't continue
return Promise.reject(error);
}
}
// Continue with the rest of the request lifecycle - post response vars, script, assertions, tests
const { data, dataBuffer } = parseDataFromResponse(response);
response.data = data;
// run post-response vars // run post-response vars
const postResponseVars = get(request, 'vars.res', []); const postResponseVars = get(request, 'vars.res', []);
if (postResponseVars?.length) { if (postResponseVars?.length) {
@ -402,6 +346,122 @@ const registerNetworkIpc = (mainWindow) => {
collectionUid collectionUid
}); });
} }
};
// handler for sending http request
ipcMain.handle('send-http-request', async (event, item, collection, environment, collectionVariables) => {
const collectionUid = collection.uid;
const collectionPath = collection.pathname;
const cancelTokenUid = uuid();
const requestUid = uuid();
mainWindow.webContents.send('main:run-request-event', {
type: 'request-queued',
requestUid,
collectionUid,
itemUid: item.uid,
cancelTokenUid
});
const collectionRoot = get(collection, 'root', {});
const _request = item.draft ? item.draft.request : item.request;
const request = prepareRequest(_request, collectionRoot);
const envVars = getEnvVars(environment);
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collectionUid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
try {
const cancelToken = axios.CancelToken.source();
request.cancelToken = cancelToken.token;
saveCancelToken(cancelTokenUid, cancelToken);
await runPreRequest(
request,
requestUid,
envVars,
collectionPath,
collectionRoot,
collectionUid,
collectionVariables,
processEnvVars,
scriptingConfig
);
// todo:
// i have no clue why electron can't send the request object
// without safeParseJSON(safeStringifyJSON(request.data))
mainWindow.webContents.send('main:run-request-event', {
type: 'request-sent',
requestSent: {
url: request.url,
method: request.method,
headers: request.headers,
data: safeParseJSON(safeStringifyJSON(request.data)),
timestamp: Date.now()
},
collectionUid,
itemUid: item.uid,
requestUid,
cancelTokenUid
});
const axiosInstance = await configureRequest(
collectionUid,
request,
envVars,
collectionVariables,
processEnvVars,
collectionPath
);
let response, responseTime;
try {
/** @type {import('axios').AxiosResponse} */
response = await axiosInstance(request);
// Prevents the duration on leaking to the actual result
responseTime = response.headers.get('request-duration');
response.headers.delete('request-duration');
} catch (error) {
deleteCancelToken(cancelTokenUid);
// if it's a cancel request, don't continue
if (axios.isCancel(error)) {
let error = new Error('Request cancelled');
error.isCancel = true;
return Promise.reject(error);
}
if (error?.response) {
response = error.response;
// Prevents the duration on leaking to the actual result
responseTime = response.headers.get('request-duration');
response.headers.delete('request-duration');
} else {
// if it's not a network error, don't continue
return Promise.reject(error);
}
}
// Continue with the rest of the request lifecycle - post response vars, script, assertions, tests
const { data, dataBuffer } = parseDataFromResponse(response);
response.data = data;
await runPostResponse(
request,
response,
requestUid,
envVars,
collectionPath,
collectionRoot,
collectionUid,
collectionVariables,
processEnvVars,
scriptingConfig
);
// run assertions // run assertions
const assertions = get(request, 'assertions'); const assertions = get(request, 'assertions');
@ -502,18 +562,49 @@ const registerNetworkIpc = (mainWindow) => {
}); });
} }
const processEnvVars = getProcessEnvVars(collection.uid); const requestUid = uuid();
interpolateVars(preparedRequest, envVars, collection.collectionVariables, processEnvVars); const collectionPath = collection.pathname;
const collectionUid = collection.uid;
const collectionVariables = collection.collectionVariables;
const processEnvVars = getProcessEnvVars(collectionUid);
const brunoConfig = getBrunoConfig(collection.uid);
const scriptingConfig = get(brunoConfig, 'scripts', {});
await runPreRequest(
request,
requestUid,
envVars,
collectionPath,
collectionRoot,
collectionUid,
collectionVariables,
processEnvVars,
scriptingConfig
);
const axiosInstance = await configureRequest( const axiosInstance = await configureRequest(
collection.uid, collection.uid,
preparedRequest, preparedRequest,
envVars, envVars,
collection.collectionVariables, collection.collectionVariables,
processEnvVars processEnvVars,
collectionPath
); );
const response = await axiosInstance(preparedRequest); const response = await axiosInstance(preparedRequest);
await runPostResponse(
request,
response,
requestUid,
envVars,
collectionPath,
collectionRoot,
collectionUid,
collectionVariables,
processEnvVars,
scriptingConfig
);
return { return {
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,
@ -544,15 +635,6 @@ const registerNetworkIpc = (mainWindow) => {
const scriptingConfig = get(brunoConfig, 'scripts', {}); const scriptingConfig = get(brunoConfig, 'scripts', {});
const collectionRoot = get(collection, 'root', {}); const collectionRoot = get(collection, 'root', {});
const onConsoleLog = (type, args) => {
console[type](...args);
mainWindow.webContents.send('main:console-log', {
type,
args
});
};
if (!folder) { if (!folder) {
folder = collection; folder = collection;
} }
@ -602,68 +684,22 @@ const registerNetworkIpc = (mainWindow) => {
const _request = item.draft ? item.draft.request : item.request; const _request = item.draft ? item.draft.request : item.request;
const request = prepareRequest(_request, collectionRoot); const request = prepareRequest(_request, collectionRoot);
const requestUid = uuid();
const processEnvVars = getProcessEnvVars(collectionUid); const processEnvVars = getProcessEnvVars(collectionUid);
try { try {
// make axios work in node using form data await runPreRequest(
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
if (request.headers && request.headers['content-type'] === 'multipart/form-data') {
const form = new FormData();
forOwn(request.data, (value, key) => {
form.append(key, value);
});
extend(request.headers, form.getHeaders());
request.data = form;
}
// run pre-request vars
const preRequestVars = get(request, 'vars.req', []);
if (preRequestVars && preRequestVars.length) {
const varsRuntime = new VarsRuntime();
const result = varsRuntime.runPreRequestVars(
preRequestVars,
request, request,
requestUid,
envVars, envVars,
collectionVariables,
collectionPath
);
if (result) {
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
collectionUid
});
}
}
// run pre-request script
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(
os.EOL
);
if (requestScript?.length) {
const scriptRuntime = new ScriptRuntime();
const result = await scriptRuntime.runRequestScript(
decomment(requestScript),
request,
envVars,
collectionVariables,
collectionPath, collectionPath,
onConsoleLog, collectionRoot,
collectionUid,
collectionVariables,
processEnvVars, processEnvVars,
scriptingConfig scriptingConfig
); );
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
collectionUid
});
}
// interpolate variables inside request
interpolateVars(request, envVars, collectionVariables, processEnvVars);
// todo: // todo:
// i have no clue why electron can't send the request object // i have no clue why electron can't send the request object
// without safeParseJSON(safeStringifyJSON(request.data)) // without safeParseJSON(safeStringifyJSON(request.data))
@ -683,7 +719,8 @@ const registerNetworkIpc = (mainWindow) => {
request, request,
envVars, envVars,
collectionVariables, collectionVariables,
processEnvVars processEnvVars,
collectionPath
); );
timeStart = Date.now(); timeStart = Date.now();
@ -738,55 +775,19 @@ const registerNetworkIpc = (mainWindow) => {
} }
} }
// run post-response vars await runPostResponse(
const postResponseVars = get(request, 'vars.res', []);
if (postResponseVars?.length) {
const varsRuntime = new VarsRuntime();
const result = varsRuntime.runPostResponseVars(
postResponseVars,
request, request,
response, response,
requestUid,
envVars, envVars,
collectionVariables,
collectionPath, collectionPath,
processEnvVars collectionRoot,
); collectionUid,
if (result) {
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
collectionUid
});
}
}
// run response script
const responseScript = compact([
get(collectionRoot, 'request.script.res'),
get(request, 'script.res')
]).join(os.EOL);
if (responseScript && responseScript.length) {
const scriptRuntime = new ScriptRuntime();
const result = await scriptRuntime.runResponseScript(
decomment(responseScript),
request,
response,
envVars,
collectionVariables, collectionVariables,
collectionPath,
onConsoleLog,
processEnvVars, processEnvVars,
scriptingConfig scriptingConfig
); );
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
collectionUid
});
}
// run assertions // run assertions
const assertions = get(item, 'request.assertions'); const assertions = get(item, 'request.assertions');
if (assertions) { if (assertions) {
@ -862,6 +863,51 @@ const registerNetworkIpc = (mainWindow) => {
} }
} }
); );
// save response to file
ipcMain.handle('renderer:save-response-to-file', async (event, response, url) => {
try {
const getHeaderValue = (headerName) => {
if (response.headers) {
const header = response.headers.find((header) => header[0] === headerName);
if (header && header.length > 1) {
return header[1];
}
}
};
const getFileNameFromContentDispositionHeader = () => {
const contentDisposition = getHeaderValue('content-disposition');
try {
const disposition = contentDispositionParser.parse(contentDisposition);
return disposition && disposition.parameters['filename'];
} catch (error) {}
};
const getFileNameFromUrlPath = () => {
const lastPathLevel = new URL(url).pathname.split('/').pop();
if (lastPathLevel && /\..+/.exec(lastPathLevel)) {
return lastPathLevel;
}
};
const getFileNameBasedOnContentTypeHeader = () => {
const contentType = getHeaderValue('content-type');
const extension = (contentType && mime.extension(contentType)) || 'txt';
return `response.${extension}`;
};
const fileName =
getFileNameFromContentDispositionHeader() || getFileNameFromUrlPath() || getFileNameBasedOnContentTypeHeader();
const filePath = await chooseFileToSave(mainWindow, fileName);
if (filePath) {
await writeBinaryFile(filePath, Buffer.from(response.dataBuffer, 'base64'));
}
} catch (error) {
return Promise.reject(error);
}
});
}; };
module.exports = registerNetworkIpc; module.exports = registerNetworkIpc;

View File

@ -137,6 +137,15 @@ const prepareRequest = (request, collectionRoot) => {
each(enabledParams, (p) => (params[p.name] = p.value)); each(enabledParams, (p) => (params[p.name] = p.value));
axiosRequest.headers['content-type'] = 'multipart/form-data'; axiosRequest.headers['content-type'] = 'multipart/form-data';
axiosRequest.data = params; axiosRequest.data = params;
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
const form = new FormData();
forOwn(axiosRequest.data, (value, key) => {
form.append(key, value);
});
extend(axiosRequest.headers, form.getHeaders());
axiosRequest.data = form;
} }
if (request.body.mode === 'graphql') { if (request.body.mode === 'graphql') {

View File

@ -23,6 +23,10 @@ const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
} }
}); });
ipcMain.on('main:open-preferences', () => {
mainWindow.webContents.send('main:open-preferences');
});
ipcMain.handle('renderer:save-preferences', async (event, preferences) => { ipcMain.handle('renderer:save-preferences', async (event, preferences) => {
try { try {
await savePreferences(preferences); await savePreferences(preferences);

View File

@ -60,6 +60,14 @@ const writeFile = async (pathname, content) => {
} }
}; };
const writeBinaryFile = async (pathname, content) => {
try {
fs.writeFileSync(pathname, content);
} catch (err) {
return Promise.reject(err);
}
};
const hasJsonExtension = (filename) => { const hasJsonExtension = (filename) => {
if (!filename || typeof filename !== 'string') return false; if (!filename || typeof filename !== 'string') return false;
return ['json'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`)); return ['json'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`));
@ -95,6 +103,14 @@ const browseDirectory = async (win) => {
return isDirectory(resolvedPath) ? resolvedPath : false; return isDirectory(resolvedPath) ? resolvedPath : false;
}; };
const chooseFileToSave = async (win, preferredFileName = '') => {
const { filePath } = await dialog.showSaveDialog(win, {
defaultPath: preferredFileName
});
return filePath;
};
const searchForFiles = (dir, extension) => { const searchForFiles = (dir, extension) => {
let results = []; let results = [];
const files = fs.readdirSync(dir); const files = fs.readdirSync(dir);
@ -126,10 +142,12 @@ module.exports = {
isDirectory, isDirectory,
normalizeAndResolvePath, normalizeAndResolvePath,
writeFile, writeFile,
writeBinaryFile,
hasJsonExtension, hasJsonExtension,
hasBruExtension, hasBruExtension,
createDirectory, createDirectory,
browseDirectory, browseDirectory,
chooseFileToSave,
searchForFiles, searchForFiles,
searchForBruFiles, searchForBruFiles,
sanitizeDirectoryName sanitizeDirectoryName

View File

@ -1,5 +1,6 @@
const parseUrl = require('url').parse; const parseUrl = require('url').parse;
const { isEmpty } = require('lodash'); const { isEmpty } = require('lodash');
const { HttpsProxyAgent } = require('https-proxy-agent');
const DEFAULT_PORTS = { const DEFAULT_PORTS = {
ftp: 21, ftp: 21,
@ -61,6 +62,24 @@ const shouldUseProxy = (url, proxyBypass) => {
}); });
}; };
/**
* Patched version of HttpsProxyAgent to get around a bug that ignores options
* such as ca and rejectUnauthorized when upgrading the proxied socket to TLS:
* https://github.com/TooTallNate/proxy-agents/issues/194
*/
class PatchedHttpsProxyAgent extends HttpsProxyAgent {
constructor(proxy, opts) {
super(proxy, opts);
this.constructorOpts = opts;
}
async connect(req, opts) {
const combinedOpts = { ...this.constructorOpts, ...opts };
return super.connect(req, combinedOpts);
}
}
module.exports = { module.exports = {
shouldUseProxy shouldUseProxy,
PatchedHttpsProxyAgent
}; };

View File

@ -1,6 +1,6 @@
{ {
"name": "@usebruno/js", "name": "@usebruno/js",
"version": "0.8.0", "version": "0.9.1",
"license": "MIT", "license": "MIT",
"main": "src/index.js", "main": "src/index.js",
"files": [ "files": [
@ -20,6 +20,7 @@
"axios": "^0.26.0", "axios": "^0.26.0",
"btoa": "^1.2.1", "btoa": "^1.2.1",
"chai": "^4.3.7", "chai": "^4.3.7",
"chai-string": "^1.5.0",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"json-query": "^2.2.2", "json-query": "^2.2.2",

View File

@ -1,6 +1,6 @@
{ {
"name": "@usebruno/lang", "name": "@usebruno/lang",
"version": "0.8.0", "version": "0.9.0",
"license": "MIT", "license": "MIT",
"main": "src/index.js", "main": "src/index.js",
"files": [ "files": [

View File

@ -104,7 +104,7 @@ const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
} }
return _.map(pairList[0], (pair) => { return _.map(pairList[0], (pair) => {
let name = _.keys(pair)[0]; let name = _.keys(pair)[0];
let value = pair[name]; let value = decodeURIComponent(pair[name]);
if (!parseEnabled) { if (!parseEnabled) {
return { return {

View File

@ -154,7 +154,7 @@ ${indentString(body.sparql)}
if (enabled(body.formUrlEncoded).length) { if (enabled(body.formUrlEncoded).length) {
bru += `\n${indentString( bru += `\n${indentString(
enabled(body.formUrlEncoded) enabled(body.formUrlEncoded)
.map((item) => `${item.name}: ${item.value}`) .map((item) => `${item.name}: ${encodeURIComponent(item.value)}`)
.join('\n') .join('\n')
)}`; )}`;
} }
@ -162,7 +162,7 @@ ${indentString(body.sparql)}
if (disabled(body.formUrlEncoded).length) { if (disabled(body.formUrlEncoded).length) {
bru += `\n${indentString( bru += `\n${indentString(
disabled(body.formUrlEncoded) disabled(body.formUrlEncoded)
.map((item) => `~${item.name}: ${item.value}`) .map((item) => `~${item.name}: ${encodeURIComponent(item.value)}`)
.join('\n') .join('\n')
)}`; )}`;
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@usebruno/schema", "name": "@usebruno/schema",
"version": "0.5.0", "version": "0.6.0",
"license": "MIT", "license": "MIT",
"main": "src/index.js", "main": "src/index.js",
"files": [ "files": [

View File

@ -7,7 +7,7 @@ const devServer = async (dir, port) => {
// Build the renderer code and watch the files // Build the renderer code and watch the files
await next.prepare(); await next.prepare();
// NextJS Server // Next.js Server
const server = createServer(requestHandler); const server = createServer(requestHandler);
server.listen(port || 8000, () => { server.listen(port || 8000, () => {

6
publishing.md Normal file
View File

@ -0,0 +1,6 @@
### Publishing Bruno to a new package manager
While our code is open source and available for everyone to use, we kindly request that you reach out to us before considering publication on new package managers. As the creator of Bruno, I hold the trademark `Bruno` for this project and would like to manage its distribution. If you'd like to see Bruno on a new package manager, please raise a GitHub issue.
While the majority of our features are free and open source (which covers REST and GraphQL Apis),
we strive to strike a harmonious balance between open-source principles and sustainability - https://github.com/usebruno/bruno/discussions/269

View File

@ -10,7 +10,7 @@
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
**English** | [Українська](/readme_ua.md) | [Русский](/readme_ru.md) **English** | [Українська](/readme_ua.md) | [Русский](/readme_ru.md) | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md) | [Français](/readme_fr.md)
Bruno is a new and innovative API client, aimed at revolutionizing the status quo represented by Postman and similar tools out there. Bruno is a new and innovative API client, aimed at revolutionizing the status quo represented by Postman and similar tools out there.
@ -38,6 +38,7 @@ Or any version control system of your choice
- [Roadmap](https://github.com/usebruno/bruno/discussions/384) - [Roadmap](https://github.com/usebruno/bruno/discussions/384)
- [Documentation](https://docs.usebruno.com) - [Documentation](https://docs.usebruno.com)
- [Website](https://www.usebruno.com) - [Website](https://www.usebruno.com)
- [Pricing](https://www.usebruno.com/pricing)
- [Download](https://www.usebruno.com/downloads) - [Download](https://www.usebruno.com/downloads)
### Showcase 🎥 ### Showcase 🎥
@ -54,6 +55,10 @@ Woof! If you like project, hit that ⭐ button !!
If Bruno has helped you at work and your teams, please don't forget to share your [testimonials on our GitHub discussion](https://github.com/usebruno/bruno/discussions/343) If Bruno has helped you at work and your teams, please don't forget to share your [testimonials on our GitHub discussion](https://github.com/usebruno/bruno/discussions/343)
### Publishing to New Package Managers
Please see [here](publishing.md) for more information.
### Contribute 👩‍💻🧑‍💻 ### Contribute 👩‍💻🧑‍💻
I am happy that you are looking to improve bruno. Please check out the [contributing guide](contributing.md) I am happy that you are looking to improve bruno. Please check out the [contributing guide](contributing.md)
@ -75,6 +80,16 @@ Even if you are not able to make contributions via code, please don't hesitate t
[Discord](https://discord.com/invite/KgcZUncpjq) <br /> [Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno) [LinkedIn](https://www.linkedin.com/company/usebruno)
### Trademark
**Name**
`Bruno` is a trademark held by [Anoop M D](https://www.helloanoop.com/)
**Logo**
The logo is sourced from [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### License 📄 ### License 📄
[MIT](license.md) [MIT](license.md)

95
readme_de.md Normal file
View File

@ -0,0 +1,95 @@
<br />
<img src="assets/images/logo-transparent.png" width="80"/>
### Bruno - Opensource IDE zum Erkunden und Testen von APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | [Українська](/readme_ua.md) | [Русский](/readme_ru.md) | [Türkçe](/readme_tr.md) | **Deutsch** | [Français](/readme_fr.md)
Bruno ist ein neuer und innovativer API-Client, der den Status Quo von Postman und ähnlichen Tools revolutionieren soll.
Bruno speichert Deine Sammlungen direkt in einem Ordner in Deinem Dateisystem. Wir verwenden eine einfache Textauszeichnungssprache - Bru - um Informationen über API-Anfragen zu speichern.
Du kannst Git oder eine andere Versionskontrolle deiner Wahl verwenden, um an deinen API-Sammlungen gemeinsam mit anderen zu arbeiten.
Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno eine Cloud-Synchronisation hinzuzufügen. Wir schätzen den Schutz Deiner Daten und glauben, dass sie auf Deinem Gerät bleiben sollten. Lies unsere Langzeit-Vision [hier](https://github.com/usebruno/bruno/discussions/269).
![bruno](assets/images/landing-2.png) <br /><br />
### Einsatz auf verschiedensten Plattformen 🖥️
![bruno](assets/images/run-anywhere.png) <br /><br />
### Zusammenarbeiten mit Git 👩‍💻🧑‍💻
oder eine Versionskontrolle Deiner Wahl
![bruno](assets/images/version-control.png) <br /><br />
### Wichtige Links 📌
- [Unsere Langzeit-Vision](https://github.com/usebruno/bruno/discussions/269)
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
- [Dokumentation](https://docs.usebruno.com)
- [Webseite](https://www.usebruno.com)
- [Preise](https://www.usebruno.com/pricing)
- [Download](https://www.usebruno.com/downloads)
### Showcase 🎥
- [Erfahrungsberichte](https://github.com/usebruno/bruno/discussions/343)
- [Wissenswertes](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
### Unterstützung ❤️
Wuff! Wenn Du dieses Projekt magst, klick den ⭐ Button !!
### Teile Erfahrungsberichte 📣
Wenn Bruno Dir bei Deiner Arbeit und in Deinen Teams geholfen hat, vergiss bitte nicht, Deine [Erfahrungsberichte auf unserer GitHub-Diskussion](https://github.com/usebruno/bruno/discussions/343) zu teilen.
### Veröffentlichung in neuen Paketmanagern
Bitte [hier](publishing.md) für mehr Informationen lesen.
### Mitmachen 👩‍💻🧑‍💻
Ich freue mich, dass Du Bruno verbessern willst. Bitte schau Dir den [Leitfaden zum Mitmachen](contributing_de.md) an.
Auch wenn Du nicht in der Lage bist, einen Beitrag in Form von Code zu leisten, zögere bitte nicht, uns Fehler und Funktionswünsche mitzuteilen, die implementiert werden müssen, um Deinen Anwendungsfall zu unterstützen.
### Autoren
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### In Verbindung bleiben 🌐
[Twitter](https://twitter.com/use_bruno) <br />
[Webseite](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno)
### Markenzeichen
**Name**
`Bruno` ist ein Markenzeichen von [Anoop M D](https://www.helloanoop.com/)
**Logo**
Das Logo stammt von [OpenMoji](https://openmoji.org/library/emoji-1F436/). Lizenz: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### Lizenz 📄
[MIT](license.md)

96
readme_fr.md Normal file
View File

@ -0,0 +1,96 @@
<br />
<img src="assets/images/logo-transparent.png" width="80"/>
### Bruno - IDE Opensource pour explorer et tester des APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | [Українська](/readme_ua.md) | [Русский](/readme_ru.md) | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md) | **Français**
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 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](assets/images/landing-2.png) <br /><br />
### Fonctionne sur de multiples platformes 🖥️
![bruno](assets/images/run-anywhere.png) <br /><br />
### Collaborer via Git 👩‍💻🧑‍💻
Ou n'importe quel système de gestion de sources
![bruno](assets/images/version-control.png) <br /><br />
### Liens importants 📌
- [Notre vision à long terme (en anglais)](https://github.com/usebruno/bruno/discussions/269)
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
- [Documentation](https://docs.usebruno.com)
- [Site web](https://www.usebruno.com)
- [Prix](https://www.usebruno.com/pricing)
- [Téléchargement](https://www.usebruno.com/downloads)
### Showcase 🎥
- [Témoignages](https://github.com/usebruno/bruno/discussions/343)
- [Centre de connaissance](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
### Soutien ❤️
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)
### Publier Bruno sur un nouveau gestionnaire de paquets
Veuillez regarder [ici](publishing.md) pour plus d'information.
### Contribuer 👩‍💻🧑‍💻
Je suis heureux de voir que vous cherchez à améliorer Bruno. Merci de consulter le [guide de contribution](contributing_fr.md)
Même si vous n'êtes pas en mesure de contribuer directement via du code, n'hésitez pas à consigner les bogues et les demandes de nouvelles fonctionnalités pour résoudre vos cas d'usage !
### Auteurs
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### Restons en contact 🌐
[Twitter](https://twitter.com/use_bruno) <br />
[Website](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno)
### Marque
**Nom**
`Bruno` est une marque appartenant à [Anoop M D](https://www.helloanoop.com/)
**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 📄
[MIT](license.md)

View File

@ -10,7 +10,7 @@
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | [Українська](/readme_ua.md) | **Русский** [English](/readme.md) | [Українська](/readme_ua.md) | **Русский** | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md) | [Français](/readme_fr.md)
Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами. Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами.

80
readme_tr.md Normal file
View File

@ -0,0 +1,80 @@
<br />
<img src="assets/images/logo-transparent.png" width="80"/>
### Bruno - API'leri keşfetmek ve test etmek için açık kaynaklı IDE.
[![GitHub sürümü](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Web Sitesi](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![İndir](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | [Українська](/readme_ua.md) | [Русский](/readme_ru.md) | **Türkçe** | [Deutsch](/readme_de.md) | [Français](/readme_fr.md)
Bruno, Postman ve benzeri araçlar tarafından temsil edilen statükoda devrim yaratmayı amaçlayan yeni ve yenilikçi bir API istemcisidir.
Bruno koleksiyonlarınızı doğrudan dosya sisteminizdeki bir klasörde saklar. API istekleri hakkındaki bilgileri kaydetmek için düz bir metin biçimlendirme dili olan Bru kullanıyoruz.
API koleksiyonlarınız üzerinde işbirliği yapmak için git veya seçtiğiniz herhangi bir sürüm kontrolünü kullanabilirsiniz.
Bruno yalnızca çevrimdışıdır. Bruno'ya bulut senkronizasyonu eklemek gibi bir planımız yok. Veri gizliliğinize değer veriyoruz ve cihazınızda kalması gerektiğine inanıyoruz. Uzun vadeli vizyonumuzu okuyun [burada](https://github.com/usebruno/bruno/discussions/269)
![bruno](assets/images/landing-2.png) <br /><br />
### Birden fazla platformda çalıştırın 🖥️
![bruno](assets/images/run-anywhere.png) <br /><br />
### Git üzerinden işbirliği yapın 👩‍💻🧑‍💻
Veya seçtiğiniz herhangi bir sürüm kontrol sistemi
![bruno](assets/images/version-control.png) <br /><br />
### Önemli Bağlantılar 📌
- [Uzun Vadeli Vizyonumuz](https://github.com/usebruno/bruno/discussions/269)
- [Yol Haritası](https://github.com/usebruno/bruno/discussions/384)
- [Dokümantasyon](https://docs.usebruno.com)
- [Web sitesi](https://www.usebruno.com)
- [İndir](https://www.usebruno.com/downloads)
### Vitrin 🎥
- [Görüşler](https://github.com/usebruno/bruno/discussions/343)
- [Bilgi Merkezi](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
### Destek ❤️
Woof! Projeyi beğendiyseniz, şu ⭐ düğmesine basın!
### Referansları Paylaşın 📣
Bruno işinizde ve ekiplerinizde size yardımcı olduysa, lütfen [github tartışmamızdaki referanslarınızı](https://github.com/usebruno/bruno/discussions/343) paylaşmayı unutmayın
### Katkıda Bulunun 👩‍💻🧑‍💻
Bruno'yu geliştirmek istemenize sevindim. Lütfen [katkıda bulunma kılavuzu](contributing.md)'na göz atın
Kod yoluyla katkıda bulunamasanız bile, lütfen kullanım durumunuzu çözmek için uygulanması gereken hataları ve özellik isteklerini bildirmekten çekinmeyin.
### Katkıda Bulunanlar
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### İletişimde Kalın 🌐
[Twitter](https://twitter.com/use_bruno) <br />
[Website](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno)
### Lisans 📄
[MIT](license.md)

View File

@ -10,7 +10,7 @@
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com) [![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads) [![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | **Українська** | [Русский](/readme_ru.md) [English](/readme.md) | **Українська** | [Русский](/readme_ru.md) | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md) | [Français](/readme_fr.md)
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman. Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman.