mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-29 11:23:57 +01:00
Merge branch 'main' into feature/add-bru-setNextRequest
This commit is contained in:
commit
3374db1ac8
12
.github/PULL_REQUEST_TEMPLATE.md
vendored
12
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,7 +1,17 @@
|
|||||||
# Description
|
# Description
|
||||||
|
|
||||||
<!-- Explain here the changes your PR introduces and text to help us understand the context of this change. -->
|
<!-- Explain here the changes your PR introduces and text to help us understand the context of this change. -->
|
||||||
|
|
||||||
# Contribution Checklist:
|
### Contribution Checklist:
|
||||||
|
|
||||||
|
- [ ] **The pull request only addresses one issue or adds one feature.**
|
||||||
- [ ] **The pull request does not introduce any breaking changes**
|
- [ ] **The pull request does not introduce any breaking changes**
|
||||||
|
- [ ] **I have added screenshots or gifs to help explain the change if applicable.**
|
||||||
- [ ] **I have read the [contribution guidelines](https://github.com/usebruno/bruno/blob/main/contributing.md).**
|
- [ ] **I have read the [contribution guidelines](https://github.com/usebruno/bruno/blob/main/contributing.md).**
|
||||||
- [ ] **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.
|
||||||
|
|
||||||
|
### Publishing to New Package Managers
|
||||||
|
|
||||||
|
Please see [here](../publishing.md) for more information.
|
||||||
|
5
.github/workflows/release-snap.yml
vendored
5
.github/workflows/release-snap.yml
vendored
@ -27,7 +27,10 @@ jobs:
|
|||||||
run: npm install --legacy-peer-deps
|
run: npm install --legacy-peer-deps
|
||||||
|
|
||||||
- name: Build Electron app
|
- name: Build Electron app
|
||||||
run: npm run build:electron:snap
|
run: |
|
||||||
|
npm run build:bruno-query
|
||||||
|
npm run build:graphql-docs
|
||||||
|
npm run build:electron:snap
|
||||||
|
|
||||||
- name: Install Snapcraft
|
- name: Install Snapcraft
|
||||||
run: |
|
run: |
|
||||||
|
22
.github/workflows/unit-tests.yml
vendored
22
.github/workflows/unit-tests.yml
vendored
@ -5,18 +5,16 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
tests:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version-file: '.nvmrc'
|
||||||
- name: Check package-lock.json
|
|
||||||
run: npm ci
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm i --legacy-peer-deps
|
run: npm ci --legacy-peer-deps
|
||||||
- name: Test Package bruno-query
|
- name: Test Package bruno-query
|
||||||
run: npm run test --workspace=packages/bruno-query
|
run: npm run test --workspace=packages/bruno-query
|
||||||
- name: Build Package bruno-query
|
- name: Build Package bruno-query
|
||||||
@ -33,3 +31,15 @@ jobs:
|
|||||||
run: npm run test --workspace=packages/bruno-cli
|
run: npm run test --workspace=packages/bruno-cli
|
||||||
- name: Test Package bruno-electron
|
- name: Test Package bruno-electron
|
||||||
run: npm run test --workspace=packages/bruno-electron
|
run: npm run test --workspace=packages/bruno-electron
|
||||||
|
|
||||||
|
prettier:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci --legacy-peer-deps
|
||||||
|
- name: Run Prettier
|
||||||
|
run: npm run test:prettier:web
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
**English** | [Русский](/contributing_ru.md)
|
**English** | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md)
|
||||||
|
|
||||||
## Lets make bruno better, together !!
|
## Lets make bruno better, together !!
|
||||||
|
|
||||||
@ -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
37
contributing_de.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
[English](/contributing.md) | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | **Deutsch**
|
||||||
|
|
||||||
|
## 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 NextJs 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
|
@ -1,4 +1,4 @@
|
|||||||
[English](/contributing.md) | **Русский**
|
[English](/contributing.md) | [Українська](/contributing_ua.md) | **Русский** | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md)
|
||||||
|
|
||||||
## Давайте вместе сделаем Бруно лучше!!!
|
## Давайте вместе сделаем Бруно лучше!!!
|
||||||
|
|
||||||
|
37
contributing_tr.md
Normal file
37
contributing_tr.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
[English](/readme.md) | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | **Türkçe** | [Deutsch](/contributing_de.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, NextJs 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
|
37
contributing_ua.md
Normal file
37
contributing_ua.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
[English](/contributing.md) | **Українська** | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md)
|
||||||
|
|
||||||
|
## Давайте зробимо Bruno краще, разом !!
|
||||||
|
|
||||||
|
Я дуже радий що Ви бажаєте покращити Bruno. Нижче наведені вказівки як розпочати розробку Bruno на Вашому комп'ютері.
|
||||||
|
|
||||||
|
### Стек технологій
|
||||||
|
|
||||||
|
Bruno побудований на NextJs та React. Також для десктопної версії (яка підтримує локальні колекції) використовується Electron
|
||||||
|
|
||||||
|
Бібліотеки, які ми використовуємо
|
||||||
|
|
||||||
|
- CSS - Tailwind
|
||||||
|
- Редактори коду - Codemirror
|
||||||
|
- Керування станом - Redux
|
||||||
|
- Іконки - Tabler Icons
|
||||||
|
- Форми - formik
|
||||||
|
- Валідація по схемі - Yup
|
||||||
|
- Клієнт запитів - axios
|
||||||
|
- Спостерігач за файловою системою - chokidar
|
||||||
|
|
||||||
|
### Залежності
|
||||||
|
|
||||||
|
Вам знадобиться [Node v18.x або остання LTS версія](https://nodejs.org/en/) та npm 8.x. Ми використовуєм npm workspaces в цьому проекті
|
||||||
|
|
||||||
|
### Починаєм писати код
|
||||||
|
|
||||||
|
Будь ласка, зверніться до [development_ua.md](docs/development_ua.md) за інструкціями щодо запуску локального середовища розробки.
|
||||||
|
|
||||||
|
### Створення Pull Request-ів
|
||||||
|
|
||||||
|
- Будь ласка, робіть PR-и маленькими і сфокусованими на одній речі
|
||||||
|
- Будь ласка, слідуйте формату назв гілок
|
||||||
|
- feature/[назва feature]: Така гілка має містити зміни лише щодо конкретної feature
|
||||||
|
- Приклад: feature/dark-mode
|
||||||
|
- bugfix/[назва баґу]: Така гілка має містити лише виправлення конкретного багу
|
||||||
|
- Приклад: bugfix/bug-1
|
@ -1,4 +1,4 @@
|
|||||||
**English** | [Русский](/docs/development_ru.md)
|
**English** | [Українська](/docs/development_ua.md) | [Русский](/docs/development_ru.md) | [Deutsch](/docs/development_de.md)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
55
docs/development_de.md
Normal file
55
docs/development_de.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
[English](/docs/development.md) | [Українська](/docs/development_ua.md) | [Русский](/docs/development_ru.md) | **Deutsch**
|
||||||
|
|
||||||
|
## Entwicklung
|
||||||
|
|
||||||
|
Bruno wird als Desktop-Anwendung entwickelt. Um die App zu starten, musst Du zuerst die NextJs-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
|
||||||
|
```
|
@ -1,4 +1,4 @@
|
|||||||
[English](/docs/development.md) | **Русский**
|
[English](/docs/development.md) | [Українська](/docs/development_ua.md) | **Русский** | [Deutsch](/docs/development_de.md)
|
||||||
|
|
||||||
## Разработка
|
## Разработка
|
||||||
|
|
||||||
|
55
docs/development_ua.md
Normal file
55
docs/development_ua.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
[English](/docs/development.md) | **Українська** | [Русский](/docs/development_ru.md) | [Deutsch](/docs/development_de.md)
|
||||||
|
|
||||||
|
## Розробка
|
||||||
|
|
||||||
|
Bruno розробляється як декстопний застосунок. Вам потрібно запустити nextjs в одній сесії терміналу, та запустити застосунок Electron в іншій сесії терміналу.
|
||||||
|
|
||||||
|
### Залежності
|
||||||
|
|
||||||
|
- NodeJS v18
|
||||||
|
|
||||||
|
### Локальна розробка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Використовуйте nodejs 18-ї версії
|
||||||
|
nvm use
|
||||||
|
|
||||||
|
# встановіть залежності
|
||||||
|
npm i --legacy-peer-deps
|
||||||
|
|
||||||
|
# зберіть документацію graphql
|
||||||
|
npm run build:graphql-docs
|
||||||
|
|
||||||
|
# зберіть bruno query
|
||||||
|
npm run build:bruno-query
|
||||||
|
|
||||||
|
# запустіть додаток next (термінал 1)
|
||||||
|
npm run dev:web
|
||||||
|
|
||||||
|
# запустіть додаток електрон (термінал 2)
|
||||||
|
npm run dev:electron
|
||||||
|
```
|
||||||
|
|
||||||
|
### Усунення несправностей
|
||||||
|
|
||||||
|
Ви можете зтикнутись із помилкою `Unsupported platform` коли запускаєте `npm install`. Щоб усунути цю проблему, вам потрібно видалити `node_modules` та `package-lock.json`, і тоді запустити `npm install`. Це має встановити всі потрібні для запуску додатку пекеджі.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Видаліть node_modules в піддиректоріях
|
||||||
|
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||||
|
rm -rf "$dir"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Видаліть package-lock в піддиректоріях
|
||||||
|
find . -type f -name "package-lock.json" -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
### Тестування
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# bruno-schema
|
||||||
|
npm test --workspace=packages/bruno-schema
|
||||||
|
|
||||||
|
# bruno-lang
|
||||||
|
npm test --workspace=packages/bruno-lang
|
||||||
|
```
|
216
package-lock.json
generated
216
package-lock.json
generated
@ -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,17 +16630,16 @@
|
|||||||
},
|
},
|
||||||
"packages/bruno-electron": {
|
"packages/bruno-electron": {
|
||||||
"name": "bruno",
|
"name": "bruno",
|
||||||
"version": "v0.24.0",
|
"version": "v0.27.1",
|
||||||
"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",
|
||||||
"decomment": "^0.9.5",
|
"decomment": "^0.9.5",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
@ -16742,6 +16654,7 @@
|
|||||||
"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",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
@ -16776,6 +16689,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 +16810,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 +16857,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 +16866,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 +16907,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 +16944,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 +20508,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 +20590,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 +20686,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 +21238,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 +21499,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,14 +21511,13 @@
|
|||||||
"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",
|
||||||
"decomment": "^0.9.5",
|
"decomment": "^0.9.5",
|
||||||
"dmg-license": "^1.0.11",
|
"dmg-license": "^1.0.11",
|
||||||
@ -21633,6 +21536,7 @@
|
|||||||
"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",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
@ -21654,6 +21558,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 +21633,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 +22865,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 +23347,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 +25355,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 +25381,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 +27827,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"
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
"build:electron:snap": "./scripts/build-electron.sh snap",
|
"build:electron:snap": "./scripts/build-electron.sh snap",
|
||||||
"test:e2e": "npx playwright test",
|
"test:e2e": "npx playwright test",
|
||||||
"test:report": "npx playwright show-report",
|
"test:report": "npx playwright show-report",
|
||||||
|
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
|
||||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -18,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",
|
||||||
|
@ -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,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'github-markdown-css/github-markdown.css';
|
import 'github-markdown-css/github-markdown.css';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections';
|
import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections';
|
||||||
import { useTheme } from 'providers/Theme/index';
|
import { useTheme } from 'providers/Theme';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
@ -1,13 +1,55 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import * as Yup from 'yup';
|
import Tooltip from 'components/Tooltip';
|
||||||
|
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||||
|
const proxySchema = Yup.object({
|
||||||
|
enabled: Yup.string().oneOf(['global', 'true', 'false']),
|
||||||
|
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
|
||||||
|
hostname: Yup.string()
|
||||||
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
|
||||||
|
otherwise: (hostname) => hostname.nullable()
|
||||||
|
})
|
||||||
|
.max(1024),
|
||||||
|
port: Yup.number()
|
||||||
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
.min(1)
|
||||||
|
.max(65535),
|
||||||
|
auth: Yup.object()
|
||||||
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.object({
|
||||||
|
enabled: Yup.boolean(),
|
||||||
|
username: Yup.string()
|
||||||
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: (username) => username.required('Specify username for proxy authentication.')
|
||||||
|
})
|
||||||
|
.max(1024),
|
||||||
|
password: Yup.string()
|
||||||
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: (password) => password.required('Specify password for proxy authentication.')
|
||||||
|
})
|
||||||
|
.max(1024)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
bypassProxy: Yup.string().optional().max(1024)
|
||||||
|
});
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
enabled: proxyConfig.enabled || false,
|
enabled: proxyConfig.enabled || 'global',
|
||||||
protocol: proxyConfig.protocol || 'http',
|
protocol: proxyConfig.protocol || 'http',
|
||||||
hostname: proxyConfig.hostname || '',
|
hostname: proxyConfig.hostname || '',
|
||||||
port: proxyConfig.port || '',
|
port: proxyConfig.port || '',
|
||||||
@ -15,27 +57,33 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
|||||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||||
}
|
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
bypassProxy: proxyConfig.bypassProxy || ''
|
||||||
enabled: Yup.boolean(),
|
},
|
||||||
protocol: Yup.string().oneOf(['http', 'https', 'socks5']),
|
validationSchema: proxySchema,
|
||||||
hostname: Yup.string().max(1024),
|
|
||||||
port: Yup.number().min(0).max(65535),
|
|
||||||
auth: Yup.object({
|
|
||||||
enabled: Yup.boolean(),
|
|
||||||
username: Yup.string().max(1024),
|
|
||||||
password: Yup.string().max(1024)
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
onUpdate(values);
|
proxySchema
|
||||||
|
.validate(values, { abortEarly: true })
|
||||||
|
.then((validatedProxy) => {
|
||||||
|
// serialize 'enabled' to boolean
|
||||||
|
if (validatedProxy.enabled === 'true') {
|
||||||
|
validatedProxy.enabled = true;
|
||||||
|
} else if (validatedProxy.enabled === 'false') {
|
||||||
|
validatedProxy.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(validatedProxy);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
let errMsg = error.message || 'Preferences validation error';
|
||||||
|
toast.error(errMsg);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formik.setValues({
|
formik.setValues({
|
||||||
enabled: proxyConfig.enabled || false,
|
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 || '',
|
||||||
@ -43,18 +91,66 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
|||||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||||
}
|
},
|
||||||
|
bypassProxy: proxyConfig.bypassProxy || ''
|
||||||
});
|
});
|
||||||
}, [proxyConfig]);
|
}, [proxyConfig]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
|
<h1 className="font-medium mb-3">Proxy Settings</h1>
|
||||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||||
<div className="mb-3 flex items-center">
|
<div className="mb-3 flex items-center">
|
||||||
<label className="settings-label" htmlFor="enabled">
|
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||||
Enabled
|
Config
|
||||||
|
<Tooltip
|
||||||
|
text={`
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li><span style="width: 50px;display:inline-block;">global</span> - use global proxy config</li>
|
||||||
|
<li><span style="width: 50px;display:inline-block;">enabled</span> - use collection proxy config</li>
|
||||||
|
<li><span style="width: 50px;display:inline-block;">disable</span> - disable proxy</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
tooltipId="request-var"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
|
<div className="flex items-center">
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="enabled"
|
||||||
|
value="global"
|
||||||
|
checked={formik.values.enabled === 'global'}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
className="mr-1"
|
||||||
|
/>
|
||||||
|
global
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center ml-4">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="enabled"
|
||||||
|
value={'true'}
|
||||||
|
checked={formik.values.enabled === 'true'}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
className="mr-1"
|
||||||
|
/>
|
||||||
|
enabled
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center ml-4">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="enabled"
|
||||||
|
value={'false'}
|
||||||
|
checked={formik.values.enabled === 'false'}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
className="mr-1"
|
||||||
|
/>
|
||||||
|
disabled
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3 flex items-center">
|
<div className="mb-3 flex items-center">
|
||||||
<label className="settings-label" htmlFor="protocol">
|
<label className="settings-label" htmlFor="protocol">
|
||||||
@ -83,6 +179,17 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
|||||||
/>
|
/>
|
||||||
https
|
https
|
||||||
</label>
|
</label>
|
||||||
|
<label className="flex items-center ml-4">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="protocol"
|
||||||
|
value="socks5"
|
||||||
|
checked={formik.values.protocol === 'socks4'}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
className="mr-1"
|
||||||
|
/>
|
||||||
|
socks4
|
||||||
|
</label>
|
||||||
<label className="flex items-center ml-4">
|
<label className="flex items-center ml-4">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@ -113,7 +220,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
|||||||
value={formik.values.hostname || ''}
|
value={formik.values.hostname || ''}
|
||||||
/>
|
/>
|
||||||
{formik.touched.hostname && formik.errors.hostname ? (
|
{formik.touched.hostname && formik.errors.hostname ? (
|
||||||
<div className="text-red-500">{formik.errors.hostname}</div>
|
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3 flex items-center">
|
<div className="mb-3 flex items-center">
|
||||||
@ -132,7 +239,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
|||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
value={formik.values.port}
|
value={formik.values.port}
|
||||||
/>
|
/>
|
||||||
{formik.touched.port && formik.errors.port ? <div className="text-red-500">{formik.errors.port}</div> : null}
|
{formik.touched.port && formik.errors.port ? (
|
||||||
|
<div className="ml-3 text-red-500">{formik.errors.port}</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3 flex items-center">
|
<div className="mb-3 flex items-center">
|
||||||
<label className="settings-label" htmlFor="auth.enabled">
|
<label className="settings-label" htmlFor="auth.enabled">
|
||||||
@ -163,7 +272,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
|||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
{formik.touched.auth?.username && formik.errors.auth?.username ? (
|
{formik.touched.auth?.username && formik.errors.auth?.username ? (
|
||||||
<div className="text-red-500">{formik.errors.auth.username}</div>
|
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3 flex items-center">
|
<div className="mb-3 flex items-center">
|
||||||
@ -183,10 +292,30 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
|||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
{formik.touched.auth?.password && formik.errors.auth?.password ? (
|
{formik.touched.auth?.password && formik.errors.auth?.password ? (
|
||||||
<div className="text-red-500">{formik.errors.auth.password}</div>
|
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mb-3 flex items-center">
|
||||||
|
<label className="settings-label" htmlFor="bypassProxy">
|
||||||
|
Proxy Bypass
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="bypassProxy"
|
||||||
|
type="text"
|
||||||
|
name="bypassProxy"
|
||||||
|
className="block textbox"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.bypassProxy || ''}
|
||||||
|
/>
|
||||||
|
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
|
||||||
|
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||||
Save
|
Save
|
||||||
|
@ -36,7 +36,7 @@ const CollectionSettings = ({ collection }) => {
|
|||||||
brunoConfig.proxy = config;
|
brunoConfig.proxy = config;
|
||||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success('Collection settings updated successfully');
|
toast.success('Collection settings updated successfully.');
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||||
};
|
};
|
||||||
@ -60,7 +60,9 @@ const CollectionSettings = ({ collection }) => {
|
|||||||
|
|
||||||
const onClientCertSettingsRemove = (config) => {
|
const onClientCertSettingsRemove = (config) => {
|
||||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||||
brunoConfig.clientCertificates = brunoConfig.clientCertificates.filter((item) => item.domain != config.domain);
|
brunoConfig.clientCertificates.certs = brunoConfig.clientCertificates.certs.filter(
|
||||||
|
(item) => item.domain != config.domain
|
||||||
|
);
|
||||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success('Collection settings updated successfully');
|
toast.success('Collection settings updated successfully');
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -5,7 +5,7 @@ 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';
|
||||||
import { IconUpload } from '@tabler/icons';
|
import { IconDownload } from '@tabler/icons';
|
||||||
import ImportEnvironment from '../ImportEnvironment';
|
import ImportEnvironment from '../ImportEnvironment';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ const EnvironmentList = ({ collection }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-auto flex items-center btn-import-environment" onClick={() => setOpenImportModal(true)}>
|
<div className="mt-auto flex items-center btn-import-environment" onClick={() => setOpenImportModal(true)}>
|
||||||
<IconUpload size={12} strokeWidth={2} />
|
<IconDownload size={12} strokeWidth={2} />
|
||||||
<span className="label ml-1 text-xs">Import</span>
|
<span className="label ml-1 text-xs">Import</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,67 +1,103 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
const General = ({ close }) => {
|
const General = ({ close }) => {
|
||||||
const preferences = useSelector((state) => state.app.preferences);
|
const preferences = useSelector((state) => state.app.preferences);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const [sslVerification, setSslVerification] = useState(preferences.request.sslVerification);
|
const preferencesSchema = Yup.object().shape({
|
||||||
const [timeout, setTimeout] = useState(preferences.request.timeout);
|
sslVerification: Yup.boolean(),
|
||||||
|
timeout: Yup.mixed()
|
||||||
|
.transform((value, originalValue) => {
|
||||||
|
return originalValue === '' ? undefined : value;
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.test('isNumber', 'Request Timeout must be a number', (value) => {
|
||||||
|
return value === undefined || !isNaN(value);
|
||||||
|
})
|
||||||
|
.test('isValidTimeout', 'Request Timeout must be equal or greater than 0', (value) => {
|
||||||
|
return value === undefined || Number(value) >= 0;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
const handleSave = () => {
|
const formik = useFormik({
|
||||||
|
initialValues: {
|
||||||
|
sslVerification: preferences.request.sslVerification,
|
||||||
|
timeout: preferences.request.timeout
|
||||||
|
},
|
||||||
|
validationSchema: preferencesSchema,
|
||||||
|
onSubmit: async (values) => {
|
||||||
|
try {
|
||||||
|
const newPreferences = await preferencesSchema.validate(values, { abortEarly: true });
|
||||||
|
handleSave(newPreferences);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Preferences validation error:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = (newPreferences) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
savePreferences({
|
savePreferences({
|
||||||
...preferences,
|
...preferences,
|
||||||
request: {
|
request: {
|
||||||
sslVerification,
|
sslVerification: newPreferences.sslVerification,
|
||||||
timeout
|
timeout: newPreferences.timeout
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
).then(() => {
|
)
|
||||||
|
.then(() => {
|
||||||
close();
|
close();
|
||||||
});
|
})
|
||||||
};
|
.catch((err) => console.log(err) && toast.error('Failed to update preferences'));
|
||||||
|
|
||||||
const handleTimeoutChange = (value) => {
|
|
||||||
const validTimeout = isNaN(Number(value)) ? timeout : Number(value);
|
|
||||||
setTimeout(validTimeout);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
|
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||||
<div className="flex items-center mt-2">
|
<div className="flex items-center mt-2">
|
||||||
<label className="mr-2 select-none" style={{ minWidth: 200 }} htmlFor="ssl-cert-verification">
|
<label className="block font-medium mr-2 select-none" style={{ minWidth: 200 }} htmlFor="sslVerification">
|
||||||
SSL Certificate Verification
|
SSL/TLS Certificate Verification
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="ssl-cert-verification"
|
id="ssl-cert-verification"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={sslVerification}
|
name="sslVerification"
|
||||||
onChange={() => setSslVerification(!sslVerification)}
|
checked={formik.values.sslVerification}
|
||||||
|
onChange={formik.handleChange}
|
||||||
className="mousetrap mr-0"
|
className="mousetrap mr-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col mt-6">
|
<div className="flex flex-col mt-6">
|
||||||
<label className="block font-medium select-none">Request Timeout (in ms)</label>
|
<label className="block font-medium select-none" htmlFor="timeout">
|
||||||
|
Request Timeout (in ms)
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="block textbox mt-2 w-1/4"
|
name="timeout"
|
||||||
|
className="block textbox mt-2 w-16"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
onChange={(e) => handleTimeoutChange(e.target.value)}
|
onChange={formik.handleChange}
|
||||||
defaultValue={timeout === 0 ? '' : timeout}
|
value={formik.values.timeout}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{formik.touched.timeout && formik.errors.timeout ? (
|
||||||
|
<div className="text-red-500">{formik.errors.timeout}</div>
|
||||||
|
) : null}
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const StyledWrapper = styled.div`
|
||||||
|
.settings-label {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textbox {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 0.15rem 0.45rem;
|
||||||
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color ease-in-out 0.1s;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: ${(props) => props.theme.modal.input.bg};
|
||||||
|
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default StyledWrapper;
|
@ -0,0 +1,292 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useFormik } from 'formik';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||||
|
|
||||||
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
const ProxySettings = ({ close }) => {
|
||||||
|
const preferences = useSelector((state) => state.app.preferences);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const proxySchema = Yup.object({
|
||||||
|
enabled: Yup.boolean(),
|
||||||
|
protocol: Yup.string().required().oneOf(['http', 'https', 'socks4', 'socks5']),
|
||||||
|
hostname: Yup.string()
|
||||||
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
|
||||||
|
otherwise: (hostname) => hostname.nullable()
|
||||||
|
})
|
||||||
|
.max(1024),
|
||||||
|
port: Yup.number()
|
||||||
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
.min(1)
|
||||||
|
.max(65535),
|
||||||
|
auth: Yup.object()
|
||||||
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.object({
|
||||||
|
enabled: Yup.boolean(),
|
||||||
|
username: Yup.string()
|
||||||
|
.when(['enabled'], {
|
||||||
|
is: true,
|
||||||
|
then: (username) => username.required('Specify username for proxy authentication.')
|
||||||
|
})
|
||||||
|
.max(1024),
|
||||||
|
password: Yup.string()
|
||||||
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: (password) => password.required('Specify password for proxy authentication.')
|
||||||
|
})
|
||||||
|
.max(1024)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
bypassProxy: Yup.string().optional().max(1024)
|
||||||
|
});
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues: {
|
||||||
|
enabled: preferences.proxy.enabled || false,
|
||||||
|
protocol: preferences.proxy.protocol || 'http',
|
||||||
|
hostname: preferences.proxy.hostname || '',
|
||||||
|
port: preferences.proxy.port || 0,
|
||||||
|
auth: {
|
||||||
|
enabled: preferences.proxy.auth ? preferences.proxy.auth.enabled || false : false,
|
||||||
|
username: preferences.proxy.auth ? preferences.proxy.auth.username || '' : '',
|
||||||
|
password: preferences.proxy.auth ? preferences.proxy.auth.password || '' : ''
|
||||||
|
},
|
||||||
|
bypassProxy: preferences.proxy.bypassProxy || ''
|
||||||
|
},
|
||||||
|
validationSchema: proxySchema,
|
||||||
|
onSubmit: (values) => {
|
||||||
|
onUpdate(values);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onUpdate = (values) => {
|
||||||
|
proxySchema
|
||||||
|
.validate(values, { abortEarly: true })
|
||||||
|
.then((validatedProxy) => {
|
||||||
|
dispatch(
|
||||||
|
savePreferences({
|
||||||
|
...preferences,
|
||||||
|
proxy: validatedProxy
|
||||||
|
})
|
||||||
|
).then(() => {
|
||||||
|
close();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
let errMsg = error.message || 'Preferences validation error';
|
||||||
|
toast.error(errMsg);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
formik.setValues({
|
||||||
|
enabled: preferences.proxy.enabled || false,
|
||||||
|
protocol: preferences.proxy.protocol || 'http',
|
||||||
|
hostname: preferences.proxy.hostname || '',
|
||||||
|
port: preferences.proxy.port || '',
|
||||||
|
auth: {
|
||||||
|
enabled: preferences.proxy.auth ? preferences.proxy.auth.enabled || false : false,
|
||||||
|
username: preferences.proxy.auth ? preferences.proxy.auth.username || '' : '',
|
||||||
|
password: preferences.proxy.auth ? preferences.proxy.auth.password || '' : ''
|
||||||
|
},
|
||||||
|
bypassProxy: preferences.proxy.bypassProxy || ''
|
||||||
|
});
|
||||||
|
}, [preferences]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledWrapper>
|
||||||
|
<h1 className="font-medium mb-3">Global Proxy Settings</h1>
|
||||||
|
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||||
|
<div className="mb-3 flex items-center">
|
||||||
|
<label className="settings-label" htmlFor="enabled">
|
||||||
|
Enabled
|
||||||
|
</label>
|
||||||
|
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 flex items-center">
|
||||||
|
<label className="settings-label" htmlFor="protocol">
|
||||||
|
Protocol
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<label className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="protocol"
|
||||||
|
value="http"
|
||||||
|
checked={formik.values.protocol === 'http'}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
className="mr-1"
|
||||||
|
/>
|
||||||
|
http
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center ml-4">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="protocol"
|
||||||
|
value="https"
|
||||||
|
checked={formik.values.protocol === 'https'}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
className="mr-1"
|
||||||
|
/>
|
||||||
|
https
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center ml-4">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="protocol"
|
||||||
|
value="socks5"
|
||||||
|
checked={formik.values.protocol === 'socks4'}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
className="mr-1"
|
||||||
|
/>
|
||||||
|
socks4
|
||||||
|
</label>
|
||||||
|
<label className="flex items-center ml-4">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="protocol"
|
||||||
|
value="socks5"
|
||||||
|
checked={formik.values.protocol === 'socks5'}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
className="mr-1"
|
||||||
|
/>
|
||||||
|
socks5
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 flex items-center">
|
||||||
|
<label className="settings-label" htmlFor="hostname">
|
||||||
|
Hostname
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="hostname"
|
||||||
|
type="text"
|
||||||
|
name="hostname"
|
||||||
|
className="block textbox"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.hostname || ''}
|
||||||
|
/>
|
||||||
|
{formik.touched.hostname && formik.errors.hostname ? (
|
||||||
|
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 flex items-center">
|
||||||
|
<label className="settings-label" htmlFor="port">
|
||||||
|
Port
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="port"
|
||||||
|
type="number"
|
||||||
|
name="port"
|
||||||
|
className="block textbox"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.port}
|
||||||
|
/>
|
||||||
|
{formik.touched.port && formik.errors.port ? (
|
||||||
|
<div className="ml-3 text-red-500">{formik.errors.port}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 flex items-center">
|
||||||
|
<label className="settings-label" htmlFor="auth.enabled">
|
||||||
|
Auth
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="auth.enabled"
|
||||||
|
checked={formik.values.auth.enabled}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mb-3 flex items-center">
|
||||||
|
<label className="settings-label" htmlFor="auth.username">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="auth.username"
|
||||||
|
type="text"
|
||||||
|
name="auth.username"
|
||||||
|
className="block textbox"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
value={formik.values.auth.username}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
/>
|
||||||
|
{formik.touched.auth?.username && formik.errors.auth?.username ? (
|
||||||
|
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 flex items-center">
|
||||||
|
<label className="settings-label" htmlFor="auth.password">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="auth.password"
|
||||||
|
type="text"
|
||||||
|
name="auth.password"
|
||||||
|
className="block textbox"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
value={formik.values.auth.password}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
/>
|
||||||
|
{formik.touched.auth?.password && formik.errors.auth?.password ? (
|
||||||
|
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 flex items-center">
|
||||||
|
<label className="settings-label" htmlFor="bypassProxy">
|
||||||
|
Proxy Bypass
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="bypassProxy"
|
||||||
|
type="text"
|
||||||
|
name="bypassProxy"
|
||||||
|
className="block textbox"
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.bypassProxy || ''}
|
||||||
|
/>
|
||||||
|
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
|
||||||
|
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="mt-6">
|
||||||
|
<button type="submit" className="submit btn btn-md btn-secondary">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProxySettings;
|
@ -5,6 +5,7 @@ import Support from './Support';
|
|||||||
import General from './General';
|
import General from './General';
|
||||||
import Font from './Font';
|
import Font from './Font';
|
||||||
import Theme from './Theme';
|
import Theme from './Theme';
|
||||||
|
import Proxy from './ProxySettings';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
|
||||||
const Preferences = ({ onClose }) => {
|
const Preferences = ({ onClose }) => {
|
||||||
@ -22,6 +23,10 @@ const Preferences = ({ onClose }) => {
|
|||||||
return <General close={onClose} />;
|
return <General close={onClose} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'proxy': {
|
||||||
|
return <Proxy close={onClose} />;
|
||||||
|
}
|
||||||
|
|
||||||
case 'theme': {
|
case 'theme': {
|
||||||
return <Theme close={onClose} />;
|
return <Theme close={onClose} />;
|
||||||
}
|
}
|
||||||
@ -49,6 +54,9 @@ const Preferences = ({ onClose }) => {
|
|||||||
<div className={getTabClassname('font')} role="tab" onClick={() => setTab('font')}>
|
<div className={getTabClassname('font')} role="tab" onClick={() => setTab('font')}>
|
||||||
Font
|
Font
|
||||||
</div>
|
</div>
|
||||||
|
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||||
|
Proxy
|
||||||
|
</div>
|
||||||
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
|
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
|
||||||
Support
|
Support
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,6 +107,7 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
|||||||
'value'
|
'value'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
allowNewlines={true}
|
||||||
onRun={handleRun}
|
onRun={handleRun}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
/>
|
/>
|
||||||
|
@ -36,12 +36,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
|||||||
|
|
||||||
const request = item.draft ? item.draft.request : item.request;
|
const request = item.draft ? item.draft.request : item.request;
|
||||||
|
|
||||||
let {
|
let { schema, loadSchema, isLoading: isSchemaLoading } = useGraphqlSchema(url, environment, request, collection);
|
||||||
schema,
|
|
||||||
loadSchema,
|
|
||||||
isLoading: isSchemaLoading,
|
|
||||||
error: schemaError
|
|
||||||
} = useGraphqlSchema(url, environment, request, collection);
|
|
||||||
|
|
||||||
const loadGqlSchema = () => {
|
const loadGqlSchema = () => {
|
||||||
if (!isSchemaLoading) {
|
if (!isSchemaLoading) {
|
||||||
|
@ -26,7 +26,12 @@ const useGraphqlSchema = (endpoint, environment, request, collection) => {
|
|||||||
const loadSchema = () => {
|
const loadSchema = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
fetchGqlSchema(endpoint, environment, request, collection)
|
fetchGqlSchema(endpoint, environment, request, collection)
|
||||||
.then((res) => res.data)
|
.then((res) => {
|
||||||
|
if (!res || res.status !== 200) {
|
||||||
|
return Promise.reject(new Error(res.statusText));
|
||||||
|
}
|
||||||
|
return res.data;
|
||||||
|
})
|
||||||
.then((s) => {
|
.then((s) => {
|
||||||
if (s && s.data) {
|
if (s && s.data) {
|
||||||
setSchema(buildClientSchema(s.data));
|
setSchema(buildClientSchema(s.data));
|
||||||
@ -40,7 +45,7 @@ const useGraphqlSchema = (endpoint, environment, request, collection) => {
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError(err);
|
setError(err);
|
||||||
toast.error('Error occurred while loading GraphQL Schema');
|
toast.error(`Error occurred while loading GraphQL Schema: ${err.message}`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ const Wrapper = styled.div`
|
|||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
|
|
||||||
.body-mode-selector {
|
.body-mode-selector {
|
||||||
background: ${(props) => props.theme.requestTabPanel.bodyModeSelect.color};
|
background: transparent;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
@ -15,6 +15,10 @@ const Wrapper = styled.div`
|
|||||||
.label-item {
|
.label-item {
|
||||||
padding: 0.2rem 0.6rem !important;
|
padding: 0.2rem 0.6rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-body-mode {
|
||||||
|
color: ${(props) => props.theme.colors.text.yellow};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.caret {
|
.caret {
|
||||||
|
@ -6,16 +6,19 @@ import { useDispatch } from 'react-redux';
|
|||||||
import { updateRequestBodyMode } from 'providers/ReduxStore/slices/collections';
|
import { updateRequestBodyMode } from 'providers/ReduxStore/slices/collections';
|
||||||
import { humanizeRequestBodyMode } from 'utils/collections';
|
import { humanizeRequestBodyMode } from 'utils/collections';
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
|
import { updateRequestBody } from 'providers/ReduxStore/slices/collections/index';
|
||||||
|
import { toastError } from 'utils/common/error';
|
||||||
|
|
||||||
const RequestBodyMode = ({ item, collection }) => {
|
const RequestBodyMode = ({ item, collection }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const dropdownTippyRef = useRef();
|
const dropdownTippyRef = useRef();
|
||||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||||
const bodyMode = item.draft ? get(item, 'draft.request.body.mode') : get(item, 'request.body.mode');
|
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||||
|
const bodyMode = body?.mode;
|
||||||
|
|
||||||
const Icon = forwardRef((props, ref) => {
|
const Icon = forwardRef((props, ref) => {
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none">
|
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none selected-body-mode">
|
||||||
{humanizeRequestBodyMode(bodyMode)} <IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
|
{humanizeRequestBodyMode(bodyMode)} <IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -31,6 +34,24 @@ const RequestBodyMode = ({ item, collection }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onPrettify = () => {
|
||||||
|
if (body?.json && bodyMode === 'json') {
|
||||||
|
try {
|
||||||
|
const bodyJson = JSON.parse(body.json);
|
||||||
|
const prettyBodyJson = JSON.stringify(bodyJson, null, 2);
|
||||||
|
dispatch(
|
||||||
|
updateRequestBody({
|
||||||
|
content: prettyBodyJson,
|
||||||
|
itemUid: item.uid,
|
||||||
|
collectionUid: collection.uid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
toastError(new Error('Unable to prettify. Invalid JSON format.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<div className="inline-flex items-center cursor-pointer body-mode-selector">
|
<div className="inline-flex items-center cursor-pointer body-mode-selector">
|
||||||
@ -103,6 +124,11 @@ const RequestBodyMode = ({ item, collection }) => {
|
|||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
{bodyMode === 'json' && (
|
||||||
|
<button className="ml-1" onClick={onPrettify}>
|
||||||
|
Prettify
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
import CodeEditor from 'components/CodeEditor/index';
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
|
|
||||||
|
const QueryResultPreview = ({
|
||||||
|
previewTab,
|
||||||
|
allowedPreviewModes,
|
||||||
|
data,
|
||||||
|
dataBuffer,
|
||||||
|
formattedData,
|
||||||
|
item,
|
||||||
|
contentType,
|
||||||
|
collection,
|
||||||
|
mode,
|
||||||
|
disableRunEventListener,
|
||||||
|
storedTheme
|
||||||
|
}) => {
|
||||||
|
const preferences = useSelector((state) => state.app.preferences);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
// Fail safe, so we don't render anything with an invalid tab
|
||||||
|
if (!allowedPreviewModes.includes(previewTab)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRun = () => {
|
||||||
|
if (disableRunEventListener) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(sendRequest(item, collection.uid));
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (previewTab) {
|
||||||
|
case 'preview-web': {
|
||||||
|
const webViewSrc = data.replace('<head>', `<head><base href="${item.requestSent?.url || ''}">`);
|
||||||
|
return (
|
||||||
|
<webview
|
||||||
|
src={`data:text/html; charset=utf-8,${encodeURIComponent(webViewSrc)}`}
|
||||||
|
webpreferences="disableDialogs=true, javascript=yes"
|
||||||
|
className="h-full bg-white"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case 'preview-image': {
|
||||||
|
return <img src={`data:${contentType.replace(/\;(.*)/, '')};base64,${dataBuffer}`} className="mx-auto" />;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
case 'raw': {
|
||||||
|
return (
|
||||||
|
<CodeEditor
|
||||||
|
collection={collection}
|
||||||
|
font={get(preferences, 'font.codeFont', 'default')}
|
||||||
|
theme={storedTheme}
|
||||||
|
onRun={onRun}
|
||||||
|
value={formattedData}
|
||||||
|
mode={mode}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueryResultPreview;
|
@ -1,24 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import get from 'lodash/get';
|
|
||||||
import CodeEditor from 'components/CodeEditor';
|
|
||||||
import { useTheme } from 'providers/Theme';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
|
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
|
||||||
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
|
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
|
||||||
|
import QueryResultPreview from './QueryResultPreview';
|
||||||
|
|
||||||
import StyledWrapper from './StyledWrapper';
|
import StyledWrapper from './StyledWrapper';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
const QueryResult = ({ item, collection, data, width, disableRunEventListener, headers, error }) => {
|
import { useTheme } from 'providers/Theme/index';
|
||||||
const { storedTheme } = useTheme();
|
|
||||||
const preferences = useSelector((state) => state.app.preferences);
|
|
||||||
const [tab, setTab] = useState('preview');
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const contentType = getContentType(headers);
|
|
||||||
const mode = getCodeMirrorModeBasedOnContentType(contentType);
|
|
||||||
|
|
||||||
const formatResponse = (data, mode) => {
|
const formatResponse = (data, mode) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@ -31,7 +21,6 @@ const QueryResult = ({ item, collection, data, width, disableRunEventListener, h
|
|||||||
|
|
||||||
if (mode.includes('xml')) {
|
if (mode.includes('xml')) {
|
||||||
let parsed = safeParseXML(data, { collapseContent: true });
|
let parsed = safeParseXML(data, { collapseContent: true });
|
||||||
|
|
||||||
if (typeof parsed === 'string') {
|
if (typeof parsed === 'string') {
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
@ -39,99 +28,79 @@ const QueryResult = ({ item, collection, data, width, disableRunEventListener, h
|
|||||||
return safeStringifyJSON(parsed, true);
|
return safeStringifyJSON(parsed, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['text', 'html'].includes(mode)) {
|
if (['text', 'html'].includes(mode) || typeof data === 'string') {
|
||||||
if (typeof data === 'string') {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return safeStringifyJSON(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode.includes('image')) {
|
|
||||||
return item.requestSent.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
// final fallback
|
|
||||||
if (typeof data === 'string') {
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return safeStringifyJSON(data);
|
return safeStringifyJSON(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = formatResponse(data, mode);
|
const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEventListener, headers, error }) => {
|
||||||
|
const contentType = getContentType(headers);
|
||||||
|
const mode = getCodeMirrorModeBasedOnContentType(contentType);
|
||||||
|
const formattedData = formatResponse(data, mode);
|
||||||
|
const { storedTheme } = useTheme();
|
||||||
|
|
||||||
const onRun = () => {
|
const allowedPreviewModes = useMemo(() => {
|
||||||
if (disableRunEventListener) {
|
// Always show raw
|
||||||
return;
|
const allowedPreviewModes = ['raw'];
|
||||||
|
|
||||||
|
if (mode.includes('html') && typeof data === 'string') {
|
||||||
|
allowedPreviewModes.unshift('preview-web');
|
||||||
|
} else if (mode.includes('image')) {
|
||||||
|
allowedPreviewModes.unshift('preview-image');
|
||||||
}
|
}
|
||||||
dispatch(sendRequest(item, collection.uid));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTabClassname = (tabName) => {
|
return allowedPreviewModes;
|
||||||
return classnames(`select-none ${tabName}`, {
|
}, [mode, data, formattedData]);
|
||||||
active: tabName === tab,
|
|
||||||
'cursor-pointer': tabName !== tab
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTabs = () => {
|
const [previewTab, setPreviewTab] = useState(allowedPreviewModes[0]);
|
||||||
if (!mode.includes('html')) {
|
// Ensure the active Tab is always allowed
|
||||||
|
useEffect(() => {
|
||||||
|
if (!allowedPreviewModes.includes(previewTab)) {
|
||||||
|
setPreviewTab(allowedPreviewModes[0]);
|
||||||
|
}
|
||||||
|
}, [previewTab, allowedPreviewModes]);
|
||||||
|
|
||||||
|
const tabs = useMemo(() => {
|
||||||
|
if (allowedPreviewModes.length === 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return allowedPreviewModes.map((previewMode) => (
|
||||||
<>
|
<div
|
||||||
<div className={getTabClassname('raw')} role="tab" onClick={() => setTab('raw')}>
|
className={classnames('select-none capitalize', previewMode === previewTab ? 'active' : 'cursor-pointer')}
|
||||||
Raw
|
role="tab"
|
||||||
|
onClick={() => setPreviewTab(previewMode)}
|
||||||
|
key={previewMode}
|
||||||
|
>
|
||||||
|
{previewMode.replace(/-(.*)/, ' ')}
|
||||||
</div>
|
</div>
|
||||||
<div className={getTabClassname('preview')} role="tab" onClick={() => setTab('preview')}>
|
));
|
||||||
Preview
|
}, [allowedPreviewModes, previewTab]);
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeResult = useMemo(() => {
|
|
||||||
if (
|
|
||||||
tab === 'preview' &&
|
|
||||||
mode.includes('html') &&
|
|
||||||
item.requestSent &&
|
|
||||||
item.requestSent.url &&
|
|
||||||
typeof data === 'string'
|
|
||||||
) {
|
|
||||||
// Add the Base tag to the head so content loads properly. This also needs the correct CSP settings
|
|
||||||
const webViewSrc = data.replace('<head>', `<head><base href="${item.requestSent.url}">`);
|
|
||||||
return (
|
|
||||||
<webview
|
|
||||||
src={`data:text/html; charset=utf-8,${encodeURIComponent(webViewSrc)}`}
|
|
||||||
webpreferences="disableDialogs=true, javascript=yes"
|
|
||||||
className="h-full bg-white"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (mode.includes('image')) {
|
|
||||||
return <img src={item.requestSent.url} alt="image" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CodeEditor
|
|
||||||
collection={collection}
|
|
||||||
font={get(preferences, 'font.codeFont', 'default')}
|
|
||||||
theme={storedTheme}
|
|
||||||
onRun={onRun}
|
|
||||||
value={value}
|
|
||||||
mode={mode}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}, [tab, collection, storedTheme, onRun, value, mode]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledWrapper className="w-full h-full" style={{ maxWidth: width }}>
|
<StyledWrapper className="w-full h-full" style={{ maxWidth: width }}>
|
||||||
<div className="flex justify-end gap-2 text-xs" role="tablist">
|
<div className="flex justify-end gap-2 text-xs" role="tablist">
|
||||||
{getTabs()}
|
{tabs}
|
||||||
</div>
|
</div>
|
||||||
{error ? <span className="text-red-500">{error}</span> : activeResult}
|
{error ? (
|
||||||
|
<span className="text-red-500">{error}</span>
|
||||||
|
) : (
|
||||||
|
<QueryResultPreview
|
||||||
|
previewTab={previewTab}
|
||||||
|
data={data}
|
||||||
|
dataBuffer={dataBuffer}
|
||||||
|
formattedData={formattedData}
|
||||||
|
item={item}
|
||||||
|
contentType={contentType}
|
||||||
|
mode={mode}
|
||||||
|
collection={collection}
|
||||||
|
allowedPreviewModes={allowedPreviewModes}
|
||||||
|
disableRunEventListener={disableRunEventListener}
|
||||||
|
storedTheme={storedTheme}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -13,6 +13,10 @@ const ResponseSize = ({ size }) => {
|
|||||||
sizeToDisplay = size + 'B';
|
sizeToDisplay = size + 'B';
|
||||||
}
|
}
|
||||||
|
|
||||||
return <StyledWrapper className="ml-4">{sizeToDisplay}</StyledWrapper>;
|
return (
|
||||||
|
<StyledWrapper title={size.toLocaleString() + 'B'} className="ml-4">
|
||||||
|
{sizeToDisplay}
|
||||||
|
</StyledWrapper>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
export default ResponseSize;
|
export default ResponseSize;
|
||||||
|
@ -41,8 +41,10 @@ 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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,9 @@ 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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -87,9 +87,17 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
'item-focused-in-tab': item.uid == activeTabUid
|
'item-focused-in-tab': item.uid == activeTabUid
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const scrollToTheActiveTab = () => {
|
||||||
|
const activeTab = document.querySelector('.request-tab.active');
|
||||||
|
if (activeTab) {
|
||||||
|
activeTab.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleClick = (event) => {
|
const handleClick = (event) => {
|
||||||
switch (event.button) {
|
//scroll to the active tab
|
||||||
case 0: // left click
|
setTimeout(scrollToTheActiveTab, 50);
|
||||||
|
|
||||||
if (isItemARequest(item)) {
|
if (isItemARequest(item)) {
|
||||||
dispatch(hideHomePage());
|
dispatch(hideHomePage());
|
||||||
if (itemIsOpenedInTabs(item, tabs)) {
|
if (itemIsOpenedInTabs(item, tabs)) {
|
||||||
@ -115,8 +123,9 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
collectionUid: collection.uid
|
collectionUid: collection.uid
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return;
|
};
|
||||||
case 2: // right click
|
|
||||||
|
const handleRightClick = (event) => {
|
||||||
const _menuDropdown = dropdownTippyRef.current;
|
const _menuDropdown = dropdownTippyRef.current;
|
||||||
if (_menuDropdown) {
|
if (_menuDropdown) {
|
||||||
let menuDropdownBehavior = 'show';
|
let menuDropdownBehavior = 'show';
|
||||||
@ -125,8 +134,6 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
}
|
}
|
||||||
_menuDropdown[menuDropdownBehavior]();
|
_menuDropdown[menuDropdownBehavior]();
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDoubleClick = (event) => {
|
const handleDoubleClick = (event) => {
|
||||||
@ -203,7 +210,8 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
? indents.map((i) => {
|
? indents.map((i) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onMouseUp={handleClick}
|
onClick={handleClick}
|
||||||
|
onContextMenu={handleRightClick}
|
||||||
onDoubleClick={handleDoubleClick}
|
onDoubleClick={handleDoubleClick}
|
||||||
className="indent-block"
|
className="indent-block"
|
||||||
key={i}
|
key={i}
|
||||||
@ -219,7 +227,8 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
|||||||
})
|
})
|
||||||
: null}
|
: null}
|
||||||
<div
|
<div
|
||||||
onMouseUp={handleClick}
|
onClick={handleClick}
|
||||||
|
onContextMenu={handleRightClick}
|
||||||
onDoubleClick={handleDoubleClick}
|
onDoubleClick={handleDoubleClick}
|
||||||
className="flex flex-grow items-center h-full overflow-hidden"
|
className="flex flex-grow items-center h-full overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
|
@ -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 remove this collection?
|
Are you sure you want to remove collection <span className="font-semibold">{collection.name}</span> ?
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -66,12 +66,11 @@ const Collection = ({ collection, searchText }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleClick = (event) => {
|
const handleClick = (event) => {
|
||||||
const _menuDropdown = menuDropdownTippyRef.current;
|
|
||||||
switch (event.button) {
|
|
||||||
case 0: // left click
|
|
||||||
dispatch(collectionClicked(collection.uid));
|
dispatch(collectionClicked(collection.uid));
|
||||||
return;
|
};
|
||||||
case 2: // right click
|
|
||||||
|
const handleRightClick = (event) => {
|
||||||
|
const _menuDropdown = menuDropdownTippyRef.current;
|
||||||
if (_menuDropdown) {
|
if (_menuDropdown) {
|
||||||
let menuDropdownBehavior = 'show';
|
let menuDropdownBehavior = 'show';
|
||||||
if (_menuDropdown.state.isShown) {
|
if (_menuDropdown.state.isShown) {
|
||||||
@ -79,8 +78,6 @@ const Collection = ({ collection, searchText }) => {
|
|||||||
}
|
}
|
||||||
_menuDropdown[menuDropdownBehavior]();
|
_menuDropdown[menuDropdownBehavior]();
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExportClick = () => {
|
const handleExportClick = () => {
|
||||||
@ -138,7 +135,11 @@ const Collection = ({ collection, searchText }) => {
|
|||||||
<CollectionProperties collection={collection} onClose={() => setCollectionPropertiesModal(false)} />
|
<CollectionProperties collection={collection} onClose={() => setCollectionPropertiesModal(false)} />
|
||||||
)}
|
)}
|
||||||
<div className="flex py-1 collection-name items-center" ref={drop}>
|
<div className="flex py-1 collection-name items-center" ref={drop}>
|
||||||
<div className="flex flex-grow items-center overflow-hidden" onMouseUp={handleClick}>
|
<div
|
||||||
|
className="flex flex-grow items-center overflow-hidden"
|
||||||
|
onClick={handleClick}
|
||||||
|
onContextMenu={handleRightClick}
|
||||||
|
>
|
||||||
<IconChevronRight
|
<IconChevronRight
|
||||||
size={16}
|
size={16}
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
|
@ -76,7 +76,12 @@ const CreateCollection = ({ onClose }) => {
|
|||||||
name="collectionName"
|
name="collectionName"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="block textbox mt-2 w-full"
|
className="block textbox mt-2 w-full"
|
||||||
onChange={formik.handleChange}
|
onChange={(e) => {
|
||||||
|
formik.handleChange(e);
|
||||||
|
if (formik.values.collectionName === formik.values.collectionFolderName) {
|
||||||
|
formik.setFieldValue('collectionFolderName', e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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.25.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>
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -18,7 +18,7 @@ const Tooltip = ({ text, tooltipId }) => {
|
|||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||||
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
|
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
|
||||||
</svg>
|
</svg>
|
||||||
<ReactTooltip anchorId={tooltipId} content={text} />
|
<ReactTooltip anchorId={tooltipId} html={text} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ import { useState } from 'react';
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
|
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||||
import { IconBrandGithub, IconPlus, IconUpload, IconFolders, IconSpeakerphone, IconBook } from '@tabler/icons';
|
import { IconBrandGithub, IconPlus, IconDownload, IconFolders, IconSpeakerphone, IconBook } from '@tabler/icons';
|
||||||
|
|
||||||
import Bruno from 'components/Bruno';
|
import Bruno from 'components/Bruno';
|
||||||
import CreateCollection from 'components/Sidebar/CreateCollection';
|
import CreateCollection from 'components/Sidebar/CreateCollection';
|
||||||
@ -69,7 +69,7 @@ const Welcome = () => {
|
|||||||
<span className="label ml-2">Open Collection</span>
|
<span className="label ml-2">Open Collection</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center ml-6" onClick={() => setImportCollectionModalOpen(true)}>
|
<div className="flex items-center ml-6" onClick={() => setImportCollectionModalOpen(true)}>
|
||||||
<IconUpload size={18} strokeWidth={2} />
|
<IconDownload size={18} strokeWidth={2} />
|
||||||
<span className="label ml-2" id="import-collection">
|
<span className="label ml-2" id="import-collection">
|
||||||
Import Collection
|
Import Collection
|
||||||
</span>
|
</span>
|
||||||
|
@ -9,7 +9,8 @@ const initialState = {
|
|||||||
showHomePage: false,
|
showHomePage: false,
|
||||||
preferences: {
|
preferences: {
|
||||||
request: {
|
request: {
|
||||||
sslVerification: true
|
sslVerification: true,
|
||||||
|
timeout: 0
|
||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
codeFont: 'default'
|
codeFont: 'default'
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
} from 'utils/collections';
|
} from 'utils/collections';
|
||||||
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
|
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
|
||||||
import { waitForNextTick } from 'utils/common';
|
import { waitForNextTick } from 'utils/common';
|
||||||
import { getDirectoryName, isWindowsOS } from 'utils/common/platform';
|
import { getDirectoryName, isWindowsOS, PATH_SEPARATOR } from 'utils/common/platform';
|
||||||
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
|
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -31,7 +31,6 @@ import {
|
|||||||
requestCancelled,
|
requestCancelled,
|
||||||
responseReceived,
|
responseReceived,
|
||||||
newItem as _newItem,
|
newItem as _newItem,
|
||||||
renameItem as _renameItem,
|
|
||||||
cloneItem as _cloneItem,
|
cloneItem as _cloneItem,
|
||||||
deleteItem as _deleteItem,
|
deleteItem as _deleteItem,
|
||||||
saveRequest as _saveRequest,
|
saveRequest as _saveRequest,
|
||||||
@ -48,8 +47,6 @@ import { resolveRequestFilename } from 'utils/common/platform';
|
|||||||
import { parseQueryParams, splitOnFirst } from 'utils/url/index';
|
import { parseQueryParams, splitOnFirst } from 'utils/url/index';
|
||||||
import { each } from 'lodash';
|
import { each } from 'lodash';
|
||||||
|
|
||||||
const PATH_SEPARATOR = path.sep;
|
|
||||||
|
|
||||||
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
|
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||||
@ -186,11 +183,6 @@ export const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) =>
|
|||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
// todo: this can be directly put inside the collections/index.js file
|
|
||||||
// the coding convention is to put only actions that need ipc in this file
|
|
||||||
export const sortCollections = (order) => (dispatch) => {
|
|
||||||
dispatch(_sortCollections(order));
|
|
||||||
};
|
|
||||||
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
|
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||||
@ -308,19 +300,7 @@ export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getSta
|
|||||||
}
|
}
|
||||||
const { ipcRenderer } = window;
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
ipcRenderer
|
ipcRenderer.invoke('renderer:rename-item', item.pathname, newPathname, newName).then(resolve).catch(reject);
|
||||||
.invoke('renderer:rename-item', item.pathname, newPathname, newName)
|
|
||||||
.then(() => {
|
|
||||||
// In case of Mac and Linux, we get the unlinkDir and addDir IPC events from electron which takes care of updating the state
|
|
||||||
// But in windows we don't get those events, so we need to update the state manually
|
|
||||||
// This looks like an issue in our watcher library chokidar
|
|
||||||
// GH: https://github.com/usebruno/bruno/issues/251
|
|
||||||
if (isWindowsOS()) {
|
|
||||||
dispatch(_renameItem({ newName, itemUid, collectionUid }));
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -405,13 +385,6 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
|
|||||||
ipcRenderer
|
ipcRenderer
|
||||||
.invoke('renderer:delete-item', item.pathname, item.type)
|
.invoke('renderer:delete-item', item.pathname, item.type)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// In case of Mac and Linux, we get the unlinkDir IPC event from electron which takes care of updating the state
|
|
||||||
// But in windows we don't get those events, so we need to update the state manually
|
|
||||||
// This looks like an issue in our watcher library chokidar
|
|
||||||
// GH: https://github.com/usebruno/bruno/issues/265
|
|
||||||
if (isWindowsOS()) {
|
|
||||||
dispatch(_deleteItem({ itemUid, collectionUid }));
|
|
||||||
}
|
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((error) => reject(error));
|
.catch((error) => reject(error));
|
||||||
@ -420,6 +393,9 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sortCollections = (payload) => (dispatch) => {
|
||||||
|
dispatch(_sortCollections(payload));
|
||||||
|
};
|
||||||
export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => {
|
export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import path from 'path';
|
|
||||||
import { uuid } from 'utils/common';
|
import { uuid } from 'utils/common';
|
||||||
import find from 'lodash/find';
|
import find from 'lodash/find';
|
||||||
import map from 'lodash/map';
|
import map from 'lodash/map';
|
||||||
@ -25,9 +24,7 @@ import {
|
|||||||
areItemsTheSameExceptSeqUpdate
|
areItemsTheSameExceptSeqUpdate
|
||||||
} from 'utils/collections';
|
} from 'utils/collections';
|
||||||
import { parseQueryParams, stringifyQueryParams } from 'utils/url';
|
import { parseQueryParams, stringifyQueryParams } from 'utils/url';
|
||||||
import { getSubdirectoriesFromRoot, getDirectoryName } from 'utils/common/platform';
|
import { getSubdirectoriesFromRoot, getDirectoryName, PATH_SEPARATOR } from 'utils/common/platform';
|
||||||
|
|
||||||
const PATH_SEPARATOR = path.sep;
|
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
collections: [],
|
collections: [],
|
||||||
|
@ -48,3 +48,5 @@ export const isMacOS = () => {
|
|||||||
|
|
||||||
return osFamily.includes('os x');
|
return osFamily.includes('os x');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PATH_SEPARATOR = isWindowsOS() ? '\\' : '/';
|
||||||
|
@ -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));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
386
packages/bruno-app/src/utils/importers/openapi-collection.js
Normal file
386
packages/bruno-app/src/utils/importers/openapi-collection.js
Normal 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;
|
@ -62,6 +62,31 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (i.event) {
|
||||||
|
i.event.forEach((event) => {
|
||||||
|
if (event.listen === 'prerequest' && event.script && event.script.exec) {
|
||||||
|
if (!brunoRequestItem.request.script) {
|
||||||
|
brunoRequestItem.request.script = {};
|
||||||
|
}
|
||||||
|
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 (!brunoRequestItem.request.tests) {
|
||||||
|
brunoRequestItem.request.tests = {};
|
||||||
|
}
|
||||||
|
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]} `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const bodyMode = get(i, 'request.body.mode');
|
const bodyMode = get(i, 'request.body.mode');
|
||||||
if (bodyMode) {
|
if (bodyMode) {
|
||||||
if (bodyMode === 'formdata') {
|
if (bodyMode === 'formdata') {
|
||||||
|
@ -8,8 +8,10 @@ export const sendNetworkRequest = async (item, collection, environment, collecti
|
|||||||
resolve({
|
resolve({
|
||||||
state: 'success',
|
state: 'success',
|
||||||
data: response.data,
|
data: response.data,
|
||||||
|
// Note that the Buffer is encoded as a base64 string, because Buffers / TypedArrays are not allowed in the redux store
|
||||||
|
dataBuffer: response.dataBuffer,
|
||||||
headers: Object.entries(response.headers),
|
headers: Object.entries(response.headers),
|
||||||
size: getResponseSize(response),
|
size: response.size,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
statusText: response.statusText,
|
statusText: response.statusText,
|
||||||
duration: response.duration
|
duration: response.duration
|
||||||
@ -31,10 +33,6 @@ const sendHttpRequest = async (item, collection, environment, collectionVariable
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getResponseSize = (response) => {
|
|
||||||
return response.headers['content-length'] || Buffer.byteLength(safeStringifyJSON(response.data)) || 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchGqlSchema = async (endpoint, environment, request, collection) => {
|
export const fetchGqlSchema = async (endpoint, environment, request, collection) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const { ipcRenderer } = window;
|
const { ipcRenderer } = window;
|
||||||
@ -45,6 +43,8 @@ export const fetchGqlSchema = async (endpoint, environment, request, collection)
|
|||||||
|
|
||||||
export const cancelNetworkRequest = async (cancelTokenUid) => {
|
export const cancelNetworkRequest = async (cancelTokenUid) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
const { ipcRenderer } = window;
|
||||||
|
|
||||||
ipcRenderer.invoke('cancel-http-request', cancelTokenUid).then(resolve).catch(reject);
|
ipcRenderer.invoke('cancel-http-request', cancelTokenUid).then(resolve).catch(reject);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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",
|
||||||
|
@ -162,9 +162,7 @@ const getCollectionRoot = (dir) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const content = fs.readFileSync(collectionRootPath, 'utf8');
|
const content = fs.readFileSync(collectionRootPath, 'utf8');
|
||||||
const json = collectionBruToJson(content);
|
return collectionBruToJson(content);
|
||||||
|
|
||||||
return json;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const builder = async (yargs) => {
|
const builder = async (yargs) => {
|
||||||
|
@ -88,6 +88,13 @@ const prepareRequest = (request, collectionRoot) => {
|
|||||||
axiosRequest.data = request.body.xml;
|
axiosRequest.data = request.body.xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.body.mode === 'sparql') {
|
||||||
|
if (!contentTypeDefined) {
|
||||||
|
axiosRequest.headers['content-type'] = 'application/sparql-query';
|
||||||
|
}
|
||||||
|
axiosRequest.data = request.body.sparql;
|
||||||
|
}
|
||||||
|
|
||||||
if (request.body.mode === 'formUrlEncoded') {
|
if (request.body.mode === 'formUrlEncoded') {
|
||||||
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
|
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
|
||||||
const params = {};
|
const params = {};
|
||||||
|
@ -12,10 +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, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
|
||||||
|
|
||||||
const runSingleRequest = async function (
|
const runSingleRequest = async function (
|
||||||
filename,
|
filename,
|
||||||
@ -48,7 +48,7 @@ const runSingleRequest = async function (
|
|||||||
|
|
||||||
// run pre-request vars
|
// run pre-request vars
|
||||||
const preRequestVars = get(bruJson, 'request.vars.req');
|
const preRequestVars = get(bruJson, 'request.vars.req');
|
||||||
if (preRequestVars && preRequestVars.length) {
|
if (preRequestVars?.length) {
|
||||||
const varsRuntime = new VarsRuntime();
|
const varsRuntime = new VarsRuntime();
|
||||||
varsRuntime.runPreRequestVars(
|
varsRuntime.runPreRequestVars(
|
||||||
preRequestVars,
|
preRequestVars,
|
||||||
@ -65,7 +65,7 @@ const runSingleRequest = async function (
|
|||||||
get(collectionRoot, 'request.script.req'),
|
get(collectionRoot, 'request.script.req'),
|
||||||
get(bruJson, 'request.script.req')
|
get(bruJson, 'request.script.req')
|
||||||
]).join(os.EOL);
|
]).join(os.EOL);
|
||||||
if (requestScriptFile && requestScriptFile.length) {
|
if (requestScriptFile?.length) {
|
||||||
const scriptRuntime = new ScriptRuntime();
|
const scriptRuntime = new ScriptRuntime();
|
||||||
const result = await scriptRuntime.runRequestScript(
|
const result = await scriptRuntime.runRequestScript(
|
||||||
decomment(requestScriptFile),
|
decomment(requestScriptFile),
|
||||||
@ -91,36 +91,56 @@ const runSingleRequest = async function (
|
|||||||
if (insecure) {
|
if (insecure) {
|
||||||
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
httpsAgentRequestFields['rejectUnauthorized'] = false;
|
||||||
} else {
|
} else {
|
||||||
const cacertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS];
|
const caCertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS];
|
||||||
const cacert = cacertArray.find((el) => el);
|
const caCert = caCertArray.find((el) => el);
|
||||||
if (cacert && cacert.length > 1) {
|
if (caCert && caCert.length > 1) {
|
||||||
try {
|
try {
|
||||||
caCrt = fs.readFileSync(cacert);
|
httpsAgentRequestFields['ca'] = fs.readFileSync(caCert);
|
||||||
httpsAgentRequestFields['ca'] = caCrt;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Error reading CA cert file:' + cacert, err);
|
console.log('Error reading CA cert file:' + caCert, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// set proxy if enabled
|
|
||||||
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
|
|
||||||
if (proxyEnabled) {
|
|
||||||
let proxyUri;
|
|
||||||
const interpolationOptions = {
|
const interpolationOptions = {
|
||||||
envVars: envVariables,
|
envVars: envVariables,
|
||||||
collectionVariables,
|
collectionVariables,
|
||||||
processEnvVars
|
processEnvVars
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// client certificate config
|
||||||
|
const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);
|
||||||
|
for (let clientCert of clientCertConfig) {
|
||||||
|
const domain = interpolateString(clientCert.domain, interpolationOptions);
|
||||||
|
const certFilePath = interpolateString(clientCert.certFilePath, interpolationOptions);
|
||||||
|
const keyFilePath = interpolateString(clientCert.keyFilePath, interpolationOptions);
|
||||||
|
if (domain && certFilePath && keyFilePath) {
|
||||||
|
const hostRegex = '^https:\\/\\/' + domain.replaceAll('.', '\\.').replaceAll('*', '.*');
|
||||||
|
|
||||||
|
if (request.url.match(hostRegex)) {
|
||||||
|
try {
|
||||||
|
httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);
|
||||||
|
httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error reading cert/key file', err);
|
||||||
|
}
|
||||||
|
httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set proxy if enabled
|
||||||
|
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
|
||||||
|
const shouldProxy = shouldUseProxy(request.url, get(brunoConfig, 'proxy.bypassProxy', ''));
|
||||||
|
if (proxyEnabled && shouldProxy) {
|
||||||
const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
|
const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
|
||||||
const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
|
const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
|
||||||
const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
|
const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
|
||||||
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
|
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
|
||||||
const socksEnabled = proxyProtocol.includes('socks');
|
const socksEnabled = proxyProtocol.includes('socks');
|
||||||
|
|
||||||
interpolateString;
|
let proxyUri;
|
||||||
|
|
||||||
if (proxyAuthEnabled) {
|
if (proxyAuthEnabled) {
|
||||||
const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
|
const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
|
||||||
const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
|
const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
|
||||||
@ -132,16 +152,13 @@ const runSingleRequest = async function (
|
|||||||
|
|
||||||
if (socksEnabled) {
|
if (socksEnabled) {
|
||||||
const socksProxyAgent = new SocksProxyAgent(proxyUri);
|
const socksProxyAgent = new SocksProxyAgent(proxyUri);
|
||||||
|
|
||||||
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
|
||||||
);
|
);
|
||||||
|
|
||||||
request.httpAgent = new HttpProxyAgent(proxyUri);
|
request.httpAgent = new HttpProxyAgent(proxyUri);
|
||||||
}
|
}
|
||||||
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
|
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
|
||||||
@ -167,7 +184,7 @@ const runSingleRequest = async function (
|
|||||||
responseTime = response.headers.get('request-duration');
|
responseTime = response.headers.get('request-duration');
|
||||||
response.headers.delete('request-duration');
|
response.headers.delete('request-duration');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err && err.response) {
|
if (err?.response) {
|
||||||
response = err.response;
|
response = err.response;
|
||||||
|
|
||||||
// Prevents the duration on leaking to the actual result
|
// Prevents the duration on leaking to the actual result
|
||||||
@ -204,7 +221,7 @@ const runSingleRequest = async function (
|
|||||||
|
|
||||||
// run post-response vars
|
// run post-response vars
|
||||||
const postResponseVars = get(bruJson, 'request.vars.res');
|
const postResponseVars = get(bruJson, 'request.vars.res');
|
||||||
if (postResponseVars && postResponseVars.length) {
|
if (postResponseVars?.length) {
|
||||||
const varsRuntime = new VarsRuntime();
|
const varsRuntime = new VarsRuntime();
|
||||||
varsRuntime.runPostResponseVars(
|
varsRuntime.runPostResponseVars(
|
||||||
postResponseVars,
|
postResponseVars,
|
||||||
@ -222,7 +239,7 @@ const runSingleRequest = async function (
|
|||||||
get(collectionRoot, 'request.script.res'),
|
get(collectionRoot, 'request.script.res'),
|
||||||
get(bruJson, 'request.script.res')
|
get(bruJson, 'request.script.res')
|
||||||
]).join(os.EOL);
|
]).join(os.EOL);
|
||||||
if (responseScriptFile && responseScriptFile.length) {
|
if (responseScriptFile?.length) {
|
||||||
const scriptRuntime = new ScriptRuntime();
|
const scriptRuntime = new ScriptRuntime();
|
||||||
const result = await scriptRuntime.runResponseScript(
|
const result = await scriptRuntime.runResponseScript(
|
||||||
decomment(responseScriptFile),
|
decomment(responseScriptFile),
|
||||||
@ -283,7 +300,7 @@ const runSingleRequest = async function (
|
|||||||
testResults = get(result, 'results', []);
|
testResults = get(result, 'results', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (testResults && testResults.length) {
|
if (testResults?.length) {
|
||||||
each(testResults, (testResult) => {
|
each(testResults, (testResult) => {
|
||||||
if (testResult.status === 'pass') {
|
if (testResult.status === 'pass') {
|
||||||
console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description));
|
console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description));
|
||||||
|
@ -4,10 +4,10 @@ const axios = require('axios');
|
|||||||
* Function that configures axios with timing interceptors
|
* Function that configures axios with timing interceptors
|
||||||
* Important to note here that the timings are not completely accurate.
|
* Important to note here that the timings are not completely accurate.
|
||||||
* @see https://github.com/axios/axios/issues/695
|
* @see https://github.com/axios/axios/issues/695
|
||||||
* @returns {import('axios').AxiosStatic}
|
* @returns {axios.AxiosInstance}
|
||||||
*/
|
*/
|
||||||
function makeAxiosInstance() {
|
function makeAxiosInstance() {
|
||||||
/** @type {import('axios').AxiosStatic} */
|
/** @type {axios.AxiosInstance} */
|
||||||
const instance = axios.create();
|
const instance = axios.create();
|
||||||
|
|
||||||
instance.interceptors.request.use((config) => {
|
instance.interceptors.request.use((config) => {
|
||||||
@ -26,10 +26,8 @@ function makeAxiosInstance() {
|
|||||||
if (error.response) {
|
if (error.response) {
|
||||||
const end = Date.now();
|
const end = Date.now();
|
||||||
const start = error.config.headers['request-start-time'];
|
const start = error.config.headers['request-start-time'];
|
||||||
if (error.response) {
|
|
||||||
error.response.headers['request-duration'] = end - start;
|
error.response.headers['request-duration'] = end - start;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
85
packages/bruno-cli/src/utils/proxy-util.js
Normal file
85
packages/bruno-cli/src/utils/proxy-util.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
const parseUrl = require('url').parse;
|
||||||
|
const { isEmpty } = require('lodash');
|
||||||
|
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||||
|
|
||||||
|
const DEFAULT_PORTS = {
|
||||||
|
ftp: 21,
|
||||||
|
gopher: 70,
|
||||||
|
http: 80,
|
||||||
|
https: 443,
|
||||||
|
ws: 80,
|
||||||
|
wss: 443
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* check for proxy bypass, copied form 'proxy-from-env'
|
||||||
|
*/
|
||||||
|
const shouldUseProxy = (url, proxyBypass) => {
|
||||||
|
if (proxyBypass === '*') {
|
||||||
|
return false; // Never proxy if wildcard is set.
|
||||||
|
}
|
||||||
|
|
||||||
|
// use proxy if no proxyBypass is set
|
||||||
|
if (!proxyBypass || typeof proxyBypass !== 'string' || isEmpty(proxyBypass.trim())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedUrl = typeof url === 'string' ? parseUrl(url) : url || {};
|
||||||
|
let proto = parsedUrl.protocol;
|
||||||
|
let hostname = parsedUrl.host;
|
||||||
|
let port = parsedUrl.port;
|
||||||
|
if (typeof hostname !== 'string' || !hostname || typeof proto !== 'string') {
|
||||||
|
return false; // Don't proxy URLs without a valid scheme or host.
|
||||||
|
}
|
||||||
|
|
||||||
|
proto = proto.split(':', 1)[0];
|
||||||
|
// Stripping ports in this way instead of using parsedUrl.hostname to make
|
||||||
|
// sure that the brackets around IPv6 addresses are kept.
|
||||||
|
hostname = hostname.replace(/:\d*$/, '');
|
||||||
|
port = parseInt(port) || DEFAULT_PORTS[proto] || 0;
|
||||||
|
|
||||||
|
return proxyBypass.split(/[,;\s]/).every(function (dontProxyFor) {
|
||||||
|
if (!dontProxyFor) {
|
||||||
|
return true; // Skip zero-length hosts.
|
||||||
|
}
|
||||||
|
const parsedProxy = dontProxyFor.match(/^(.+):(\d+)$/);
|
||||||
|
let parsedProxyHostname = parsedProxy ? parsedProxy[1] : dontProxyFor;
|
||||||
|
const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2]) : 0;
|
||||||
|
if (parsedProxyPort && parsedProxyPort !== port) {
|
||||||
|
return true; // Skip if ports don't match.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[.*]/.test(parsedProxyHostname)) {
|
||||||
|
// No wildcards, so stop proxying if there is an exact match.
|
||||||
|
return hostname !== parsedProxyHostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedProxyHostname.charAt(0) === '*') {
|
||||||
|
// Remove leading wildcard.
|
||||||
|
parsedProxyHostname = parsedProxyHostname.slice(1);
|
||||||
|
}
|
||||||
|
// Stop proxying if the hostname ends with the no_proxy host.
|
||||||
|
return !hostname.endsWith(parsedProxyHostname);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = {
|
||||||
|
shouldUseProxy,
|
||||||
|
PatchedHttpsProxyAgent
|
||||||
|
};
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "v0.25.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,14 +20,13 @@
|
|||||||
},
|
},
|
||||||
"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",
|
||||||
"decomment": "^0.9.5",
|
"decomment": "^0.9.5",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
@ -42,6 +41,7 @@
|
|||||||
"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",
|
||||||
"mustache": "^4.2.0",
|
"mustache": "^4.2.0",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
|
@ -44,7 +44,7 @@ const template = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'window',
|
role: 'window',
|
||||||
submenu: [{ role: 'minimize' }, { role: 'close' }]
|
submenu: [{ role: 'minimize' }, { role: 'close', accelerator: 'CommandOrControl+Shift+Q' }]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: 'help',
|
role: 'help',
|
||||||
|
@ -10,7 +10,7 @@ const registerNetworkIpc = require('./ipc/network');
|
|||||||
const registerCollectionsIpc = require('./ipc/collection');
|
const registerCollectionsIpc = require('./ipc/collection');
|
||||||
const registerPreferencesIpc = require('./ipc/preferences');
|
const registerPreferencesIpc = require('./ipc/preferences');
|
||||||
const Watcher = require('./app/watcher');
|
const Watcher = require('./app/watcher');
|
||||||
const { loadWindowState, saveWindowState } = require('./utils/window');
|
const { loadWindowState, saveBounds, saveMaximized } = require('./utils/window');
|
||||||
|
|
||||||
const lastOpenedCollections = new LastOpenedCollections();
|
const lastOpenedCollections = new LastOpenedCollections();
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ let watcher;
|
|||||||
|
|
||||||
// Prepare the renderer once the app is ready
|
// Prepare the renderer once the app is ready
|
||||||
app.on('ready', async () => {
|
app.on('ready', async () => {
|
||||||
const { x, y, width, height } = loadWindowState();
|
const { maximized, x, y, width, height } = loadWindowState();
|
||||||
|
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
x,
|
x,
|
||||||
@ -55,6 +55,10 @@ app.on('ready', async () => {
|
|||||||
// autoHideMenuBar: true
|
// autoHideMenuBar: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (maximized) {
|
||||||
|
mainWindow.maximize();
|
||||||
|
}
|
||||||
|
|
||||||
const url = isDev
|
const url = isDev
|
||||||
? 'http://localhost:3000'
|
? 'http://localhost:3000'
|
||||||
: format({
|
: format({
|
||||||
@ -66,8 +70,17 @@ app.on('ready', async () => {
|
|||||||
mainWindow.loadURL(url);
|
mainWindow.loadURL(url);
|
||||||
watcher = new Watcher();
|
watcher = new Watcher();
|
||||||
|
|
||||||
mainWindow.on('resize', () => saveWindowState(mainWindow));
|
const handleBoundsChange = () => {
|
||||||
mainWindow.on('move', () => saveWindowState(mainWindow));
|
if (!mainWindow.isMaximized()) {
|
||||||
|
saveBounds(mainWindow);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mainWindow.on('resize', handleBoundsChange);
|
||||||
|
mainWindow.on('move', handleBoundsChange);
|
||||||
|
|
||||||
|
mainWindow.on('maximize', () => saveMaximized(true));
|
||||||
|
mainWindow.on('unmaximize', () => saveMaximized(false));
|
||||||
|
|
||||||
mainWindow.webContents.on('new-window', function (e, url) {
|
mainWindow.webContents.on('new-window', function (e, url) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -75,7 +88,7 @@ app.on('ready', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// register all ipc handlers
|
// register all ipc handlers
|
||||||
registerNetworkIpc(mainWindow, watcher, lastOpenedCollections);
|
registerNetworkIpc(mainWindow);
|
||||||
registerCollectionsIpc(mainWindow, watcher, lastOpenedCollections);
|
registerCollectionsIpc(mainWindow, watcher, lastOpenedCollections);
|
||||||
registerPreferencesIpc(mainWindow, watcher, lastOpenedCollections);
|
registerPreferencesIpc(mainWindow, watcher, lastOpenedCollections);
|
||||||
});
|
});
|
||||||
|
@ -32,9 +32,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
// browse directory
|
// browse directory
|
||||||
ipcMain.handle('renderer:browse-directory', async (event, pathname, request) => {
|
ipcMain.handle('renderer:browse-directory', async (event, pathname, request) => {
|
||||||
try {
|
try {
|
||||||
const dirPath = await browseDirectory(mainWindow);
|
return await browseDirectory(mainWindow);
|
||||||
|
|
||||||
return dirPath;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
@ -67,8 +65,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
|
|
||||||
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
|
mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);
|
||||||
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
|
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
|
||||||
|
|
||||||
return;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
@ -93,8 +89,6 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
collectionPathname,
|
collectionPathname,
|
||||||
newName
|
newName
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
@ -311,7 +305,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
|
|||||||
|
|
||||||
fs.unlinkSync(pathname);
|
fs.unlinkSync(pathname);
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject(error);
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
|
@ -5,7 +5,7 @@ function isStrPresent(str) {
|
|||||||
return str && str !== '' && str !== 'undefined';
|
return str && str !== '' && str !== 'undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveCredentials(request) {
|
async function resolveAwsV4Credentials(request) {
|
||||||
const awsv4 = request.awsv4config;
|
const awsv4 = request.awsv4config;
|
||||||
if (isStrPresent(awsv4.profileName)) {
|
if (isStrPresent(awsv4.profileName)) {
|
||||||
try {
|
try {
|
||||||
@ -52,5 +52,5 @@ function addAwsV4Interceptor(axiosInstance, request) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
addAwsV4Interceptor,
|
addAwsV4Interceptor,
|
||||||
resolveCredentials
|
resolveAwsV4Credentials
|
||||||
};
|
};
|
||||||
|
@ -4,10 +4,10 @@ const axios = require('axios');
|
|||||||
* Function that configures axios with timing interceptors
|
* Function that configures axios with timing interceptors
|
||||||
* Important to note here that the timings are not completely accurate.
|
* Important to note here that the timings are not completely accurate.
|
||||||
* @see https://github.com/axios/axios/issues/695
|
* @see https://github.com/axios/axios/issues/695
|
||||||
* @returns {import('axios').AxiosStatic}
|
* @returns {axios.AxiosInstance}
|
||||||
*/
|
*/
|
||||||
function makeAxiosInstance() {
|
function makeAxiosInstance() {
|
||||||
/** @type {import('axios').AxiosStatic} */
|
/** @type {axios.AxiosInstance} */
|
||||||
const instance = axios.create();
|
const instance = axios.create();
|
||||||
|
|
||||||
instance.interceptors.request.use((config) => {
|
instance.interceptors.request.use((config) => {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -84,7 +84,8 @@ const prepareRequest = (request, collectionRoot) => {
|
|||||||
let axiosRequest = {
|
let axiosRequest = {
|
||||||
method: request.method,
|
method: request.method,
|
||||||
url: request.url,
|
url: request.url,
|
||||||
headers: headers
|
headers: headers,
|
||||||
|
responseType: 'arraybuffer'
|
||||||
};
|
};
|
||||||
|
|
||||||
axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);
|
axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);
|
||||||
@ -136,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') {
|
||||||
|
@ -2,7 +2,7 @@ const { ipcMain } = require('electron');
|
|||||||
const { getPreferences, savePreferences } = require('../store/preferences');
|
const { getPreferences, savePreferences } = require('../store/preferences');
|
||||||
const { isDirectory } = require('../utils/filesystem');
|
const { isDirectory } = require('../utils/filesystem');
|
||||||
const { openCollection } = require('../app/collections');
|
const { openCollection } = require('../app/collections');
|
||||||
|
``;
|
||||||
const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
||||||
ipcMain.handle('renderer:ready', async (event) => {
|
ipcMain.handle('renderer:ready', async (event) => {
|
||||||
// load preferences
|
// load preferences
|
||||||
@ -15,7 +15,7 @@ const registerPreferencesIpc = (mainWindow, watcher, lastOpenedCollections) => {
|
|||||||
if (lastOpened && lastOpened.length) {
|
if (lastOpened && lastOpened.length) {
|
||||||
for (let collectionPath of lastOpened) {
|
for (let collectionPath of lastOpened) {
|
||||||
if (isDirectory(collectionPath)) {
|
if (isDirectory(collectionPath)) {
|
||||||
openCollection(mainWindow, watcher, collectionPath, {
|
await openCollection(mainWindow, watcher, collectionPath, {
|
||||||
dontSendDisplayErrors: true
|
dontSendDisplayErrors: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
const Yup = require('yup');
|
const Yup = require('yup');
|
||||||
const Store = require('electron-store');
|
const Store = require('electron-store');
|
||||||
|
const { get } = require('lodash');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The preferences are stored in the electron store 'preferences.json'.
|
||||||
|
* The electron process uses this module to get the preferences.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
const defaultPreferences = {
|
const defaultPreferences = {
|
||||||
request: {
|
request: {
|
||||||
@ -8,6 +15,18 @@ const defaultPreferences = {
|
|||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
codeFont: 'default'
|
codeFont: 'default'
|
||||||
|
},
|
||||||
|
proxy: {
|
||||||
|
enabled: false,
|
||||||
|
protocol: 'http',
|
||||||
|
hostname: '',
|
||||||
|
port: null,
|
||||||
|
auth: {
|
||||||
|
enabled: false,
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
},
|
||||||
|
bypassProxy: ''
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -18,6 +37,18 @@ const preferencesSchema = Yup.object().shape({
|
|||||||
}),
|
}),
|
||||||
font: Yup.object().shape({
|
font: Yup.object().shape({
|
||||||
codeFont: Yup.string().nullable()
|
codeFont: Yup.string().nullable()
|
||||||
|
}),
|
||||||
|
proxy: Yup.object({
|
||||||
|
enabled: Yup.boolean(),
|
||||||
|
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
|
||||||
|
hostname: Yup.string().max(1024),
|
||||||
|
port: Yup.number().min(1).max(65535).nullable(),
|
||||||
|
auth: Yup.object({
|
||||||
|
enabled: Yup.boolean(),
|
||||||
|
username: Yup.string().max(1024),
|
||||||
|
password: Yup.string().max(1024)
|
||||||
|
}).optional(),
|
||||||
|
bypassProxy: Yup.string().optional().max(1024)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -61,7 +92,20 @@ const savePreferences = async (newPreferences) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const preferencesUtil = {
|
||||||
|
shouldVerifyTls: () => {
|
||||||
|
return get(getPreferences(), 'request.sslVerification', true);
|
||||||
|
},
|
||||||
|
getRequestTimeout: () => {
|
||||||
|
return get(getPreferences(), 'request.timeout', 0);
|
||||||
|
},
|
||||||
|
getGlobalProxyConfig: () => {
|
||||||
|
return get(getPreferences(), 'proxy', {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getPreferences,
|
getPreferences,
|
||||||
savePreferences
|
savePreferences,
|
||||||
|
preferencesUtil
|
||||||
};
|
};
|
||||||
|
@ -3,6 +3,8 @@ const Store = require('electron-store');
|
|||||||
const DEFAULT_WINDOW_WIDTH = 1280;
|
const DEFAULT_WINDOW_WIDTH = 1280;
|
||||||
const DEFAULT_WINDOW_HEIGHT = 768;
|
const DEFAULT_WINDOW_HEIGHT = 768;
|
||||||
|
|
||||||
|
const DEFAULT_MAXIMIZED = false;
|
||||||
|
|
||||||
class WindowStateStore {
|
class WindowStateStore {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.store = new Store({
|
this.store = new Store({
|
||||||
@ -25,6 +27,14 @@ class WindowStateStore {
|
|||||||
setBounds(bounds) {
|
setBounds(bounds) {
|
||||||
this.store.set('window-bounds', bounds);
|
this.store.set('window-bounds', bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMaximized() {
|
||||||
|
return this.store.get('maximized') || DEFAULT_MAXIMIZED;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMaximized(isMaximized) {
|
||||||
|
this.store.set('maximized', isMaximized);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = WindowStateStore;
|
module.exports = WindowStateStore;
|
||||||
|
85
packages/bruno-electron/src/utils/proxy-util.js
Normal file
85
packages/bruno-electron/src/utils/proxy-util.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
const parseUrl = require('url').parse;
|
||||||
|
const { isEmpty } = require('lodash');
|
||||||
|
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||||
|
|
||||||
|
const DEFAULT_PORTS = {
|
||||||
|
ftp: 21,
|
||||||
|
gopher: 70,
|
||||||
|
http: 80,
|
||||||
|
https: 443,
|
||||||
|
ws: 80,
|
||||||
|
wss: 443
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* check for proxy bypass, copied form 'proxy-from-env'
|
||||||
|
*/
|
||||||
|
const shouldUseProxy = (url, proxyBypass) => {
|
||||||
|
if (proxyBypass === '*') {
|
||||||
|
return false; // Never proxy if wildcard is set.
|
||||||
|
}
|
||||||
|
|
||||||
|
// use proxy if no proxyBypass is set
|
||||||
|
if (!proxyBypass || typeof proxyBypass !== 'string' || isEmpty(proxyBypass.trim())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedUrl = typeof url === 'string' ? parseUrl(url) : url || {};
|
||||||
|
let proto = parsedUrl.protocol;
|
||||||
|
let hostname = parsedUrl.host;
|
||||||
|
let port = parsedUrl.port;
|
||||||
|
if (typeof hostname !== 'string' || !hostname || typeof proto !== 'string') {
|
||||||
|
return false; // Don't proxy URLs without a valid scheme or host.
|
||||||
|
}
|
||||||
|
|
||||||
|
proto = proto.split(':', 1)[0];
|
||||||
|
// Stripping ports in this way instead of using parsedUrl.hostname to make
|
||||||
|
// sure that the brackets around IPv6 addresses are kept.
|
||||||
|
hostname = hostname.replace(/:\d*$/, '');
|
||||||
|
port = parseInt(port) || DEFAULT_PORTS[proto] || 0;
|
||||||
|
|
||||||
|
return proxyBypass.split(/[,;\s]/).every(function (dontProxyFor) {
|
||||||
|
if (!dontProxyFor) {
|
||||||
|
return true; // Skip zero-length hosts.
|
||||||
|
}
|
||||||
|
const parsedProxy = dontProxyFor.match(/^(.+):(\d+)$/);
|
||||||
|
let parsedProxyHostname = parsedProxy ? parsedProxy[1] : dontProxyFor;
|
||||||
|
const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2]) : 0;
|
||||||
|
if (parsedProxyPort && parsedProxyPort !== port) {
|
||||||
|
return true; // Skip if ports don't match.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[.*]/.test(parsedProxyHostname)) {
|
||||||
|
// No wildcards, so stop proxying if there is an exact match.
|
||||||
|
return hostname !== parsedProxyHostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedProxyHostname.charAt(0) === '*') {
|
||||||
|
// Remove leading wildcard.
|
||||||
|
parsedProxyHostname = parsedProxyHostname.slice(1);
|
||||||
|
}
|
||||||
|
// Stop proxying if the hostname ends with the no_proxy host.
|
||||||
|
return !hostname.endsWith(parsedProxyHostname);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = {
|
||||||
|
shouldUseProxy,
|
||||||
|
PatchedHttpsProxyAgent
|
||||||
|
};
|
@ -7,12 +7,14 @@ const DEFAULT_WINDOW_WIDTH = 1280;
|
|||||||
const DEFAULT_WINDOW_HEIGHT = 768;
|
const DEFAULT_WINDOW_HEIGHT = 768;
|
||||||
|
|
||||||
const loadWindowState = () => {
|
const loadWindowState = () => {
|
||||||
|
const maximized = windowStateStore.getMaximized();
|
||||||
const bounds = windowStateStore.getBounds();
|
const bounds = windowStateStore.getBounds();
|
||||||
|
|
||||||
const positionValid = isPositionValid(bounds);
|
const positionValid = isPositionValid(bounds);
|
||||||
const sizeValid = isSizeValid(bounds);
|
const sizeValid = isSizeValid(bounds);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
maximized,
|
||||||
x: bounds.x && positionValid ? bounds.x : undefined,
|
x: bounds.x && positionValid ? bounds.x : undefined,
|
||||||
y: bounds.y && positionValid ? bounds.y : undefined,
|
y: bounds.y && positionValid ? bounds.y : undefined,
|
||||||
width: bounds.width && sizeValid ? bounds.width : DEFAULT_WINDOW_WIDTH,
|
width: bounds.width && sizeValid ? bounds.width : DEFAULT_WINDOW_WIDTH,
|
||||||
@ -20,12 +22,16 @@ const loadWindowState = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveWindowState = (window) => {
|
const saveBounds = (window) => {
|
||||||
const bounds = window.getBounds();
|
const bounds = window.getBounds();
|
||||||
|
|
||||||
windowStateStore.setBounds(bounds);
|
windowStateStore.setBounds(bounds);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const saveMaximized = (isMaximized) => {
|
||||||
|
windowStateStore.setMaximized(isMaximized);
|
||||||
|
};
|
||||||
|
|
||||||
const isPositionValid = (bounds) => {
|
const isPositionValid = (bounds) => {
|
||||||
const area = getArea(bounds);
|
const area = getArea(bounds);
|
||||||
|
|
||||||
@ -49,5 +55,6 @@ const getArea = (bounds) => {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
loadWindowState,
|
loadWindowState,
|
||||||
saveWindowState
|
saveBounds,
|
||||||
|
saveMaximized
|
||||||
};
|
};
|
||||||
|
50
packages/bruno-electron/tests/utils/proxy-util.spec.js
Normal file
50
packages/bruno-electron/tests/utils/proxy-util.spec.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
const { shouldUseProxy } = require('../../src/utils/proxy-util');
|
||||||
|
|
||||||
|
test('no proxy necessary - star', () => {
|
||||||
|
const url = 'http://wwww.example.org/test';
|
||||||
|
const noProxy = '*';
|
||||||
|
|
||||||
|
expect(shouldUseProxy(url, noProxy)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no proxy necessary - no noProxy bypass', () => {
|
||||||
|
const url = 'http://wwww.example.org/test';
|
||||||
|
const noProxy = '';
|
||||||
|
|
||||||
|
expect(shouldUseProxy(url, noProxy)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no proxy necessary - wildcard match', () => {
|
||||||
|
const url = 'http://wwww.example.org/test';
|
||||||
|
const noProxy = '*example.org';
|
||||||
|
|
||||||
|
expect(shouldUseProxy(url, noProxy)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no proxy necessary - direct proxy', () => {
|
||||||
|
const url = 'http://wwww.example.org/test';
|
||||||
|
const noProxy = 'wwww.example.org';
|
||||||
|
|
||||||
|
expect(shouldUseProxy(url, noProxy)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no proxy necessary - multiple proxy', () => {
|
||||||
|
const url = 'http://wwww.example.org/test';
|
||||||
|
const noProxy = 'www.example.com,wwww.example.org';
|
||||||
|
|
||||||
|
expect(shouldUseProxy(url, noProxy)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('proxy necessary - no proxy match multiple', () => {
|
||||||
|
const url = 'https://wwww.example.test/test';
|
||||||
|
const noProxy = 'www.example.com,wwww.example.org';
|
||||||
|
|
||||||
|
expect(shouldUseProxy(url, noProxy)).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('proxy necessary - no proxy match', () => {
|
||||||
|
const url = 'https://wwww.example.test/test';
|
||||||
|
const noProxy = 'www.example.com';
|
||||||
|
|
||||||
|
expect(shouldUseProxy(url, noProxy)).toEqual(true);
|
||||||
|
});
|
@ -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",
|
||||||
|
55
packages/bruno-js/src/interpolate-string.js
Normal file
55
packages/bruno-js/src/interpolate-string.js
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
const Handlebars = require('handlebars');
|
||||||
|
const { forOwn, cloneDeep } = require('lodash');
|
||||||
|
|
||||||
|
const interpolateEnvVars = (str, processEnvVars) => {
|
||||||
|
if (!str || !str.length || typeof str !== 'string') {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = Handlebars.compile(str, { noEscape: true });
|
||||||
|
|
||||||
|
return template({
|
||||||
|
process: {
|
||||||
|
env: {
|
||||||
|
...processEnvVars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const interpolateString = (str, { envVariables, collectionVariables, processEnvVars }) => {
|
||||||
|
if (!str || !str.length || typeof str !== 'string') {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
processEnvVars = processEnvVars || {};
|
||||||
|
collectionVariables = collectionVariables || {};
|
||||||
|
|
||||||
|
// we clone envVariables because we don't want to modify the original object
|
||||||
|
envVariables = envVariables ? cloneDeep(envVariables) : {};
|
||||||
|
|
||||||
|
// envVariables can inturn have values as {{process.env.VAR_NAME}}
|
||||||
|
// so we need to interpolate envVariables first with processEnvVars
|
||||||
|
forOwn(envVariables, (value, key) => {
|
||||||
|
envVariables[key] = interpolateEnvVars(value, processEnvVars);
|
||||||
|
});
|
||||||
|
|
||||||
|
const template = Handlebars.compile(str, { noEscape: true });
|
||||||
|
|
||||||
|
// collectionVariables take precedence over envVariables
|
||||||
|
const combinedVars = {
|
||||||
|
...envVariables,
|
||||||
|
...collectionVariables,
|
||||||
|
process: {
|
||||||
|
env: {
|
||||||
|
...processEnvVars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return template(combinedVars);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
interpolateString
|
||||||
|
};
|
@ -4,6 +4,7 @@ const { nanoid } = require('nanoid');
|
|||||||
const Bru = require('../bru');
|
const Bru = require('../bru');
|
||||||
const BrunoRequest = require('../bruno-request');
|
const BrunoRequest = require('../bruno-request');
|
||||||
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
|
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
|
||||||
|
const { interpolateString } = require('../interpolate-string');
|
||||||
|
|
||||||
const { expect } = chai;
|
const { expect } = chai;
|
||||||
chai.use(require('chai-string'));
|
chai.use(require('chai-string'));
|
||||||
@ -161,17 +162,27 @@ const evaluateRhsOperand = (rhsOperand, operator, context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const interpolationContext = {
|
||||||
|
collectionVariables: context.bru.collectionVariables,
|
||||||
|
envVariables: context.bru.envVariables,
|
||||||
|
processEnvVars: context.bru.processEnvVars
|
||||||
|
};
|
||||||
|
|
||||||
// gracefully allow both a,b as well as [a, b]
|
// gracefully allow both a,b as well as [a, b]
|
||||||
if (operator === 'in' || operator === 'notIn') {
|
if (operator === 'in' || operator === 'notIn') {
|
||||||
if (rhsOperand.startsWith('[') && rhsOperand.endsWith(']')) {
|
if (rhsOperand.startsWith('[') && rhsOperand.endsWith(']')) {
|
||||||
rhsOperand = rhsOperand.substring(1, rhsOperand.length - 1);
|
rhsOperand = rhsOperand.substring(1, rhsOperand.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rhsOperand.split(',').map((v) => evaluateJsTemplateLiteral(v.trim(), context));
|
return rhsOperand
|
||||||
|
.split(',')
|
||||||
|
.map((v) => evaluateJsTemplateLiteral(interpolateString(v.trim(), interpolationContext), context));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (operator === 'between') {
|
if (operator === 'between') {
|
||||||
const [lhs, rhs] = rhsOperand.split(',').map((v) => evaluateJsTemplateLiteral(v.trim(), context));
|
const [lhs, rhs] = rhsOperand
|
||||||
|
.split(',')
|
||||||
|
.map((v) => evaluateJsTemplateLiteral(interpolateString(v.trim(), interpolationContext), context));
|
||||||
return [lhs, rhs];
|
return [lhs, rhs];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,10 +192,10 @@ const evaluateRhsOperand = (rhsOperand, operator, context) => {
|
|||||||
rhsOperand = rhsOperand.substring(1, rhsOperand.length - 1);
|
rhsOperand = rhsOperand.substring(1, rhsOperand.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rhsOperand;
|
return interpolateString(rhsOperand, interpolationContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
return evaluateJsTemplateLiteral(rhsOperand, context);
|
return evaluateJsTemplateLiteral(interpolateString(rhsOperand, interpolationContext), context);
|
||||||
};
|
};
|
||||||
|
|
||||||
class AssertRuntime {
|
class AssertRuntime {
|
||||||
|
@ -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": [
|
||||||
|
@ -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 {
|
||||||
|
@ -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')
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
@ -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": [
|
||||||
|
6
publishing.md
Normal file
6
publishing.md
Normal 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, 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
|
21
readme.md
21
readme.md
@ -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_ru.md)
|
**English** | [Українська](/readme_ua.md) | [Русский](/readme_ru.md) | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.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 🎥
|
||||||
@ -52,7 +53,11 @@ Woof! If you like project, hit that ⭐ button !!
|
|||||||
|
|
||||||
### Share Testimonials 📣
|
### Share Testimonials 📣
|
||||||
|
|
||||||
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 👩💻🧑💻
|
||||||
|
|
||||||
@ -72,9 +77,19 @@ Even if you are not able to make contributions via code, please don't hesitate t
|
|||||||
|
|
||||||
[Twitter](https://twitter.com/use_bruno) <br />
|
[Twitter](https://twitter.com/use_bruno) <br />
|
||||||
[Website](https://www.usebruno.com) <br />
|
[Website](https://www.usebruno.com) <br />
|
||||||
[Discord](https://discord.com/invite/KgcZUncpjq)
|
[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
95
readme_de.md
Normal 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**
|
||||||
|
|
||||||
|
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)
|
@ -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) | **Русский**
|
[English](/readme.md) | [Українська](/readme_ua.md) | **Русский** | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md)
|
||||||
|
|
||||||
Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами.
|
Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами.
|
||||||
|
|
||||||
|
80
readme_tr.md
Normal file
80
readme_tr.md
Normal 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)
|
||||||
|
|
||||||
|
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)
|
80
readme_ua.md
Normal file
80
readme_ua.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<br />
|
||||||
|
<img src="assets/images/logo-transparent.png" width="80"/>
|
||||||
|
|
||||||
|
### Bruno - IDE із відкритим кодом для тестування та дослідження API
|
||||||
|
|
||||||
|
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
|
||||||
|
[![CI](https://github.com/usebruno/bruno/actions/workflows/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_ru.md) | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md)
|
||||||
|
|
||||||
|
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman.
|
||||||
|
|
||||||
|
Bruno зберігає ваші колекції напряму у теці на вашому диску. Він використовує текстову мову розмітки Bru для збереження інформації про ваші API запити.
|
||||||
|
|
||||||
|
Ви можете використовувати git або будь-яку іншу систему контролю версій щоб спільно працювати над вашими колекціями API запитів.
|
||||||
|
|
||||||
|
Bruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Взнати більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269)
|
||||||
|
|
||||||
|
![bruno](assets/images/landing-2.png) <br /><br />
|
||||||
|
|
||||||
|
### Кросплатформенність 🖥️
|
||||||
|
|
||||||
|
![bruno](assets/images/run-anywhere.png) <br /><br />
|
||||||
|
|
||||||
|
### Спільна робота через Git 👩💻🧑💻
|
||||||
|
|
||||||
|
Або будь-яку іншу систему контролю версій на ваш вибір
|
||||||
|
|
||||||
|
![bruno](assets/images/version-control.png) <br /><br />
|
||||||
|
|
||||||
|
### Важливі посилання 📌
|
||||||
|
|
||||||
|
- [Наше бачення довготривалої перспективи проекту](https://github.com/usebruno/bruno/discussions/269)
|
||||||
|
- [Дорожня карта проекту](https://github.com/usebruno/bruno/discussions/384)
|
||||||
|
- [Документація](https://docs.usebruno.com)
|
||||||
|
- [Сайт](https://www.usebruno.com)
|
||||||
|
- [Завантаження](https://www.usebruno.com/downloads)
|
||||||
|
|
||||||
|
### Вітрина 🎥
|
||||||
|
|
||||||
|
- [Відгуки](https://github.com/usebruno/bruno/discussions/343)
|
||||||
|
- [Хаб знань](https://github.com/usebruno/bruno/discussions/386)
|
||||||
|
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||||
|
|
||||||
|
### Підтримка ❤️
|
||||||
|
|
||||||
|
Гав! Якщо вам сподобався проект, тисніть на ⭐ !!
|
||||||
|
|
||||||
|
### Поділитись відгуками 📣
|
||||||
|
|
||||||
|
Якщо Bruno допоміг вам у вашій роботі і вашим командам, будь ласка не забудьте поділитись вашими [відгуками у github дискусії](https://github.com/usebruno/bruno/discussions/343)
|
||||||
|
|
||||||
|
### Зробити свій внесок 👩💻🧑💻
|
||||||
|
|
||||||
|
Я радий що ви бажаєте покращити Bruno. Будь ласка переглянте [інструкцію по контрибуції](contributing_ua.md)
|
||||||
|
|
||||||
|
Навіть якщо ви не можете зробити свій внесок пишучи програмний код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі.
|
||||||
|
|
||||||
|
### Автори
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Залишайтесь на зв'язку 🌐
|
||||||
|
|
||||||
|
[Twitter](https://twitter.com/use_bruno) <br />
|
||||||
|
[Сайт](https://www.usebruno.com) <br />
|
||||||
|
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
|
||||||
|
[LinkedIn](https://www.linkedin.com/company/usebruno)
|
||||||
|
|
||||||
|
### Ліцензія 📄
|
||||||
|
|
||||||
|
[MIT](license.md)
|
@ -1,8 +1,7 @@
|
|||||||
const os = require('os');
|
const os = require('os');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const exec = util.promisify(require('child_process').exec);
|
const spawn = util.promisify(require('child_process').spawn);
|
||||||
|
|
||||||
|
|
||||||
async function deleteFileIfExists(filePath) {
|
async function deleteFileIfExists(filePath) {
|
||||||
try {
|
try {
|
||||||
@ -47,6 +46,25 @@ async function removeSourceMapFiles(directory) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function execCommandWithOutput(command) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const childProcess = await spawn(command, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
childProcess.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
childProcess.on('exit', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Command exited with code ${code}.`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
// Remove out directory
|
// Remove out directory
|
||||||
@ -67,13 +85,13 @@ async function main() {
|
|||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.endsWith('.html')) {
|
if (file.endsWith('.html')) {
|
||||||
let content = await fs.readFile(`packages/bruno-electron/web/${file}`, 'utf8');
|
let content = await fs.readFile(`packages/bruno-electron/web/${file}`, 'utf8');
|
||||||
content = content.replace(/\/_next\//g, '/_next/');
|
content = content.replace(/\/_next\//g, '_next/');
|
||||||
await fs.writeFile(`packages/bruno-electron/web/${file}`, content);
|
await fs.writeFile(`packages/bruno-electron/web/${file}`, content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove sourcemaps
|
// Remove sourcemaps
|
||||||
await removeSourceMapFiles('packages/bruno-electron/web')
|
await removeSourceMapFiles('packages/bruno-electron/web');
|
||||||
|
|
||||||
// Run npm dist command
|
// Run npm dist command
|
||||||
console.log('Building the Electron distribution');
|
console.log('Building the Electron distribution');
|
||||||
@ -88,8 +106,7 @@ async function main() {
|
|||||||
osArg = 'linux';
|
osArg = 'linux';
|
||||||
}
|
}
|
||||||
|
|
||||||
await exec(`npm run dist-${osArg} --workspace=packages/bruno-electron`);
|
await execCommandWithOutput(`npm run dist:${osArg} --workspace=packages/bruno-electron`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('An error occurred:', error);
|
console.error('An error occurred:', error);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
const { HomePage } = require('../tests/pages/home.page');
|
const { HomePage } = require('../tests/pages/home.page');
|
||||||
import * as faker from './utils/data-faker';
|
const { faker } = require('./utils/data-faker');
|
||||||
|
|
||||||
test.describe('bruno e2e test', () => {
|
test.describe('bruno e2e test', () => {
|
||||||
let homePage;
|
let homePage;
|
||||||
|
Loading…
Reference in New Issue
Block a user