Merge branch 'main' into feature/add-properties-to-collections

This commit is contained in:
Anoop M D 2023-11-19 12:21:48 +05:30 committed by GitHub
commit f345155a94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
127 changed files with 6691 additions and 1276 deletions

View File

@ -11,3 +11,7 @@
- [ ] **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.

View File

@ -30,6 +30,7 @@ jobs:
run: |
npm run build:bruno-query
npm run build:graphql-docs
npm run build:web
npm run build:electron:snap
- name: Install Snapcraft

View File

@ -1,12 +1,12 @@
**English** | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md)
**English** | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md)
## Lets make bruno better, together !!
## Let's make bruno better, together !!
I am happy that you are looking to improve bruno. Below are the guidelines to get started bringing up bruno on your computer.
We are happy that you are looking to improve bruno. Below are the guidelines to get started bringing up bruno on your computer.
### Technology Stack
Bruno is built using NextJs and React. We also use electron to ship a desktop version (that supports local collections)
Bruno is built using Next.js and React. We also use electron to ship a desktop version (that supports local collections)
Libraries we use
@ -23,9 +23,59 @@ Libraries we use
You would need [Node v18.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
### Lets start coding
## Development
Please reference [development.md](docs/development.md) for instructions on running the local development environment.
Bruno is being developed as a desktop app. You need to load the app by running the Next.js app in one terminal and then run the electron app in another terminal.
### Dependencies
- NodeJS v18
### Local Development
```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
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
```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
```
### Testing
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Raising Pull Request
@ -33,5 +83,5 @@ Please reference [development.md](docs/development.md) for instructions on runni
- Please follow the format of creating branches
- feature/[feature name]: This branch should contain changes for a specific feature
- 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

View File

@ -1,37 +0,0 @@
[English](/contributing.md) | [Українська](/contributing_ua.md) | **Русский**
## Давайте вместе сделаем Бруно лучше!!!
Я рад, что Вы хотите усовершенствовать 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
### Приступим к коду
Пожалуйста, обратитесь к [development_ru.md](docs/development_ru.md) для получения инструкций по запуску локальной среды разработки.
### Создание Pull Request
- Пожалуйста, пусть PR будет небольшим и сфокусированным на одной вещи
- Пожалуйста, соблюдайте формат создания веток
- feature/[название функции]: Эта ветка должна содержать изменения для конкретной функции
- Пример: feature/dark-mode
- bugfix/[название ошибки]: Эта ветка должна содержать только исправления для конкретной ошибки
- Пример bugfix/bug-1

View File

@ -1,37 +0,0 @@
[English](/contributing.md) | **Українська** | [Русский](/contributing_ru.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

View File

@ -0,0 +1,87 @@
[English](/contributing.md) | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](/contributing_fr.md) | **বাংলা**
## আসুন ব্রুনোকে আরও ভালো করি, একসাথে!!
আমরা খুশি যে আপনি ব্রুনোর উন্নতি করতে চাইছেন। নীচে আপনার কম্পিউটারে ব্রুনো ইনষ্টল করার নির্দেশিকা রয়েছে৷।
### Technology Stack (প্রযুক্তি স্ট্যাক)
ব্রুনো Next.js এবং React ব্যবহার করে নির্মিত। এছাড়াও আমরা একটি ডেস্কটপ সংস্করণ পাঠাতে ইলেক্ট্রন ব্যবহার করি (যা স্থানীয় সংগ্রহ সমর্থন করে)
নিম্ন লিখিত লাইব্রেরি আমরা ব্যবহার করি -
- CSS - Tailwind
- Code Editors - Codemirror
- State Management - Redux
- Icons - Tabler Icons
- Forms - formik
- Schema Validation - Yup
- Request Client - axios
- Filesystem Watcher - chokidar
### Dependencies (নির্ভরতা)
আপনার প্রয়োজন হবে [নোড v18.x বা সর্বশেষ LTS সংস্করণ](https://nodejs.org/en/) এবং npm 8.x। আমরা প্রকল্পে npm ওয়ার্কস্পেস ব্যবহার করি ।
## Development
ব্রুনো একটি ডেস্কটপ অ্যাপ হিসেবে তৈরি করা হচ্ছে। আপনাকে একটি টার্মিনালে Next.js অ্যাপটি চালিয়ে অ্যাপটি লোড করতে হবে এবং তারপরে অন্য টার্মিনালে ইলেক্ট্রন অ্যাপটি চালাতে হবে।
### Dependencies (নির্ভরতা)
- NodeJS v18
### Local Development
```bash
# nodejs 18 সংস্করণ ব্যবহার করুন
nvm use
# নির্ভরতা ইনস্টল করুন
npm i --legacy-peer-deps
# গ্রাফকিউএল ডক্স তৈরি করুন
npm run build:graphql-docs
# ব্রুনো কোয়েরি তৈরি করুন
npm run build:bruno-query
# NextJs অ্যাপ চালান (টার্মিনাল 1)
npm run dev:web
# ইলেক্ট্রন অ্যাপ চালান (টার্মিনাল 2)
npm run dev:electron
```
### Troubleshooting (সমস্যা সমাধান)
আপনি যখন 'npm install' চালান তখন আপনি একটি 'অসমর্থিত প্ল্যাটফর্ম' ত্রুটির সম্মুখীন হতে পারেন৷ এটি ঠিক করতে, আপনাকে `node_modules` এবং `package-lock.json` মুছে ফেলতে হবে এবং `npm install` চালাতে হবে। এটি অ্যাপটি চালানোর জন্য প্রয়োজনীয় সমস্ত প্যাকেজ ইনস্টল করবে যাতে এই ত্রুটি ঠিক হয়ে যেতে পারে ।
```shell
# সাব-ডিরেক্টরিতে নোড_মডিউল মুছুন
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# সাব-ডিরেক্টরিতে প্যাকেজ-লক মুছুন
find . -type f -name "package-lock.json" -delete
```
### Testing (পরীক্ষা)
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Raising Pull Request (পুল অনুরোধ উত্থাপন)
- অনুগ্রহ করে PR এর আকার ছোট রাখুন এবং একটি বিষয়ে ফোকাস করুন।
- অনুগ্রহ করে শাখা তৈরির বিন্যাস অনুসরণ করুন।
- বৈশিষ্ট্য/[ফিচারের নাম]: এই শাখায় একটি নির্দিষ্ট বৈশিষ্ট্যের জন্য পরিবর্তন থাকতে হবে।
- উদাহরণ: বৈশিষ্ট্য/ডার্ক-মোড।
- বাগফিক্স/[বাগ নাম]: এই শাখায় একটি নির্দিষ্ট বাগ-এর জন্য শুধুমাত্র বাগ ফিক্স থাকা উচিত।
- উদাহরণ বাগফিক্স/বাগ-1।

View File

@ -0,0 +1,91 @@
[English](/contributing.md) | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | **Deutsch** | [Français](/contributing_fr.md) | [বাংলা](docs/contributing/contributing_bn.md)
## Lass uns Bruno noch besser machen, gemeinsam !!
Ich freue mich, dass Du Bruno verbessern möchtest. Hier findest Du eine Anleitung, mit der Du Bruno auf Deinem Computer einrichten kannst.
### Technologie Stack
Bruno ist mit Next.js und React erstellt. Außerdem benötigen wir electron für die Desktop Version (die lokale Sammlungen unterstützt).
Bibliotheken die wir benutzen
- CSS - Tailwind
- Code Editoren - Codemirror
- State Management - Redux
- Icons - Tabler Icons
- Formulare - formik
- Schema Validierung - Yup
- Request Client - axios
- Dateisystem Watcher - chokidar
### Abhängigkeiten
Du benötigst [Node v18.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
### Lass uns coden
Eine Anleitung zum Ausführen einer lokalen Entwicklungsumgebung findest Du in [development.md](docs/development_de.md).
### Pull Request erstellen
- Bitte halte die PRs klein und begrenzt auf eine Sache
- Bitte halte Dich beim Erstellen eines Branches an das folgende Format
- feature/[feature name]: Dieser Branch soll Änderungen für ein bestimmtes Feature enthalten
- Beispiel: feature/dark-mode
- bugfix/[bug name]: Dieser Branch soll ausschließlich Bugfixes für einen bestimmten Bug enthalten
- Beispiel: bugfix/bug-1
## Entwicklung
Bruno wird als Desktop-Anwendung entwickelt. Um die App zu starten, musst Du zuerst die Next.js App in einem Terminal ausführen und anschließend in einem anderen Terminal die Electron-App.
### Abhängigkeiten
- NodeJS v18
### Lokales Entwickeln
```bash
# use nodejs 18 version
nvm use
# install deps
npm i --legacy-peer-deps
# build graphql docs
npm run build:graphql-docs
# build bruno query
npm run build:bruno-query
# run next app (terminal 1)
npm run dev:web
# run electron app (terminal 2)
npm run dev:electron
```
### Troubleshooting
Es kann sein, dass Du einen `Unsupported platform`-Fehler bekommst, wenn Du `npm install` ausführst. Um dies zu beheben, musst Du `node_modules` und `package-lock.json` löschen und `npm install` erneut ausführen. Dies sollte alle notwendigen Pakete installieren, die zum Ausführen der Anwendung benötigt werden.
```shell
# Delete node_modules in sub-directories
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# Delete package-lock in sub-directories
find . -type f -name "package-lock.json" -delete
```
### Testen
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```

View File

@ -0,0 +1,85 @@
## ¡Juntos, hagamos a Bruno mejor!
Estamos encantados de que quieras ayudar a mejorar Bruno. A continuación encontrarás las instrucciones para empezar a trabajar con Bruno en tu computadora.
### Tecnologías utilizadas
Bruno está construido con NextJs y React. También usamos electron para distribuir una versión de escritorio (que soporta colecciones locales).
Librerías que utilizamos:
- CSS - Tailwind
- Editores de código - Codemirror
- Manejo del estado - Redux
- Íconos - Tabler Icons
- Formularios - formik
- Validación de esquemas - Yup
- Cliente de peticiones - axios
- Monitor del sistema de archivos - chokidar
### Dependencias
Necesitarás [Node v18.x o la última versión LTS](https://nodejs.org/es) y npm 8.x. Ten en cuenta que utilizamos espacios de trabajo de npm en el proyecto.
## Desarrollo
Bruno está siendo desarrollado como una aplicación de escritorio. Para ejecutarlo, primero debes ejecutar la aplicación de nextjs en una terminal y luego ejecutar la aplicación de electron en otra terminal.
### Dependencias
- NodeJS v18
### Desarrollo local
```bash
# Utiliza la versión 18 de nodejs
nvm use
# Instala las dependencias
npm i --legacy-peer-deps
# Construye la documentación de graphql
npm run build:graphql-docs
# Construye bruno-query
npm run build:bruno-query
# Ejecuta la aplicación de nextjs (terminal 1)
npm run dev:web
# Ejecuta la aplicación de electron (terminal 2)
npm run dev:electron
```
### Solución de problemas
Es posible que encuentres un error de `Unsupported platform` cuando ejecutes `npm install`. Para solucionarlo, debes eliminar la carpeta `node_modules` y el archivo `package-lock.json`, luego, ejecuta `npm install`. Lo anterior debería instalar todos los paquetes necesarios para ejecutar la aplicación.
```shell
# Elimina la carpeta node_modules en los subdirectorios
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# Elimina el archivo package-lock en los subdirectorios
find . -type f -name "package-lock.json" -delete
```
### Pruebas
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Crea un Pull Request
- Por favor, mantén los Pull Request pequeños y enfocados en una sola cosa.
- Por favor, sigue el siguiente formato para la creación de ramas:
- feature/[nombre de la funcionalidad]: Esta rama debe contener los cambios para una funcionalidad específica.
- Ejemplo: feature/dark-mode
- bugfix/[nombre del error]: Esta rama debe contener solo correcciones de errores para un error específico.
- Ejemplo: bugfix/bug-1

View File

@ -0,0 +1,91 @@
[English](/contributing.md) | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md) | **Français** | [বাংলা](docs/contributing/contributing_bn.md)
## Ensemble, améliorons Bruno !
Je suis content de voir que vous envisagez améliorer Bruno. Ci-dessous, vous trouverez les règles et guides pour récupérer Bruno sur votre ordinateur.
### Technologies utilisées
Bruno est construit en utilisant NextJs et React. Nous utilisons aussi Electron pour embarquer la version ordinateur (qui permet les collections locales).
Les bibliothèques que nous utilisons :
- CSS - Tailwind
- Code Editors - Codemirror
- State Management - Redux
- Icons - Tabler Icons
- Forms - formik
- Schema Validation - Yup
- Request Client - axios
- Filesystem Watcher - chokidar
### Dépendances
Vous aurez besoin de [Node v18.x ou la dernière version LTS](https://nodejs.org/en/) et npm 8.x. Nous utilisons aussi les espaces de travail npm (_npm workspaces_) dans ce projet.
### Commençons à coder
Veuillez vous référez à la [documentation de développement](docs/development_fr.md) pour les instructions de démarrage de l'environnement de développement local.
### Ouvrir une Pull Request
- Merci de conserver les PR petites et focalisées sur un seul objectif
- Merci de suivre le format de nom des branches
- feature/[feature name]: Cette branche devrait contenir une fonctionnalité spécifique
- Exemple: feature/dark-mode
- bugfix/[bug name]: Cette branche devrait contenir seulement une solution pour pour une bogue spécifique
- Exemple: bugfix/bug-1
## Développement
Bruno est développé comme une application de _lourde_. Vous devez charger l'application en démarrant nextjs dans un terminal, puis démarre l'application Electron dans un autre terminal.
### Dépendances
- NodeJS v18
### Développement local
```bash
# use nodejs 18 version
nvm use
# install deps
npm i --legacy-peer-deps
# build graphql docs
npm run build:graphql-docs
# build bruno query
npm run build:bruno-query
# run next app (terminal 1)
npm run dev:web
# run electron app (terminal 2)
npm run dev:electron
```
### Dépannage
Vous pourriez rencontrer une error `Unsupported platform` pendant le lancement de `npm install`. Pour résoudre cela, veuillez supprimer le répertoire `node_modules`, le fichier `package-lock.json` et lancer à nouveau `npm install`. Cela devrait isntaller tous les paquets nécessaires pour lancer l'application.
```shell
# Delete node_modules in sub-directories
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# Delete package-lock in sub-directories
find . -type f -name "package-lock.json" -delete
```
### Tests
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```

View File

@ -0,0 +1,89 @@
## Insieme, miglioriamo Bruno!
Sono felice di vedere che hai intenzione di migliorare Bruno. Di seguito, troverai le regole e le guide per ripristinare Bruno sul tuo computer.
### Tecnologie utilizzate
Bruno è costruito utilizzando Next.js e React. Utilizziamo anche Electron per incorporare la versione desktop (che consente raccolte locali).
Le librerie che utilizziamo sono:
- CSS - Tailwind
- Code Editors - Codemirror
- State Management - Redux
- Icons - Tabler Icons
- Forms - formik
- Schema Validation - Yup
- Request Client - axios
- Filesystem Watcher - chokidar
### Dependences
Hai bisogno di [Node v18.x o dell'ultima versione LTS](https://nodejs.org/en/) di npm 8.x. Utilizziamo gli spazi di lavoro npm (_npm workspaces_) in questo progetto.
### Iniziamo a codificare
Si prega di fare riferimento alla [documentazione di sviluppo](docs/development_it.md) per le istruzioni su come avviare l'ambiente di sviluppo locale.
### Aprire una richiesta di pull (Pull Request)
- Si prega di mantenere le Pull Request (PR) brevi e concentrate su un singolo obiettivo.
- Si prega di seguire il formato di denominazione dei rami.
- feature/[feature name]: Questo ramo dovrebbe contenere una specifica funzionalità.
- Esempio: feature/dark-mode
- bugfix/[bug name]: Questo ramo dovrebbe contenere solo una soluzione per un bug specifico.
- Esempio: bugfix/bug-1
## Sviluppo
Bruno è sviluppato come un'applicazione "heavy". È necessario caricare l'applicazione avviando Next.js in una finestra del terminale e quindi avviare l'applicazione Electron in un altro terminale.
### Sviluppo
- NodeJS v18
### Sviluppo locale
```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
```
### Risoluzione dei problemi
Potresti trovare un errore `Unsupported platform` durante l'esecuzione di `npm install`. Per risolvere questo problema, ti preghiamo di eliminare la cartella `node_modules`, il file `package-lock.json` e di seguito nuovamente `npm install`. Qeusto dovrebbe installare tutti i pacchetti necessari per avviare l'applicazione.
```shell
# delete node_modules in sub-directories
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
rm -rf "$dir"
done
# delete package-lock in sub-directories
find . -type f -name "package-lock.json" -delete
```
### Tests
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```

View File

@ -0,0 +1,85 @@
## Vamos tornar o Bruno melhor, juntos!!
Estamos felizes que você queira ajudar a melhorar o Bruno. Abaixo estão as diretrizes e orientações para começar a executar o Bruno no seu computador.
### Stack de Tecnologias
O Bruno é construído usando Next.js e React. Também usamos o Electron para disponibilizar uma versão para desktop (que suporta coleções locais).
Bibliotecas que utilizamos:
- CSS - Tailwind
- Editor de Código - Codemirror
- Gerenciador de Estado - Redux
- Ícones - Tabler Icons
- Formulários - formik
- Validador de Schema - Yup
- Cliente de Requisições - axios
- Monitor de Arquivos - chokidar
### Dependências
Você precisará do [Node v18.x (ou da versão LTS mais recente)](https://nodejs.org/en/) e do npm na versão 8.x. Nós utilizamos npm workspaces no projeto.
## Desenvolvimento
Bruno está sendo desenvolvido como um aplicativo de desktop. Você precisa carregar o programa executando o aplicativo Next.js em um terminal e, em seguida, executar o aplicativo Electron em outro terminal.
### Dependências
- NodeJS v18
### Desenvolvimento Local
```bash
# use nodejs 18 version
nvm use
# install deps
npm i --legacy-peer-deps
# build graphql docs
npm run build:graphql-docs
# build bruno query
npm run build:bruno-query
# run next app (terminal 1)
npm run dev:web
# run electron app (terminal 2)
npm run dev:electron
```
### Troubleshooting
Você pode se deparar com o erro `Unsupported platform` ao executar o comando `npm install`. Para corrigir isso, você precisará excluir a pasta `node_modules` e o arquivo `package-lock.json` e, em seguida, executar o comando `npm install` novamente. Isso deve instalar todos os pacotes necessários para executar o aplicativo.
```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
```
### Testando
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```
### Envio de Pull Request
- Por favor, mantenha os PRs pequenos e focados em uma única coisa.
- Siga o formato de criação de branches.
- feature/[nome da funcionalidade]: Esta branch deve conter alterações para uma funcionalidade específica.
- Exemplo: feature/dark-mode
- bugfix/[nome do bug]: Esta branch deve conter apenas correções para um bug específico.
- Exemplo: bugfix/bug-1

View File

@ -0,0 +1,91 @@
[English](/contributing.md) | [Українська](/contributing_ua.md) | **Русский** | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md) | [Français](/contributing_fr.md) | [বাংলা](docs/contributing/contributing_bn.md)
## Давайте вместе сделаем Бруно лучше!!!
Я рад, что Вы хотите усовершенствовать bruno. Ниже приведены рекомендации по запуску bruno на вашем компьютере.
### Стек
Bruno построен с использованием Next.js и React. Мы также используем electron для поставки десктопной версии ( которая поддерживает локальные коллекции )
Библиотеки, которые мы используем
- CSS - Tailwind
- Редакторы кода - Codemirror
- Управление состоянием - Redux
- Иконки - Tabler Icons
- Формы - formik
- Валидация схем - Yup
- Запросы клиента - axios
- Наблюдатель за файловой системой - chokidar
### Зависимости
Вам потребуется [Node v18.x или последняя версия LTS](https://nodejs.org/en/) и npm 8.x. В проекте мы используем рабочие пространства npm
### Приступим к коду
Пожалуйста, обратитесь к [development_ru.md](docs/development_ru.md) для получения инструкций по запуску локальной среды разработки.
### Создание Pull Request
- Пожалуйста, пусть PR будет небольшим и сфокусированным на одной вещи
- Пожалуйста, соблюдайте формат создания веток
- feature/[название функции]: Эта ветка должна содержать изменения для конкретной функции
- Пример: feature/dark-mode
- bugfix/[название ошибки]: Эта ветка должна содержать только исправления для конкретной ошибки
- Пример bugfix/bug-1
## Разработка
Bruno разрабатывается как десктопное приложение. Необходимо загрузить приложение, запустив приложение Next.js в одном терминале, а затем запустить приложение 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
# запустить приложение electron ( терминал 2 )
npm run dev:electron
```
### Устранение неисправностей
При запуске `npm install` может возникнуть ошибка `Unsupported platform`. Чтобы исправить это, необходимо удалить `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
```

View File

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

View File

@ -0,0 +1,91 @@
[English](/contributing.md) | **Українська** | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md) | [Français](/contributing_fr.md) | [বাংলা](docs/contributing/contributing_bn.md)
## Давайте зробимо Bruno краще, разом !!
Я дуже радий що Ви бажаєте покращити Bruno. Нижче наведені вказівки як розпочати розробку Bruno на Вашому комп'ютері.
### Стек технологій
Bruno побудований на Next.js та 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
## Розробка
Bruno розробляється як декстопний застосунок. Вам потрібно запустити Next.js в одній сесії терміналу, та запустити застосунок 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
```

View File

@ -1,55 +0,0 @@
**English** | [Українська](/docs/development_ua.md) | [Русский](/docs/development_ru.md)
## Development
Bruno is being developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
### Dependencies
- NodeJS v18
### Local Development
```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
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
```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
```
### Testing
```bash
# bruno-schema
npm test --workspace=packages/bruno-schema
# bruno-lang
npm test --workspace=packages/bruno-lang
```

View File

@ -1,55 +0,0 @@
[English](/docs/development.md) | [Українська](/docs/development_ua.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
# запустить приложение electron ( терминал 2 )
npm run dev:electron
```
### Устранение неисправностей
При запуске `npm install` может возникнуть ошибка `Unsupported platform`. Чтобы исправить это, необходимо удалить `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
```

View File

@ -1,55 +0,0 @@
[English](/docs/development.md) | **Українська** | [Русский](/docs/development_ru.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
```

View File

@ -0,0 +1,5 @@
### Publicando Bruno em um novo gerenciador de pacotes
Embora nosso código seja de código aberto e esteja disponível para todos usarem, pedimos gentilmente que entre em contato conosco antes de considerar a publicação em novos gerenciadores de pacotes. Como o criador da ferramenta, mantenho a marca registrada `Bruno` para este projeto e gostaria de gerenciar sua distribuição. Se deseja ver o Bruno em um novo gerenciador de pacotes, por favor, solicite através de uma issue no GitHub.
Embora a maioria de nossas funcionalidades seja gratuita e de código aberto (o que abrange API's REST e GraphQL), buscamos alcançar um equilíbrio harmonioso entre os princípios de código aberto e sustentabilidade. - https://github.com/usebruno/bruno/discussions/269

123
docs/readme/readme_bn.md Normal file
View File

@ -0,0 +1,123 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### ব্রুনো - API অন্বেষণ এবং পরীক্ষা করার জন্য ওপেনসোর্স IDE।
[![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) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | **বাংলা**
ব্রুনো হল একটি নতুন এবং উদ্ভাবনী API ক্লায়েন্ট, যার লক্ষ্য পোস্টম্যান এবং অনুরূপ সরঞ্জাম দ্বারা প্রতিনিধিত্ব করা স্থিতাবস্থায় বিপ্লব ঘটানো।
ব্রুনো আপনার সংগ্রহগুলি সরাসরি আপনার ফাইল সিস্টেমের একটি ফোল্ডারে সঞ্চয় করে। আমরা API অনুরোধ সম্পর্কে তথ্য সংরক্ষণ করতে একটি প্লেইন টেক্সট মার্কআপ ভাষা, ব্রু ব্যবহার করি।
আপনি আপনার API সংগ্রহে সহযোগিতা করতে গিট বা আপনার পছন্দের যেকোনো সংস্করণ নিয়ন্ত্রণ ব্যবহার করতে পারেন।
ব্রুনো শুধুমাত্র অফলাইন। ব্রুনোতে ক্লাউড-সিঙ্ক যোগ করার কোন পরিকল্পনা নেই, কখনও। আমরা আপনার ডেটা গোপনীয়তার মূল্য দিই এবং বিশ্বাস করি এটি আপনার ডিভাইসে থাকা উচিত। আমাদের দীর্ঘমেয়াদী দৃষ্টি পড়ুন। [এখানে ](https://github.com/usebruno/bruno/discussions/269)
📢 ইন্ডিয়া FOSS 3.0 সম্মেলনে আমাদের সাম্প্রতিক আলোচনা দেখুন [এখানে](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](/assets/images/landing-2.png) <br /><br />
### স্থাপন
ব্রুনো বাইনারি ডাউনলোড হিসাবে উপলব্ধ [আমাদের ওয়েবসাইটে](https://www.usebruno.com/downloads) ম্যাক, উইন্ডোজ এবং লিনাক্সের জন্য।
আপনি Homebrew, Chocolatey, Snap এবং Apt এর মত প্যাকেজ ম্যানেজারদের মাধ্যমে ব্রুনো ইনস্টল করতে পারেন।
```sh
# Homebrew এর মাধ্যমে Mac-এ
brew install bruno
# চকোলেটির মাধ্যমে উইন্ডোজে
choco install bruno
# স্ন্যাপ এর মাধ্যমে লিনাক্সে
snap install bruno
# Apt এর মাধ্যমে লিনাক্সে
sudo mkdir -p /etc/apt/keyrings
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
sudo apt update
sudo apt install bruno
```
### একাধিক প্ল্যাটফর্মে চালান 🖥️
![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/pricing)
- [ডাউনলোড করুন](https://www.usebruno.com/downloads)
### শোকেস 🎥
- [প্রশংসাপত্র](https://github.com/usebruno/bruno/discussions/343)
- [নলেজ হাব](https://github.com/usebruno/bruno/discussions/386)
- [স্ক্রিপ্টম্যানিয়া](https://github.com/usebruno/bruno/discussions/385)
### সমর্থন ❤️
উফ ! আপনি যদি প্রকল্পটি পছন্দ করেন তবে ⭐ বোতামটি টিপুন !!
### প্রশংসাপত্র শেয়ার করুন 📣
যদি ব্রুনো আপনাকে কর্মক্ষেত্রে এবং আপনার দলগুলিতে সাহায্য করে থাকে, অনুগ্রহ করে আপনার [আমাদের গিটহাব আলোচনায় প্রশংসাপত্রগুলি](https://github.com/usebruno/bruno/discussions/343) শেয়ার করতে ভুলবেন না
### নতুন প্যাকেজ পরিচালকদের কাছে প্রকাশ করা হচ্ছে
আরও তথ্যের জন্য অনুগ্রহ করে [এখানে](publishing.md) দেখুন।
### অবদান 👩‍💻🧑‍💻
আমি খুশি যে আপনি ব্রুনোর উন্নতি করতে চাইছেন। অনুগ্রহ করে [অবদানকারী নির্দেশিকা](contributing.md) দেখুন
আপনি কোডের মাধ্যমে অবদান রাখতে না পারলেও, অনুগ্রহ করে বাগ এবং বৈশিষ্ট্যের অনুরোধ ফাইল করতে দ্বিধা করবেন না যা আপনার ব্যবহারের ক্ষেত্রে সমাধান করার জন্য প্রয়োগ করা প্রয়োজন।
### লেখক
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### সাথে থাকুন 🌐
[𝕏 (টুইটার)](https://twitter.com/use_bruno) <br />
[ওয়েবসাইট](https://www.usebruno.com) <br />
[ডিসকর্ড](https://discord.com/invite/KgcZUncpjq) <br />
[লিঙ্কডইন](https://www.linkedin.com/company/usebruno)
### ট্রেডমার্ক
**নাম**
`Bruno` হল একটি ট্রেডমার্ক [Anoop M D](https://www.helloanoop.com/)
**লোগো**
লোগোটি [OpenMoji](https://openmoji.org/library/emoji-1F436/) থেকে নেওয়া হয়েছে। লাইসেন্স: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### লাইসেন্স 📄
[MIT](license.md)

95
docs/readme/readme_de.md Normal file
View File

@ -0,0 +1,95 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### Bruno - Opensource IDE zum Erkunden und Testen von APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | [Українська](/readme_ua.md) | [Русский](/readme_ru.md) | [Türkçe](/readme_tr.md) | **Deutsch** | [Français](/readme_fr.md) | [বাংলা](docs/readme/readme_bn.md)
Bruno ist ein neuer und innovativer API-Client, der den Status Quo von Postman und ähnlichen Tools revolutionieren soll.
Bruno speichert Deine Sammlungen direkt in einem Ordner in Deinem Dateisystem. Wir verwenden eine einfache Textauszeichnungssprache - Bru - um Informationen über API-Anfragen zu speichern.
Du kannst Git oder eine andere Versionskontrolle deiner Wahl verwenden, um an deinen API-Sammlungen gemeinsam mit anderen zu arbeiten.
Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno eine Cloud-Synchronisation hinzuzufügen. Wir schätzen den Schutz Deiner Daten und glauben, dass sie auf Deinem Gerät bleiben sollten. Lies unsere Langzeit-Vision [hier](https://github.com/usebruno/bruno/discussions/269).
![bruno](/assets/images/landing-2.png) <br /><br />
### Einsatz auf verschiedensten Plattformen 🖥️
![bruno](/assets/images/run-anywhere.png) <br /><br />
### Zusammenarbeiten mit Git 👩‍💻🧑‍💻
oder eine Versionskontrolle Deiner Wahl
![bruno](/assets/images/version-control.png) <br /><br />
### Wichtige Links 📌
- [Unsere Langzeit-Vision](https://github.com/usebruno/bruno/discussions/269)
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
- [Dokumentation](https://docs.usebruno.com)
- [Webseite](https://www.usebruno.com)
- [Preise](https://www.usebruno.com/pricing)
- [Download](https://www.usebruno.com/downloads)
### Showcase 🎥
- [Erfahrungsberichte](https://github.com/usebruno/bruno/discussions/343)
- [Wissenswertes](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
### Unterstützung ❤️
Wuff! Wenn Du dieses Projekt magst, klick den ⭐ Button !!
### Teile Erfahrungsberichte 📣
Wenn Bruno Dir bei Deiner Arbeit und in Deinen Teams geholfen hat, vergiss bitte nicht, Deine [Erfahrungsberichte auf unserer GitHub-Diskussion](https://github.com/usebruno/bruno/discussions/343) zu teilen.
### Veröffentlichung in neuen Paketmanagern
Bitte [hier](/publishing.md) für mehr Informationen lesen.
### Mitmachen 👩‍💻🧑‍💻
Ich freue mich, dass Du Bruno verbessern willst. Bitte schau Dir den [Leitfaden zum Mitmachen](../contributing/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)

93
docs/readme/readme_es.md Normal file
View File

@ -0,0 +1,93 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### Bruno - IDE de código abierto para explorar y probar APIs.
[![Versión en Github](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)
[![Actividad de Commits](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)
[![Sitio Web](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Descargas](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
Bruno un cliente de APIs nuevo e innovador, creado con el objetivo de revolucionar el panorama actual representado por Postman y otras herramientas similares.
Bruno almacena tus colecciones directamente en una carpeta de tu sistema de archivos. Usamos un lenguaje de marcado de texto plano, llamado Bru, para guardar información sobre las peticiones a tus APIs.
Puedes usar git o cualquier otro sistema de control de versiones que prefieras para colaborar en tus colecciones.
Bruno funciona sin conexión a internet. No tenemos intenciones de añadir sincronización en la nube a Bruno, en ningún momento. Valoramos tu privacidad y creemos que tus datos deben permanecer en tu dispositivo. Puedes leer nuestra visión a largo plazo [aquí](https://github.com/usebruno/bruno/discussions/269)
![bruno](/assets/images/landing-2.png) <br /><br />
### Ejecútalo en múltiples plataformas 🖥️
![bruno](/assets/images/run-anywhere.png) <br /><br />
### Colabora vía Git 👩‍💻🧑‍💻
O cualquier otro sistema de control de versiones que prefieras
![bruno](/assets/images/version-control.png) <br /><br />
### Enlaces importantes 📌
- [Nuestra Visión a Largo Plazo](https://github.com/usebruno/bruno/discussions/269)
- [Hoja de Ruta](https://github.com/usebruno/bruno/discussions/384)
- [Documentación](https://docs.usebruno.com)
- [Sitio Web](https://www.usebruno.com)
- [Precios](https://www.usebruno.com/pricing)
- [Descargas](https://www.usebruno.com/downloads)
### Casos de uso 🎥
- [Testimonios](https://github.com/usebruno/bruno/discussions/343)
- [Centro de Conocimiento](https://github.com/usebruno/bruno/discussions/386)
- [Scripts de la Comunidad](https://github.com/usebruno/bruno/discussions/385)
### Apoya el proyecto ❤️
¡Guau! Si te gusta el proyecto, ¡dale al botón de ⭐!
### Comparte tus testimonios 📣
Si Bruno te ha ayudado en tu trabajo y con tus equipos, por favor, no olvides compartir tus testimonios en [nuestras discusiones de GitHub](https://github.com/usebruno/bruno/discussions/343)
### Publicar en nuevos gestores de paquetes
Por favor, consulta [aquí](publishing.md) para más información.
### Contribuye 👩‍💻🧑‍💻
Estamos encantados de que quieras ayudar a mejorar Bruno. Por favor, consulta la [guía de contribución](contributing_es.md) para más información.
Incluso si no puedes contribuir con código, no dudes en reportar errores y solicitar nuevas funcionalidades que necesites para resolver tu caso de uso.
### Colaboradores
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### Mantente en contacto 🌐
[X](https://twitter.com/use_bruno) <br />
[Sitio Web](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno)
### Marca
**Nombre**
`Bruno` es una marca propiedad de [Anoop M D](https://www.helloanoop.com/).
**Logo**
El logo fue obtenido de [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licencia: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### Licencia 📄
[MIT](license.md)

97
docs/readme/readme_fr.md Normal file
View File

@ -0,0 +1,97 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### Bruno - IDE Opensource pour explorer et tester des APIs.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | [Українська](/readme_ua.md) | [Русский](/readme_ru.md) | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md) | **Français** | [বাংলা](docs/readme/readme_bn.md)
Bruno est un nouveau client API, innovant, qui a pour but de révolutionner le _status quo_ que représente Postman et les autres outils.
Bruno sauvegarde vos collections directement sur votre système de fichiers. Nous utilisons un langage de balise de type texte pour décrire les requêtes API.
Vous pouvez utiliser git ou tout autre gestionnaire de version pour travailler de manière collaborative sur vos collections d'APIs.
Bruno ne fonctionne qu'en mode déconnecté. Il n'y a pas de d'abonnement ou de synchronisation avec le cloud Bruno, il n'y en aura jamais. Nous sommes conscients de la confidentialité de vos données et nous sommes convaincus qu'elles doivent rester sur vos appareils. Vous pouvez lire notre vision à long terme [ici (en anglais)](https://github.com/usebruno/bruno/discussions/269).
![bruno](/assets/images/landing-2.png) <br /><br />
### Fonctionne sur de multiples platformes 🖥️
![bruno](/assets/images/run-anywhere.png) <br /><br />
### Collaborer via Git 👩‍💻🧑‍💻
Ou n'importe quel système de gestion de sources
![bruno](/assets/images/version-control.png) <br /><br />
### Liens importants 📌
- [Notre vision à long terme (en anglais)](https://github.com/usebruno/bruno/discussions/269)
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
- [Documentation](https://docs.usebruno.com)
- [Site web](https://www.usebruno.com)
- [Prix](https://www.usebruno.com/pricing)
- [Téléchargement](https://www.usebruno.com/downloads)
### Showcase 🎥
- [Témoignages](https://github.com/usebruno/bruno/discussions/343)
- [Centre de connaissance](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
### Soutien ❤️
Ouaf! Si vous aimez le projet, cliquez sur le bouton ⭐ !!
### Partage de témoignages 📣
Si Bruno vous a aidé dans votre travail, au sein de votre équipe, merci de penser à partager votre témoignage sur la [page discussion Github dédiée](https://github.com/usebruno/bruno/discussions/343)
### Publier Bruno sur un nouveau gestionnaire de paquets
Veuillez regarder [ici](/publishing.md) pour plus d'information.
### Contribuer 👩‍💻🧑‍💻
Je suis heureux de voir que vous cherchez à améliorer Bruno. Merci de consulter le [guide de contribution](../contributing/contributing_fr.md)
Même si vous n'êtes pas en mesure de contribuer directement via du code, n'hésitez pas à consigner les bogues et les demandes de nouvelles fonctionnalités pour résoudre vos cas d'usage !
### Auteurs
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### Restons en contact 🌐
[Twitter](https://twitter.com/use_bruno) <br />
[Website](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno)
### Marque
**Nom**
`Bruno` est une marque appartenant à [Anoop M D](https://www.helloanoop.com/)
**Logo**
Le logo est issu de [OpenMoji](https://openmoji.org/library/emoji-1F436/).
Licence: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### Licence 📄
[MIT](/license.md)

121
docs/readme/readme_it.md Normal file
View File

@ -0,0 +1,121 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### Bruno - Opensource IDE per esplorare e testare gli 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)
Bruno è un nuovo ed innovativo API client, mirato a rivoluzionare lo status quo rappresentato da Postman e strumenti simili disponibili.
Bruno memorizza le tue raccolte direttamente in una cartella del tuo filesystem. Utilizziamo un linguaggio di markup in testo semplice chiamato Bru per salvare informazioni sulle richeste API.
Puoi utilizzare Git o qualsiasi sistema di controllo che preferisci per collaborare sulle tue raccolte di API.
Bruno funziona solo in modalità offline. Non ci sono piani per aggiungere la sincronizzazione su cloud a Bruno in futuro. Valorizziamo la privacy dei tuoi dati e crediamo che dovrebbero rimanere sul tuo dispositivo. Puoi leggere la nostra visione a lungo termine [qui (in inglese)](https://github.com/usebruno/bruno/discussions/269)
📢 Guarda la nostra presentazione più recente alla conferenza India FOSS 3.0 [qui](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](/assets/images/landing-2.png) <br /><br />
### Installazione
Bruno è disponisible come download binario [sul nostro sito](https://www.usebruno.com/downloads) per Mac, Windows e Linux.
Puoi installare Bruno anche tramite package manger come Homebrew, Chocolatey, Snap e Apt.
```sh
# Su Mac come Homebrew
brew install bruno
# Su Windows come Chocolatey
choco install bruno
# Su Linux tramite Snap
snap install bruno
# Su Linux tramite Apt
sudo mkdir -p /etc/apt/keyrings
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
sudo apt update
sudo apt install bruno
```
### Funziona su diverse piattaforme 🖥️
![bruno](/assets/images/run-anywhere.png) <br /><br />
### Collabora tramite Git 👩‍💻🧑‍💻
O con qualsiasi sistema di controllo di versioni a tua scelta
![bruno](/assets/images/version-control.png) <br /><br />
### Collegamenti importanti 📌
- [La nostra visione a lungo termine](https://github.com/usebruno/bruno/discussions/269)
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
- [Documentazione](https://docs.usebruno.com)
- [Sito internet](https://www.usebruno.com)
- [Prezzo](https://www.usebruno.com/pricing)
- [Download](https://www.usebruno.com/downloads)
### Showcase 🎥
- [Testimonianze](https://github.com/usebruno/bruno/discussions/343)
- [Centro di conoscenza](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
### Supporto ❤️
Woof! se ti piace il progetto, premi quel ⭐ pulsante !!
### Testimonianze condivise 📣
Se Bruno ti ha aiutato con il tuo lavoro ed il tuo team, per favore non dimenticare di condividere le tue [testimonianze nella nostra discussione su GitHub](https://github.com/usebruno/bruno/discussions/343)
### Pubblica Bruno su un nuovo gestore di pacchetti
Per favore vedi [qui](publishing.md) per accedere a più informazioni.
### Contribuire 👩‍💻🧑‍💻
Sono felice che vuoi migliorare Bruno. Per favore controlla la [guida per la partecipazione](contributing.md)
Anche se non sei in grado di contribuire tramite il codice, non esitare a segnalare bug e richieste di funzionalità che devono essere implementati per risolvere il tuo caso d'uso.
### Autori
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### Resta in contatto 🌐
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
[Sito internet](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno)
### Marchio
**Nome**
`Bruno` è un marchio registrato appartenente a [Anoop M D](https://www.helloanoop.com/)
**Logo**
Il logo è stato creato da [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licenza: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### Licenza 📄
[MIT](license.md)

121
docs/readme/readme_kr.md Normal file
View File

@ -0,0 +1,121 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### Bruno - API 탐색 및 테스트를 위한 오픈소스 IDE.
[![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)
Bruno는 새롭고 혁신적인 API 클라이언트로, Postman과 유사한 툴들을 혁신하는 것을 목표로 합니다.
Bruno는 사용자의 컬렉션을 파일 시스템의 폴더에 직접 저장합니다. 일반 텍스트 마크업 언어인 Bru를 사용해 API 요청에 대한 정보를 저장합니다.
Git 또는 원하는 버전 관리 도구를 사용하여 API 컬렉션을 연동할 수 있습니다.
브루는 오프라인 전용입니다. 브루노에 클라우드 동기화 기능을 추가할 계획은 없습니다. 저희는 사용자의 데이터 프라이버시를 소중히 여기며, 데이터는 사용자의 기기에 남아 있어야 한다고 믿습니다. 장기 비전 읽기 [링크](https://github.com/usebruno/bruno/discussions/269)
📢 Watch our recent talk at India FOSS 3.0 Conference [here](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](/assets/images/landing-2.png) <br /><br />
### 설치
Bruno 는 여기에서 다운로드 받을 수 있습니다.[링크](https://www.usebruno.com/downloads) (맥, 윈도우, 리눅스)
Homebrew, Chocolatey, Snap, Apt 같은 패키지 관리자를 통해서도 Bruno를 설치할 수 있습니다.
```sh
# On Mac via Homebrew
brew install bruno
# On Windows via Chocolatey
choco install bruno
# On Linux via Snap
snap install bruno
# On Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
sudo apt update
sudo apt install bruno
```
### 여러 플랫폼에서 실행하세요. 🖥️
![bruno](assets/images/run-anywhere.png) <br /><br />
### Git과 연동하세요. 👩‍💻🧑‍💻
또는 원하는 버전 관리 시스템을 선택하세요.
![bruno](assets/images/version-control.png) <br /><br />
### 중요 링크 📌
- [Our Long Term Vision](https://github.com/usebruno/bruno/discussions/269)
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
- [Documentation](https://docs.usebruno.com)
- [Website](https://www.usebruno.com)
- [Pricing](https://www.usebruno.com/pricing)
- [Download](https://www.usebruno.com/downloads)
### 쇼케이스 🎥
- [Testimonials](https://github.com/usebruno/bruno/discussions/343)
- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
### 지원 ❤️
프로젝트가 마음에 들면 ⭐ 버튼을 눌러 주세요.
### 후기 공유 📣
Bruno가 여러분과 여러분의 팀에 도움이 되었다면, 잊지 말고 공유해 주세요. [Github discussion 공유 링크](https://github.com/usebruno/bruno/discussions/343)
### 새 패키지 관리자에게 게시
더 많은 정보를 확인하시려명 링크를 클릭해 주세요.[배포 가이드](publishing.md)
### 컨트리뷰트 👩‍💻🧑‍💻
컨트리뷰트에 관심이 있으시면 링크를 참고해 주세요. [컨트리뷰트 가이드](contributing.md)
코드를 통해 기여할 수 없더라도 사용 사례를 해결하기 위해 구현이 필요한 버그나 기능 요청을 주저하지 마시고 제출해 주세요.
### Authors
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### Stay in touch 🌐
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
[Website](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno)
### 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 📄
[MIT](license.md)

121
docs/readme/readme_pt_br.md Normal file
View File

@ -0,0 +1,121 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### Bruno - IDE de código aberto para explorar e testar 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)
Bruno é um novo e inovador cliente de API, com o objetivo de revolucionar o status quo representado por ferramentas como o Postman e outras semelhantes.
Bruno armazena suas coleções diretamente em uma pasta no seu sistema de arquivos. Utilizamos uma linguagem de marcação de texto simples, chamada Bru, para salvar informações sobre requisições de API.
Você pode usar o Git ou qualquer sistema de controle de versão de sua escolha para colaborar em suas coleções de API.
Bruno é totalmente offline. Não há planos de adicionar sincronização em nuvem ao Bruno, nunca. Valorizamos a privacidade de seus dados e acreditamos que eles devem permanecer em seu dispositivo. Saiba mais sobre nossa visão a longo prazo [aqui](https://github.com/usebruno/bruno/discussions/269).
📢 Assista à nossa palestra recente na India FOSS 3.0 Conference [aqui](https://www.youtube.com/watch?v=7bSMFpbcPiY).
![bruno](../../assets/images/landing-2.png) <br /><br />
### Instalação
Bruno está disponível para download como binário [em nosso site](https://www.usebruno.com/downloads) para Mac, Windows e Linux.
Você também pode instalar o Bruno via gerenciadores de pacotes como Homebrew, Chocolatey, Snap e Apt.
```sh
# Mac via Homebrew
brew install bruno
# Windows via Chocolatey
choco install bruno
# Linux via Snap
snap install bruno
# Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
sudo apt update
sudo apt install bruno
```
### Execute em várias plataformas 🖥️
![bruno](../../assets/images/run-anywhere.png) <br /><br />
### Colaboração via Git 👩‍💻🧑‍💻
Ou qualquer sistema de controle de versão de sua escolha.
![bruno](../../assets/images/version-control.png) <br /><br />
### Links Importantes 📌
- [Nossa Visão de Longo Prazo](https://github.com/usebruno/bruno/discussions/269)
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
- [Documentação](https://docs.usebruno.com)
- [Website](https://www.usebruno.com)
- [Preços](https://www.usebruno.com/pricing)
- [Download](https://www.usebruno.com/downloads)
### Showcase 🎥
- [Depoimentos](https://github.com/usebruno/bruno/discussions/343)
- [Hub de Conhecimento](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
### Apoie ❤️
Au-au! Se você gosta do projeto, clique no botão ⭐!!
### Compartilhe sua experiência 📣
Se o Bruno ajudou no seu trabalho e/ou no trabalho de sua equipe, por favor, não se esqueça de compartilhar seu [depoimento em nossas discussões no GitHub](https://github.com/usebruno/bruno/discussions/343).
### Publicando em Novos Gerenciadores de Pacotes
Por favor, verifique [aqui](../publishing/publishing_pt_br.md) mais informações.
### Colabore 👩‍💻🧑‍💻
Fico feliz que você queira melhorar o Bruno. Por favor, confira o [guia de colaboração](../contributing/contributing_pt_br.md).
Mesmo que você não possa contribuir codificando, não deixe de relatar problemas e solicitar recursos que precisam ser implementados para atender ao contexto de seu dia a dia.
### Authors
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### Mantenha Contato 🌐
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
[Website](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno)
### Trademark
**Nome**
`Bruno` é uma marca registrada de [Anoop M D](https://www.helloanoop.com/).
**Logo**
A logo é original do [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licença: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).
### Licença 📄
[MIT](license.md)

View File

@ -1,5 +1,5 @@
<br />
<img src="assets/images/logo-transparent.png" width="80"/>
<img src="../../assets/images/logo-transparent.png" width="80"/>
### Bruno - IDE с открытым исходным кодом для изучения и тестирования API.
@ -10,7 +10,8 @@
[![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) | **Русский**
[English](/readme.md) | [Українська](/readme_ua.md) | **Русский** | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md) | [Français](/readme_fr.md) | [বাংলা](docs/readme/readme_bn.md)
Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами.
@ -20,17 +21,17 @@ Bruno хранит ваши коллекции непосредственно в
Bruno работает только в автономном режиме. Добавление облачной синхронизации в Bruno не планируется. Мы ценим конфиденциальность ваших данных и считаем, что они должны оставаться на вашем устройстве. Ознакомьтесь с нашим долгосрочным видением [здесь](https://github.com/usebruno/bruno/discussions/269)
![bruno](assets/images/landing-2.png) <br /><br />
![bruno](/assets/images/landing-2.png) <br /><br />
### Работа на нескольких платформах 🖥️
![bruno](assets/images/run-anywhere.png) <br /><br />
![bruno](/assets/images/run-anywhere.png) <br /><br />
### Совместная работа через Git 👩‍💻🧑‍💻
Или другая система контроля версий по вашему выбору
![bruno](assets/images/version-control.png) <br /><br />
![bruno](/assets/images/version-control.png) <br /><br />
### Важные ссылки 📌
@ -56,7 +57,7 @@ Bruno работает только в автономном режиме. Доб
### Внести вклад 👩‍💻🧑‍💻
Я рад, что Вы хотите улучшить Бруно. Пожалуйста, ознакомьтесь с [этим гайдом](contributing_ru.md)
Я рад, что Вы хотите улучшить Бруно. Пожалуйста, ознакомьтесь с [этим гайдом](../contributing/contributing_ru.md)
Даже если вы не можете внести свой вклад с помощью кода, пожалуйста, не стесняйтесь сообщать об ошибках и пожеланиях к функциям, которые необходимо реализовать для решения вашей задачи.
@ -76,4 +77,4 @@ Bruno работает только в автономном режиме. Доб
### Лицензия 📄
[MIT](license.md)
[MIT](/license.md)

80
docs/readme/readme_tr.md Normal file
View File

@ -0,0 +1,80 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### Bruno - API'leri keşfetmek ve test etmek için açık kaynaklı IDE.
[![GitHub sürümü](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Web Sitesi](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![İndir](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | [Українська](/readme_ua.md) | [Русский](/readme_ru.md) | **Türkçe** | [Deutsch](/readme_de.md) | [Français](/readme_fr.md) | [বাংলা](docs/readme/readme_bn.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/contributing.md)'na göz atın
Kod yoluyla katkıda bulunamasanız bile, lütfen kullanım durumunuzu çözmek için uygulanması gereken hataları ve özellik isteklerini bildirmekten çekinmeyin.
### Katkıda Bulunanlar
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### İletişimde Kalın 🌐
[Twitter](https://twitter.com/use_bruno) <br />
[Website](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq)
[LinkedIn](https://www.linkedin.com/company/usebruno)
### Lisans 📄
[MIT](/license.md)

View File

@ -1,5 +1,5 @@
<br />
<img src="assets/images/logo-transparent.png" width="80"/>
<img src="../../assets/images/logo-transparent.png" width="80"/>
### Bruno - IDE із відкритим кодом для тестування та дослідження API
@ -10,7 +10,7 @@
[![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)
[English](/readme.md) | **Українська** | [Русский](/readme_ru.md) | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md) | [Français](/readme_fr.md) | [বাংলা](docs/readme/readme_bn.md)
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman.
@ -20,17 +20,17 @@ Bruno зберігає ваші колекції напряму у теці на
Bruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Взнати більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269)
![bruno](assets/images/landing-2.png) <br /><br />
![bruno](/assets/images/landing-2.png) <br /><br />
### Кросплатформенність 🖥️
![bruno](assets/images/run-anywhere.png) <br /><br />
![bruno](/assets/images/run-anywhere.png) <br /><br />
### Спільна робота через Git 👩‍💻🧑‍💻
Або будь-яку іншу систему контролю версій на ваш вибір
![bruno](assets/images/version-control.png) <br /><br />
![bruno](/assets/images/version-control.png) <br /><br />
### Важливі посилання 📌
@ -56,7 +56,7 @@ Bruno є повністю автономним. Немає жодних план
### Зробити свій внесок 👩‍💻🧑‍💻
Я радий що ви бажаєте покращити Bruno. Будь ласка переглянте [інструкцію по контрибуції](contributing_ua.md)
Я радий що ви бажаєте покращити Bruno. Будь ласка переглянте [інструкцію по контрибуції](../contributing/contributing_ua.md)
Навіть якщо ви не можете зробити свій внесок пишучи програмний код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі.
@ -72,9 +72,9 @@ Bruno є повністю автономним. Немає жодних план
[Twitter](https://twitter.com/use_bruno) <br />
[Сайт](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)
### Ліцензія 📄
[MIT](license.md)
[MIT](/license.md)

2425
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
"version": "0.3.0",
"private": true,
"scripts": {
"dev": "cross-env ENV=dev next dev",
"dev": "cross-env ENV=dev next dev -p 3000",
"build": "next build && next export",
"start": "next start",
"lint": "next lint",
@ -19,12 +19,14 @@
"@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6",
"@usebruno/graphql-docs": "0.1.0",
"@usebruno/schema": "0.5.0",
"axios": "^0.26.0",
"@usebruno/schema": "0.6.0",
"axios": "^1.5.1",
"classnames": "^2.3.1",
"codemirror": "^5.65.2",
"codemirror-graphql": "^1.2.5",
"cookie": "^0.6.0",
"escape-html": "^1.0.3",
"file": "^0.2.2",
"file-dialog": "^0.0.8",
"file-saver": "^2.0.5",
"formik": "^2.2.9",
@ -36,6 +38,9 @@
"httpsnippet": "^3.0.1",
"idb": "^7.0.0",
"immer": "^9.0.15",
"jsesc": "^3.0.2",
"jshint": "^2.13.6",
"jsonlint": "^1.6.3",
"know-your-http-well": "^0.5.0",
"lodash": "^4.17.21",
"markdown-it": "^13.0.2",
@ -43,9 +48,11 @@
"nanoid": "3.3.4",
"next": "12.3.3",
"path": "^0.12.7",
"pdfjs-dist": "^3.11.174",
"platform": "^1.3.6",
"posthog-node": "^2.1.0",
"qs": "^6.11.0",
"query-string": "^7.0.1",
"react": "18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
@ -53,12 +60,16 @@
"react-github-btn": "^1.4.0",
"react-hot-toast": "^2.4.0",
"react-inspector": "^6.0.2",
"react-pdf": "^7.5.1",
"react-redux": "^7.2.6",
"react-tooltip": "^5.5.2",
"sass": "^1.46.0",
"styled-components": "^5.3.3",
"system": "^2.0.1",
"tailwindcss": "^2.2.19",
"url": "^0.11.3",
"xml-formatter": "^3.5.0",
"yargs-parser": "^21.1.1",
"yup": "^0.32.11"
},
"devDependencies": {

View File

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

View File

@ -10,12 +10,85 @@ import isEqual from 'lodash/isEqual';
import { getEnvironmentVariables } from 'utils/collections';
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
import StyledWrapper from './StyledWrapper';
import jsonlint from 'jsonlint';
import { JSHINT } from 'jshint';
let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
window.jsonlint = jsonlint;
window.JSHINT = JSHINT;
//This should be done dynamically if possible
const hintWords = [
'res',
'res.status',
'res.statusText',
'res.headers',
'res.body',
'res.getStatus()',
'res.getHeader(name)',
'res.getHeaders()',
'res.getBody()',
'req',
'req.url',
'req.method',
'req.headers',
'req.body',
'req.timeout',
'req.getUrl()',
'req.setUrl(url)',
'req.getMethod()',
'req.setMethod(method)',
'req.getHeader(name)',
'req.getHeaders()',
'req.setHeader(name, value)',
'req.setHeaders(data)',
'req.getBody()',
'req.setBody(data)',
'req.setMaxRedirects(maxRedirects)',
'req.getTimeout()',
'req.setTimeout(timeout)',
'bru',
'bru.cwd()',
'bru.getEnvName(key)',
'bru.getProcessEnv(key)',
'bru.getEnvVar(key)',
'bru.setEnvVar(key,value)',
'bru.getVar(key)',
'bru.setVar(key,value)'
];
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
const cursor = editor.getCursor();
const currentLine = editor.getLine(cursor.line);
let startBru = cursor.ch;
let endBru = startBru;
while (endBru < currentLine.length && /[\w.]/.test(currentLine.charAt(endBru))) ++endBru;
while (startBru && /[\w.]/.test(currentLine.charAt(startBru - 1))) --startBru;
let curWordBru = startBru != endBru && currentLine.slice(startBru, endBru);
let start = cursor.ch;
let end = start;
while (end < currentLine.length && /[\w]/.test(currentLine.charAt(end))) ++end;
while (start && /[\w]/.test(currentLine.charAt(start - 1))) --start;
const jsHinter = CodeMirror.hint.javascript;
let result = jsHinter(editor) || { list: [] };
result.to = CodeMirror.Pos(cursor.line, end);
result.from = CodeMirror.Pos(cursor.line, start);
if (curWordBru) {
hintWords.forEach((h) => {
if (h.includes('.') == curWordBru.includes('.') && h.startsWith(curWordBru)) {
result.list.push(curWordBru.includes('.') ? h.split('.')[1] : h);
}
});
result.list?.sort();
}
return result;
});
CodeMirror.commands.autocomplete = (cm, hint, options) => {
cm.showHint({ hint, ...options });
};
}
export default class CodeEditor extends React.Component {
@ -41,7 +114,8 @@ export default class CodeEditor extends React.Component {
matchBrackets: true,
showCursorWhenSelecting: true,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
lint: { esversion: 11 },
readOnly: this.props.readOnly,
scrollbarStyle: 'overlay',
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
@ -68,15 +142,72 @@ export default class CodeEditor extends React.Component {
},
'Cmd-F': 'findPersistent',
'Ctrl-F': 'findPersistent',
'Cmd-H': 'replace',
'Ctrl-H': 'replace',
Tab: function (cm) {
cm.replaceSelection(' ', 'end');
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
? cm.execCommand('indentMore')
: cm.replaceSelection(' ', 'end');
},
'Shift-Tab': 'indentLess',
'Ctrl-Space': 'autocomplete',
'Cmd-Space': 'autocomplete',
'Ctrl-Y': 'foldAll',
'Cmd-Y': 'foldAll',
'Ctrl-I': 'unfoldAll',
'Cmd-I': 'unfoldAll'
},
foldOptions: {
widget: (from, to) => {
var count = undefined;
var internal = this.editor.getRange(from, to);
if (this.props.mode == 'application/ld+json') {
if (this.editor.getLine(from.line).endsWith('[')) {
var toParse = '[' + internal + ']';
} else var toParse = '{' + internal + '}';
try {
count = Object.keys(JSON.parse(toParse)).length;
} catch (e) {}
} else if (this.props.mode == 'application/xml') {
var doc = new DOMParser();
try {
//add header element and remove prefix namespaces for DOMParser
var dcm = doc.parseFromString(
'<a> ' + internal.replace(/(?<=\<|<\/)\w+:/g, '') + '</a>',
'application/xml'
);
count = dcm.documentElement.children.length;
} catch (e) {}
}
return count ? `\u21A4${count}\u21A6` : '\u2194';
}
}
}));
if (editor) {
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? { esversion: 11 } : false);
editor.on('change', this._onEdit);
this.addOverlay();
}
if (this.props.mode == 'javascript') {
editor.on('keyup', function (cm, event) {
const cursor = editor.getCursor();
const currentLine = editor.getLine(cursor.line);
let start = cursor.ch;
let end = start;
while (end < currentLine.length && /[^{}();\s\[\]\,]/.test(currentLine.charAt(end))) ++end;
while (start && /[^{}();\s\[\]\,]/.test(currentLine.charAt(start - 1))) --start;
let curWord = start != end && currentLine.slice(start, end);
//Qualify if autocomplete will be shown
if (
/^(?!Shift|Tab|Enter|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|\s)\w*/.test(event.key) &&
curWord.length > 0 &&
!/\/\/|\/\*|.*{{|`[^$]*{|`[^{]*$/.test(currentLine.slice(0, end)) &&
/(?<!\d)[a-zA-Z\._]$/.test(curWord)
) {
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.brunoJS, { completeSingle: false });
}
});
}
}
componentDidUpdate(prevProps) {
@ -117,6 +248,9 @@ export default class CodeEditor extends React.Component {
}
render() {
if (this.editor) {
this.editor.refresh();
}
return (
<StyledWrapper
className="h-full w-full"
@ -140,6 +274,7 @@ export default class CodeEditor extends React.Component {
_onEdit = () => {
if (!this.ignoreChangeEvent && this.editor) {
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? { esversion: 11 } : false);
this.cachedValue = this.editor.getValue();
if (this.props.onEdit) {
this.props.onEdit(this.cachedValue);

View File

@ -61,6 +61,15 @@ const AuthMode = ({ collection }) => {
>
Bearer Token
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('digest');
}}
>
Digest Auth
</div>
<div
className="dropdown-item"
onClick={() => {

View File

@ -0,0 +1,16 @@
import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
}
.single-line-editor-wrapper {
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
}
`;
export default Wrapper;

View File

@ -0,0 +1,71 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const DigestAuth = ({ collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const digestAuth = get(collection, 'root.request.auth.digest', {});
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
const handleUsernameChange = (username) => {
dispatch(
updateCollectionAuth({
mode: 'digest',
collectionUid: collection.uid,
content: {
username: username,
password: digestAuth.password
}
})
);
};
const handlePasswordChange = (password) => {
dispatch(
updateCollectionAuth({
mode: 'digest',
collectionUid: collection.uid,
content: {
username: digestAuth.username,
password: password
}
})
);
};
return (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Username</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={digestAuth.username || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleUsernameChange(val)}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">Password</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={digestAuth.password || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
collection={collection}
/>
</div>
</StyledWrapper>
);
};
export default DigestAuth;

View File

@ -5,6 +5,7 @@ import AuthMode from './AuthMode';
import AwsV4Auth from './AwsV4Auth';
import BearerAuth from './BearerAuth';
import BasicAuth from './BasicAuth';
import DigestAuth from './DigestAuth';
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
@ -25,6 +26,9 @@ const Auth = ({ collection }) => {
case 'bearer': {
return <BearerAuth collection={collection} />;
}
case 'digest': {
return <DigestAuth collection={collection} />;
}
}
};

View File

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

View File

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

View File

@ -13,10 +13,12 @@ const Wrapper = styled.div`
padding: 4px 10px;
&:nth-child(1),
&:nth-child(4),
&:nth-child(5) {
&:nth-child(4) {
width: 70px;
}
&:nth-child(5) {
width: 40px;
}
&:nth-child(2) {
width: 25%;

View File

@ -1,68 +1,83 @@
import React, { useReducer } from 'react';
import React from 'react';
import toast from 'react-hot-toast';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import reducer from './reducer';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { uuid } from 'utils/common';
import { envVariableNameRegex } from 'utils/common/regex';
const EnvironmentVariables = ({ environment, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const [state, reducerDispatch] = useReducer(reducer, { hasChanges: false, variables: environment.variables || [] });
const { variables, hasChanges } = state;
const saveChanges = () => {
dispatch(saveEnvironment(cloneDeep(variables), environment.uid, collection.uid))
.then(() => {
toast.success('Changes saved successfully');
reducerDispatch({
type: 'CHANGES_SAVED'
});
const formik = useFormik({
enableReinitialize: true,
initialValues: environment.variables || [],
validationSchema: Yup.array().of(
Yup.object({
enabled: Yup.boolean(),
name: Yup.string()
.required('Name cannot be empty')
.matches(
envVariableNameRegex,
'Name contains invalid characters. Must only contain alphanumeric characters, "-" and "_"'
)
.trim(),
secret: Yup.boolean(),
type: Yup.string(),
uid: Yup.string(),
value: Yup.string().trim().nullable()
})
.catch(() => toast.error('An error occurred while saving the changes'));
),
onSubmit: (values) => {
if (!formik.dirty) {
toast.error('Nothing to save');
return;
}
dispatch(saveEnvironment(cloneDeep(values), environment.uid, collection.uid))
.then(() => {
toast.success('Changes saved successfully');
formik.resetForm({ values });
})
.catch(() => toast.error('An error occurred while saving the changes'));
}
});
const ErrorMessage = ({ name }) => {
const meta = formik.getFieldMeta(name);
console.log(name, meta);
if (!meta.error) {
return null;
}
return (
<label htmlFor={name} className="text-red-500">
{meta.error}
</label>
);
};
const addVariable = () => {
reducerDispatch({
type: 'ADD_VAR'
});
const newVariable = {
uid: uuid(),
name: '',
value: '',
type: 'text',
secret: false,
enabled: true
};
formik.setFieldValue(formik.values.length, newVariable, false);
};
const handleVarChange = (e, _variable, type) => {
const variable = cloneDeep(_variable);
switch (type) {
case 'name': {
variable.name = e.target.value;
break;
}
case 'value': {
variable.value = e.target.value;
break;
}
case 'enabled': {
variable.enabled = e.target.checked;
break;
}
case 'secret': {
variable.secret = e.target.checked;
break;
}
}
reducerDispatch({
type: 'UPDATE_VAR',
variable
});
};
const handleRemoveVars = (variable) => {
reducerDispatch({
type: 'DELETE_VAR',
variable
});
const handleRemoveVar = (id) => {
formik.setValues(formik.values.filter((variable) => variable.uid !== id));
};
return (
@ -78,55 +93,57 @@ const EnvironmentVariables = ({ environment, collection }) => {
</tr>
</thead>
<tbody>
{variables && variables.length
? variables.map((variable, index) => {
return (
<tr key={variable.uid}>
<td className="text-center">
<input
type="checkbox"
checked={variable.enabled}
className="mr-3 mousetrap"
onChange={(e) => handleVarChange(e, variable, 'enabled')}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={variable.name}
className="mousetrap"
onChange={(e) => handleVarChange(e, variable, 'name')}
/>
</td>
<td>
<SingleLineEditor
value={variable.value}
theme={storedTheme}
onChange={(newValue) => handleVarChange({ target: { value: newValue } }, variable, 'value')}
collection={collection}
/>
</td>
<td>
<input
type="checkbox"
checked={variable.secret}
className="mr-3 mousetrap"
onChange={(e) => handleVarChange(e, variable, 'secret')}
/>
</td>
<td>
<button onClick={() => handleRemoveVars(variable)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</td>
</tr>
);
})
: null}
{formik.values.map((variable, index) => (
<tr key={variable.uid}>
<td className="text-center">
<input
type="checkbox"
className="mr-3 mousetrap"
name={`${index}.enabled`}
checked={variable.enabled}
onChange={formik.handleChange}
/>
</td>
<td>
<input
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
className="mousetrap"
id={`${index}.name`}
name={`${index}.name`}
value={variable.name}
onChange={formik.handleChange}
/>
<ErrorMessage name={`${index}.name`} />
</td>
<td>
<SingleLineEditor
theme={storedTheme}
collection={collection}
name={`${index}.value`}
value={variable.value}
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
/>
</td>
<td>
<input
type="checkbox"
className="mr-3 mousetrap"
name={`${index}.secret`}
checked={variable.secret}
onChange={formik.handleChange}
/>
</td>
<td>
<button onClick={() => handleRemoveVar(variable.uid)}>
<IconTrash strokeWidth={1.5} size={20} />
</button>
</td>
</tr>
))}
</tbody>
</table>
@ -137,12 +154,7 @@ const EnvironmentVariables = ({ environment, collection }) => {
</div>
<div>
<button
type="submit"
className="submit btn btn-md btn-secondary mt-2"
disabled={!hasChanges}
onClick={saveChanges}
>
<button type="submit" className="submit btn btn-md btn-secondary mt-2" onClick={formik.handleSubmit}>
Save
</button>
</div>

View File

@ -1,52 +0,0 @@
import produce from 'immer';
import find from 'lodash/find';
import filter from 'lodash/filter';
import { uuid } from 'utils/common';
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_VAR': {
return produce(state, (draft) => {
draft.variables.push({
uid: uuid(),
name: '',
value: '',
type: 'text',
secret: false,
enabled: true
});
draft.hasChanges = true;
});
}
case 'UPDATE_VAR': {
return produce(state, (draft) => {
const variable = find(draft.variables, (v) => v.uid === action.variable.uid);
variable.name = action.variable.name;
variable.value = action.variable.value;
variable.enabled = action.variable.enabled;
variable.secret = action.variable.secret;
draft.hasChanges = true;
});
}
case 'DELETE_VAR': {
return produce(state, (draft) => {
draft.variables = filter(draft.variables, (v) => v.uid !== action.variable.uid);
draft.hasChanges = true;
});
}
case 'CHANGES_SAVED': {
return produce(state, (draft) => {
draft.hasChanges = false;
});
}
default: {
return state;
}
}
};
export default reducer;

View File

@ -11,6 +11,8 @@ const StyledWrapper = styled.div`
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
min-height: 400px;
height: 100%;
max-height: 85vh;
overflow-y: auto;
}
.environment-item {
@ -43,10 +45,8 @@ const StyledWrapper = styled.div`
border-bottom: none;
color: ${(props) => props.theme.textLink};
&:hover {
span {
text-decoration: underline;
}
span:hover {
text-decoration: underline;
}
}

View File

@ -1,12 +1,11 @@
import React, { useEffect, useState, forwardRef, useRef } from 'react';
import { findEnvironmentInCollection } from 'utils/collections';
import toast from 'react-hot-toast';
import { toastError } from 'utils/common/error';
import usePrevious from 'hooks/usePrevious';
import EnvironmentDetails from './EnvironmentDetails';
import CreateEnvironment from '../CreateEnvironment';
import { IconDownload } from '@tabler/icons';
import { IconDownload, IconShieldLock } from '@tabler/icons';
import ImportEnvironment from '../ImportEnvironment';
import ManageSecrets from '../ManageSecrets';
import StyledWrapper from './StyledWrapper';
const EnvironmentList = ({ collection }) => {
@ -14,6 +13,7 @@ const EnvironmentList = ({ collection }) => {
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
const [openCreateModal, setOpenCreateModal] = useState(false);
const [openImportModal, setOpenImportModal] = useState(false);
const [openManageSecretsModal, setOpenManageSecretsModal] = useState(false);
const envUids = environments ? environments.map((env) => env.uid) : [];
const prevEnvUids = usePrevious(envUids);
@ -54,6 +54,7 @@ const EnvironmentList = ({ collection }) => {
<StyledWrapper>
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
{openImportModal && <ImportEnvironment collection={collection} onClose={() => setOpenImportModal(false)} />}
{openManageSecretsModal && <ManageSecrets onClose={() => setOpenManageSecretsModal(false)} />}
<div className="flex">
<div>
<div className="environments-sidebar flex flex-col">
@ -72,9 +73,15 @@ const EnvironmentList = ({ collection }) => {
+ <span>Create</span>
</div>
<div className="mt-auto flex items-center btn-import-environment" onClick={() => setOpenImportModal(true)}>
<IconDownload size={12} strokeWidth={2} />
<span className="label ml-1 text-xs">Import</span>
<div className="mt-auto btn-import-environment">
<div className="flex items-center" onClick={() => setOpenImportModal(true)}>
<IconDownload size={12} strokeWidth={2} />
<span className="label ml-1 text-xs">Import</span>
</div>
<div className="flex items-center mt-2" onClick={() => setOpenManageSecretsModal(true)}>
<IconShieldLock size={12} strokeWidth={2} />
<span className="label ml-1 text-xs">Managing Secrets</span>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,31 @@
import React from 'react';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
const ManageSecrets = ({ onClose }) => {
return (
<Portal>
<Modal size="sm" title="Manage Secrets" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
<div>
<p>In any collection, there are secrets that need to be managed.</p>
<p className="mt-2">These secrets can be anything such as API keys, passwords, or tokens.</p>
<p className="mt-4">Bruno offers two approaches to manage secrets in collections.</p>
<p className="mt-2">
Read more about it in our{' '}
<a
href="https://docs.usebruno.com/secrets-management/overview.html"
target="_blank"
rel="noreferrer"
className="text-link hover:underline"
>
docs
</a>
.
</p>
</div>
</Modal>
</Portal>
);
};
export default ManageSecrets;

View File

@ -22,13 +22,11 @@ const ProxySettings = ({ close }) => {
})
.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),
.max(65535)
.typeError('Specify port between 1 and 65535')
.nullable()
.transform((_, val) => (val ? Number(val) : null)),
auth: Yup.object()
.when('enabled', {
is: true,

View File

@ -62,6 +62,15 @@ const AuthMode = ({ item, collection }) => {
>
Bearer Token
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('digest');
}}
>
Digest Auth
</div>
<div
className="dropdown-item"
onClick={() => {

View File

@ -0,0 +1,16 @@
import styled from 'styled-components';
const Wrapper = styled.div`
label {
font-size: 0.8125rem;
}
.single-line-editor-wrapper {
padding: 0.15rem 0.4rem;
border-radius: 3px;
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
}
`;
export default Wrapper;

View File

@ -0,0 +1,76 @@
import React from 'react';
import get from 'lodash/get';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux';
import SingleLineEditor from 'components/SingleLineEditor';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const DigestAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const digestAuth = item.draft ? get(item, 'draft.request.auth.digest', {}) : get(item, 'request.auth.digest', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleUsernameChange = (username) => {
dispatch(
updateAuth({
mode: 'digest',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: username,
password: digestAuth.password
}
})
);
};
const handlePasswordChange = (password) => {
dispatch(
updateAuth({
mode: 'digest',
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: digestAuth.username,
password: password
}
})
);
};
return (
<StyledWrapper className="mt-2 w-full">
<label className="block font-medium mb-2">Username</label>
<div className="single-line-editor-wrapper mb-2">
<SingleLineEditor
value={digestAuth.username || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handleUsernameChange(val)}
onRun={handleRun}
collection={collection}
/>
</div>
<label className="block font-medium mb-2">Password</label>
<div className="single-line-editor-wrapper">
<SingleLineEditor
value={digestAuth.password || ''}
theme={storedTheme}
onSave={handleSave}
onChange={(val) => handlePasswordChange(val)}
onRun={handleRun}
collection={collection}
/>
</div>
</StyledWrapper>
);
};
export default DigestAuth;

View File

@ -4,6 +4,7 @@ import AuthMode from './AuthMode';
import AwsV4Auth from './AwsV4Auth';
import BearerAuth from './BearerAuth';
import BasicAuth from './BasicAuth';
import DigestAuth from './DigestAuth';
import StyledWrapper from './StyledWrapper';
const Auth = ({ item, collection }) => {
@ -20,6 +21,9 @@ const Auth = ({ item, collection }) => {
case 'bearer': {
return <BearerAuth collection={collection} item={item} />;
}
case 'digest': {
return <DigestAuth collection={collection} item={item} />;
}
}
};

View File

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

View File

@ -1,8 +1,7 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import find from 'lodash/find';
import get from 'lodash/get';
import classnames from 'classnames';
import { IconRefresh, IconLoader2, IconBook, IconDownload } from '@tabler/icons';
import { useSelector, useDispatch } from 'react-redux';
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
import QueryEditor from 'components/RequestPane/QueryEditor';
@ -16,10 +15,9 @@ import Tests from 'components/RequestPane/Tests';
import { useTheme } from 'providers/Theme';
import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { findEnvironmentInCollection } from 'utils/collections';
import useGraphqlSchema from './useGraphqlSchema';
import StyledWrapper from './StyledWrapper';
import Documentation from 'components/Documentation/index';
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
const dispatch = useDispatch();
@ -29,25 +27,11 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
const variables = item.draft
? get(item, 'draft.request.body.graphql.variables')
: get(item, 'request.body.graphql.variables');
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
const { storedTheme } = useTheme();
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
const request = item.draft ? item.draft.request : item.request;
let { schema, loadSchema, isLoading: isSchemaLoading } = useGraphqlSchema(url, environment, request, collection);
const loadGqlSchema = () => {
if (!isSchemaLoading) {
loadSchema();
}
};
const [schema, setSchema] = useState(null);
useEffect(() => {
if (onSchemaLoad) {
onSchemaLoad(schema);
}
onSchemaLoad(schema);
}, [schema]);
const onQueryChange = (value) => {
@ -163,18 +147,7 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
Docs
</div>
<div className="flex flex-grow justify-end items-center" style={{ fontSize: 13 }}>
<div className="flex items-center cursor-pointer hover:underline" onClick={loadGqlSchema}>
{isSchemaLoading ? <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} /> : null}
{!isSchemaLoading && !schema ? <IconDownload size={18} strokeWidth={1.5} /> : null}
{!isSchemaLoading && schema ? <IconRefresh size={18} strokeWidth={1.5} /> : null}
<span className="ml-1">Schema</span>
</div>
<div className="flex items-center cursor-pointer hover:underline ml-2" onClick={toggleDocs}>
<IconBook size={18} strokeWidth={1.5} />
<span className="ml-1">Docs</span>
</div>
</div>
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
</div>
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
</StyledWrapper>

View File

@ -1,60 +0,0 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import { buildClientSchema } from 'graphql';
import { fetchGqlSchema } from 'utils/network';
import { simpleHash } from 'utils/common';
const schemaHashPrefix = 'bruno.graphqlSchema';
const useGraphqlSchema = (endpoint, environment, request, collection) => {
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [schema, setSchema] = useState(() => {
try {
const saved = localStorage.getItem(localStorageKey);
if (!saved) {
return null;
}
return buildClientSchema(JSON.parse(saved));
} catch {
localStorage.setItem(localStorageKey, null);
return null;
}
});
const loadSchema = () => {
setIsLoading(true);
fetchGqlSchema(endpoint, environment, request, collection)
.then((res) => {
if (!res || res.status !== 200) {
return Promise.reject(new Error(res.statusText));
}
return res.data;
})
.then((s) => {
if (s && s.data) {
setSchema(buildClientSchema(s.data));
setIsLoading(false);
localStorage.setItem(localStorageKey, JSON.stringify(s.data));
toast.success('GraphQL Schema loaded successfully');
} else {
return Promise.reject(new Error('An error occurred while introspecting schema'));
}
})
.catch((err) => {
setIsLoading(false);
setError(err);
toast.error(`Error occurred while loading GraphQL Schema: ${err.message}`);
});
};
return {
isLoading,
schema,
loadSchema,
error
};
};
export default useGraphqlSchema;

View File

@ -0,0 +1,70 @@
import React, { useEffect, useRef, forwardRef } from 'react';
import useGraphqlSchema from './useGraphqlSchema';
import { IconBook, IconDownload, IconLoader2, IconRefresh } from '@tabler/icons';
import get from 'lodash/get';
import { findEnvironmentInCollection } from 'utils/collections';
import Dropdown from '../../Dropdown';
const GraphQLSchemaActions = ({ item, collection, onSchemaLoad, toggleDocs }) => {
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
const request = item.draft ? item.draft.request : item.request;
let {
schema,
schemaSource,
loadSchema,
isLoading: isSchemaLoading
} = useGraphqlSchema(url, environment, request, collection);
useEffect(() => {
if (onSchemaLoad) {
onSchemaLoad(schema);
}
}, [schema]);
const schemaDropdownTippyRef = useRef();
const onSchemaDropdownCreate = (ref) => (schemaDropdownTippyRef.current = ref);
const MenuIcon = forwardRef((props, ref) => {
return (
<div ref={ref} className="dropdown-icon cursor-pointer flex hover:underline ml-2">
{isSchemaLoading && <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} />}
{!isSchemaLoading && schema && <IconRefresh size={18} strokeWidth={1.5} />}
{!isSchemaLoading && !schema && <IconDownload size={18} strokeWidth={1.5} />}
<span className="ml-1">Schema</span>
</div>
);
});
return (
<div className="flex flex-grow justify-end items-center" style={{ fontSize: 13 }}>
<div className="flex items-center cursor-pointer hover:underline" onClick={toggleDocs}>
<IconBook size={18} strokeWidth={1.5} />
<span className="ml-1">Docs</span>
</div>
<Dropdown onCreate={onSchemaDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
<div
className="dropdown-item"
onClick={(e) => {
schemaDropdownTippyRef.current.hide();
loadSchema('introspection');
}}
>
{schema && schemaSource === 'introspection' ? 'Refresh from Introspection' : 'Load from Introspection'}
</div>
<div
className="dropdown-item"
onClick={(e) => {
schemaDropdownTippyRef.current.hide();
loadSchema('file');
}}
>
Load from File
</div>
</Dropdown>
</div>
);
};
export default GraphQLSchemaActions;

View File

@ -0,0 +1,89 @@
import { useState } from 'react';
import toast from 'react-hot-toast';
import { buildClientSchema } from 'graphql';
import { fetchGqlSchema } from 'utils/network';
import { simpleHash } from 'utils/common';
const schemaHashPrefix = 'bruno.graphqlSchema';
const useGraphqlSchema = (endpoint, environment, request, collection) => {
const { ipcRenderer } = window;
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [schemaSource, setSchemaSource] = useState('');
const [schema, setSchema] = useState(() => {
try {
const saved = localStorage.getItem(localStorageKey);
if (!saved) {
return null;
}
return buildClientSchema(JSON.parse(saved));
} catch {
localStorage.setItem(localStorageKey, null);
return null;
}
});
const loadSchemaFromIntrospection = async () => {
const response = await fetchGqlSchema(endpoint, environment, request, collection);
if (!response) {
throw new Error('Introspection query failed');
}
if (response.status !== 200) {
throw new Error(response.statusText);
}
const data = response.data?.data;
if (!data) {
throw new Error('No data returned from introspection query');
}
setSchemaSource('introspection');
return data;
};
const loadSchemaFromFile = async () => {
const schemaContent = await ipcRenderer.invoke('renderer:load-gql-schema-file');
if (!schemaContent) {
setIsLoading(false);
return;
}
setSchemaSource('file');
return schemaContent.data;
};
const loadSchema = async (schemaSource) => {
if (isLoading) {
return;
}
setIsLoading(true);
try {
let data;
if (schemaSource === 'file') {
data = await loadSchemaFromFile();
} else {
// fallback to introspection if source is unknown
data = await loadSchemaFromIntrospection();
}
setSchema(buildClientSchema(data));
localStorage.setItem(localStorageKey, JSON.stringify(data));
toast.success('GraphQL Schema loaded successfully');
} catch (err) {
setError(err);
toast.error(`Error occurred while loading GraphQL Schema: ${err.message}`);
}
setIsLoading(false);
};
return {
isLoading,
schema,
schemaSource,
loadSchema,
error
};
};
export default useGraphqlSchema;

View File

@ -34,7 +34,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
requestUrlChanged({
itemUid: item.uid,
collectionUid: collection.uid,
url: value
url: value && typeof value === 'string' ? value.trim() : value
})
);
};

View File

@ -8,6 +8,8 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
import SingleLineEditor from 'components/SingleLineEditor';
import Tooltip from 'components/Tooltip';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
import { envVariableNameRegex } from 'utils/common/regex';
const VarsTable = ({ item, collection, vars, varType }) => {
const dispatch = useDispatch();
@ -29,7 +31,21 @@ const VarsTable = ({ item, collection, vars, varType }) => {
const _var = cloneDeep(v);
switch (type) {
case 'name': {
_var.name = e.target.value;
const value = e.target.value;
if (/^(?!\d).*$/.test(value) === false) {
toast.error('Variable names must not start with a number!');
return;
}
if (envVariableNameRegex.test(value) === false) {
toast.error(
'Variable contains invalid character! Variables must only contain alpha-numeric characters, "-" and "_".'
);
return;
}
_var.name = value;
break;
}
case 'value': {
@ -88,7 +104,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
</thead>
<tbody>
{vars && vars.length
? vars.map((_var, index) => {
? vars.map((_var) => {
return (
<tr key={_var.uid}>
<td>

View File

@ -1,42 +1,12 @@
import React from 'react';
import get from 'lodash/get';
import { useDispatch } from 'react-redux';
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useTheme } from 'providers/Theme';
import VarsTable from './VarsTable';
import StyledWrapper from './StyledWrapper';
const Vars = ({ item, collection }) => {
const dispatch = useDispatch();
const requestVars = item.draft ? get(item, 'draft.request.vars.req') : get(item, 'request.vars.req');
const responseVars = item.draft ? get(item, 'draft.request.vars.res') : get(item, 'request.vars.res');
const { storedTheme } = useTheme();
const onRequestScriptEdit = (value) => {
dispatch(
updateRequestScript({
script: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onResponseScriptEdit = (value) => {
dispatch(
updateResponseScript({
script: value,
itemUid: item.uid,
collectionUid: collection.uid
})
);
};
const onRun = () => dispatch(sendRequest(item, collection.uid));
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
return (
<StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2">

View File

@ -2,6 +2,11 @@ import CodeEditor from 'components/CodeEditor/index';
import { get } from 'lodash';
import { useDispatch, useSelector } from 'react-redux';
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { Document, Page } from 'react-pdf';
import { useState } from 'react';
import 'pdfjs-dist/build/pdf.worker';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
const QueryResultPreview = ({
previewTab,
@ -19,6 +24,10 @@ const QueryResultPreview = ({
const preferences = useSelector((state) => state.app.preferences);
const dispatch = useDispatch();
const [numPages, setNumPages] = useState(null);
function onDocumentLoadSuccess({ numPages }) {
setNumPages(numPages);
}
// Fail safe, so we don't render anything with an invalid tab
if (!allowedPreviewModes.includes(previewTab)) {
return null;
@ -45,6 +54,17 @@ const QueryResultPreview = ({
case 'preview-image': {
return <img src={`data:${contentType.replace(/\;(.*)/, '')};base64,${dataBuffer}`} className="mx-auto" />;
}
case 'preview-pdf': {
return (
<div className="preview-pdf" style={{ height: '100%', overflow: 'auto', maxHeight: 'calc(100vh - 220px)' }}>
<Document file={`data:application/pdf;base64,${dataBuffer}`} onLoadSuccess={onDocumentLoadSuccess}>
{Array.from(new Array(numPages), (el, index) => (
<Page key={`page_${index + 1}`} pageNumber={index + 1} renderAnnotationLayer={false} />
))}
</Document>
</div>
);
}
default:
case 'raw': {
return (

View File

@ -18,11 +18,28 @@ const StyledWrapper = styled.div`
width: 100%;
}
.react-pdf__Page {
margin-top: 10px;
background-color: transparent !important;
}
.react-pdf__Page__textContent {
border: 1px solid darkgrey;
box-shadow: 5px 5px 5px 1px #ccc;
border-radius: 0px;
margin: 0 auto;
}
.react-pdf__Page__canvas {
margin: 0 auto;
}
div[role='tablist'] {
.active {
color: ${(props) => props.theme.colors.text.yellow};
}
}
.muted {
color: ${(props) => props.theme.colors.text.muted};
}
`;
export default StyledWrapper;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,10 +9,11 @@ const CodeView = ({ language, item }) => {
const { storedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const { target, client, language: lang } = language;
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
let snippet = '';
try {
snippet = new HTTPSnippet(buildHarRequest(item.request)).convert(target, client);
snippet = new HTTPSnippet(buildHarRequest({ request: item.request, headers })).convert(target, client);
} catch (e) {
console.error(e);
snippet = 'Error generating code snippet';

View File

@ -33,7 +33,8 @@ const Wrapper = styled.div`
overflow: hidden;
}
&:hover {
&:hover,
&.item-hovered {
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
.menu-icon {
.dropdown {

View File

@ -84,7 +84,8 @@ const CollectionItem = ({ item, collection, searchText }) => {
});
const itemRowClassName = classnames('flex collection-item-name items-center', {
'item-focused-in-tab': item.uid == activeTabUid
'item-focused-in-tab': item.uid == activeTabUid,
'item-hovered': isOver
});
const scrollToTheActiveTab = () => {

View File

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

View File

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

View File

@ -35,6 +35,10 @@ const StyledWrapper = styled.div`
}
}
}
textarea.curl-command {
min-height: 150px;
}
`;
export default StyledWrapper;

View File

@ -1,4 +1,4 @@
import React, { useRef, useEffect } from 'react';
import React, { useRef, useEffect, useCallback } from 'react';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import toast from 'react-hot-toast';
@ -11,6 +11,7 @@ import { addTab } from 'providers/ReduxStore/slices/tabs';
import HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';
import { getDefaultRequestPaneTab } from 'utils/collections';
import StyledWrapper from './StyledWrapper';
import { getRequestFromCurlCommand } from 'utils/curl';
const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
const dispatch = useDispatch();
@ -24,7 +25,8 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
requestName: '',
requestType: collectionPresets.defaultType || 'http-request',
requestUrl: collectionPresets.defaultRequestUrl || '',
requestMethod: 'GET'
requestMethod: 'GET',
curlCommand: ''
},
validationSchema: Yup.object({
requestName: Yup.string()
@ -38,7 +40,18 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
const trimmedValue = value ? value.trim().toLowerCase() : '';
return !['collection', 'folder'].includes(trimmedValue);
}
})
}),
curlCommand: Yup.string().when('requestType', {
is: (requestType) => requestType === 'from-curl',
then: Yup.string()
.min(1, 'must be at least 1 character')
.required('curlCommand is required')
.test({
name: 'curlCommand',
message: `Invalid cURL Command`,
test: (value) => getRequestFromCurlCommand(value) !== null
})
})
}),
onSubmit: (values) => {
if (isEphemeral) {
@ -64,6 +77,22 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
onClose();
})
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
} else if (values.requestType === 'from-curl') {
const request = getRequestFromCurlCommand(values.curlCommand);
dispatch(
newHttpRequest({
requestName: values.requestName,
requestType: 'http-request',
requestUrl: request.url,
requestMethod: request.method,
collectionUid: collection.uid,
itemUid: item ? item.uid : null,
headers: request.headers,
body: request.body
})
)
.then(() => onClose())
.catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));
} else {
dispatch(
newHttpRequest({
@ -89,6 +118,25 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
const onSubmit = () => formik.handleSubmit();
const handlePaste = useCallback(
(event) => {
const clipboardData = event.clipboardData || window.clipboardData;
const pastedData = clipboardData.getData('Text');
// Check if pasted data looks like a cURL command
const curlCommandRegex = /^\s*curl\s/i;
if (curlCommandRegex.test(pastedData)) {
// Switch to the 'from-curl' request type
formik.setFieldValue('requestType', 'from-curl');
formik.setFieldValue('curlCommand', pastedData);
// Prevent the default paste behavior to avoid pasting into the textarea
event.preventDefault();
}
},
[formik]
);
return (
<StyledWrapper>
<Modal size="md" title="New Request" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
@ -127,15 +175,28 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
<label htmlFor="graphql-request" className="ml-1 cursor-pointer select-none">
GraphQL
</label>
<input
id="from-curl"
className="cursor-pointer ml-auto"
type="radio"
name="requestType"
onChange={formik.handleChange}
value="from-curl"
checked={formik.values.requestType === 'from-curl'}
/>
<label htmlFor="from-curl" className="ml-1 cursor-pointer select-none">
From cURL
</label>
</div>
</div>
<div className="mt-4">
<label htmlFor="requestName" className="block font-semibold">
Name
</label>
<input
id="collection-name"
id="request-name"
type="text"
name="requestName"
ref={inputRef}
@ -151,38 +212,58 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
<div className="text-red-500">{formik.errors.requestName}</div>
) : null}
</div>
{formik.values.requestType !== 'from-curl' ? (
<>
<div className="mt-4">
<label htmlFor="request-url" className="block font-semibold">
URL
</label>
<div className="mt-4">
<label htmlFor="request-url" className="block font-semibold">
URL
</label>
<div className="flex items-center mt-2 ">
<div className="flex items-center h-full method-selector-container">
<HttpMethodSelector
method={formik.values.requestMethod}
onMethodSelect={(val) => formik.setFieldValue('requestMethod', val)}
/>
</div>
<div className="flex items-center flex-grow input-container h-full">
<input
id="request-url"
type="text"
name="requestUrl"
className="px-3 w-full "
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.requestUrl || ''}
/>
<div className="flex items-center mt-2 ">
<div className="flex items-center h-full method-selector-container">
<HttpMethodSelector
method={formik.values.requestMethod}
onMethodSelect={(val) => formik.setFieldValue('requestMethod', val)}
/>
</div>
<div className="flex items-center flex-grow input-container h-full">
<input
id="request-url"
type="text"
name="requestUrl"
className="px-3 w-full "
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
onChange={formik.handleChange}
value={formik.values.requestUrl || ''}
onPaste={handlePaste}
/>
</div>
</div>
{formik.touched.requestUrl && formik.errors.requestUrl ? (
<div className="text-red-500">{formik.errors.requestUrl}</div>
) : null}
</div>
</>
) : (
<div className="mt-4">
<label htmlFor="request-url" className="block font-semibold">
cURL Command
</label>
<textarea
name="curlCommand"
placeholder="Enter cURL request here.."
className="block textbox w-full mt-4 curl-command"
value={formik.values.curlCommand}
onChange={formik.handleChange}
></textarea>
{formik.touched.curlCommand && formik.errors.curlCommand ? (
<div className="text-red-500">{formik.errors.curlCommand}</div>
) : null}
</div>
{formik.touched.requestUrl && formik.errors.requestUrl ? (
<div className="text-red-500">{formik.errors.requestUrl}</div>
) : null}
</div>
)}
</form>
</Modal>
</StyledWrapper>

View File

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

View File

@ -57,6 +57,7 @@ class SingleLineEditor extends Component {
}
componentDidMount() {
// Initialize CodeMirror as a single line editor
/** @type {import("codemirror").Editor} */
this.editor = CodeMirror(this.editorRef.current, {
lineWrapping: false,
lineNumbers: false,
@ -84,7 +85,10 @@ class SingleLineEditor extends Component {
}
},
'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();
}
},

View File

@ -21,8 +21,12 @@ if (!SERVER_RENDERED) {
require('codemirror/addon/edit/matchbrackets');
require('codemirror/addon/fold/brace-fold');
require('codemirror/addon/fold/foldgutter');
require('codemirror/addon/fold/xml-fold');
require('codemirror/addon/hint/javascript-hint');
require('codemirror/addon/hint/show-hint');
require('codemirror/addon/lint/lint');
require('codemirror/addon/lint/javascript-lint');
require('codemirror/addon/lint/json-lint');
require('codemirror/addon/mode/overlay');
require('codemirror/addon/scroll/simplescrollbars');
require('codemirror/addon/search/jump-to-line');

View File

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

View File

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

View File

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

View File

@ -409,8 +409,8 @@ export const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {
});
};
export const sortCollections = () => (dispatch) => {
dispatch(_sortCollections());
export const sortCollections = (payload) => (dispatch) => {
dispatch(_sortCollections(payload));
};
export const moveItem = (collectionUid, draggedItemUid, targetItemUid) => (dispatch, getState) => {
const state = getState();
@ -584,7 +584,7 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di
};
export const newHttpRequest = (params) => (dispatch, getState) => {
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid } = params;
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body } = params;
return new Promise((resolve, reject) => {
const state = getState();
@ -607,9 +607,9 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
request: {
method: requestMethod,
url: requestUrl,
headers: [],
headers: headers ?? [],
params,
body: {
body: body ?? {
mode: 'none',
json: null,
text: null,

View File

@ -388,6 +388,10 @@ export const collectionsSlice = createSlice({
item.draft.request.auth.mode = 'basic';
item.draft.request.auth.basic = action.payload.content;
break;
case 'digest':
item.draft.request.auth.mode = 'digest';
item.draft.request.auth.digest = action.payload.content;
break;
}
}
}
@ -976,6 +980,9 @@ export const collectionsSlice = createSlice({
case 'basic':
set(collection, 'root.request.auth.basic', action.payload.content);
break;
case 'digest':
set(collection, 'root.request.auth.digest', action.payload.content);
break;
}
}
},

View File

@ -9,25 +9,17 @@ const createContentType = (mode) => {
case 'multipartForm':
return 'multipart/form-data';
default:
return 'application/json';
return '';
}
};
const createHeaders = (headers, mode) => {
const contentType = createContentType(mode);
const headersArray = headers
const createHeaders = (headers) => {
return headers
.filter((header) => header.enabled)
.map((header) => {
return {
name: header.name,
value: header.value
};
});
const headerNames = headersArray.map((header) => header.name);
if (!headerNames.includes('Content-Type')) {
return [...headersArray, { name: 'Content-Type', value: contentType }];
}
return headersArray;
.map((header) => ({
name: header.name,
value: header.value
}));
};
const createQuery = (queryParams = []) => {
@ -56,13 +48,13 @@ const createPostData = (body) => {
}
};
export const buildHarRequest = (request) => {
export const buildHarRequest = ({ request, headers }) => {
return {
method: request.method,
url: request.url,
httpVersion: 'HTTP/1.1',
cookies: [],
headers: createHeaders(request.headers, request.body.mode),
headers: createHeaders(headers),
queryString: createQuery(request.params),
postData: createPostData(request.body),
headersSize: 0,

View File

@ -494,6 +494,10 @@ export const humanizeRequestAuthMode = (mode) => {
label = 'Bearer Token';
break;
}
case 'digest': {
label = 'Digest Auth';
break;
}
}
return label;

View File

@ -1,5 +1,4 @@
import get from 'lodash/get';
import isString from 'lodash/isString';
let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
@ -10,7 +9,7 @@ if (!SERVER_RENDERED) {
const pathFoundInVariables = (path, obj) => {
const value = get(obj, path);
return isString(value);
return value !== undefined;
};
export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {

View File

@ -94,3 +94,15 @@ export const getContentType = (headers) => {
return '';
};
export const startsWith = (str, search) => {
if (!str || !str.length || typeof str !== 'string') {
return false;
}
if (!search || !search.length || typeof search !== 'string') {
return false;
}
return str.substr(0, search.length) === search;
};

View File

@ -1,6 +1,6 @@
const { describe, it, expect } = require('@jest/globals');
import { normalizeFileName } from './index';
import { normalizeFileName, startsWith } from './index';
describe('common utils', () => {
describe('normalizeFileName', () => {
@ -16,4 +16,37 @@ describe('common utils', () => {
expect(normalizeFileName('foo\\bar\\')).toBe('foo-bar-');
});
});
describe('startsWith', () => {
it('should return false if str is not a string', () => {
expect(startsWith(null, 'foo')).toBe(false);
expect(startsWith(undefined, 'foo')).toBe(false);
expect(startsWith(123, 'foo')).toBe(false);
expect(startsWith({}, 'foo')).toBe(false);
expect(startsWith([], 'foo')).toBe(false);
});
it('should return false if search is not a string', () => {
expect(startsWith('foo', null)).toBe(false);
expect(startsWith('foo', undefined)).toBe(false);
expect(startsWith('foo', 123)).toBe(false);
expect(startsWith('foo', {})).toBe(false);
expect(startsWith('foo', [])).toBe(false);
});
it('should return false if str does not start with search', () => {
expect(startsWith('foo', 'bar')).toBe(false);
expect(startsWith('foo', 'baz')).toBe(false);
expect(startsWith('foo', 'bar')).toBe(false);
expect(startsWith('foo', 'baz')).toBe(false);
expect(startsWith('foo', 'bar')).toBe(false);
expect(startsWith('foo', 'baz')).toBe(false);
});
it('should return true if str starts with search', () => {
expect(startsWith('foo', 'f')).toBe(true);
expect(startsWith('foo', 'fo')).toBe(true);
expect(startsWith('foo', 'foo')).toBe(true);
});
});
});

View File

@ -0,0 +1 @@
export const envVariableNameRegex = /^(?!\d)[\w-]*$/;

View File

@ -0,0 +1,169 @@
/**
* Copyright (c) 2014-2016 Nick Carneiro
* https://github.com/curlconverter/curlconverter
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import parseCurlCommand from './parse-curl';
import * as querystring from 'query-string';
import * as jsesc from 'jsesc';
function getContentType(headers = {}) {
const contentType = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type');
return contentType ? headers[contentType] : null;
}
function repr(value, isKey) {
return isKey ? "'" + jsesc(value, { quotes: 'single' }) + "'" : value;
}
function getQueries(request) {
const queries = {};
for (const paramName in request.query) {
const rawValue = request.query[paramName];
let paramValue;
if (Array.isArray(rawValue)) {
paramValue = rawValue.map(repr);
} else {
paramValue = repr(rawValue);
}
queries[repr(paramName)] = paramValue;
}
return queries;
}
function getDataString(request) {
if (typeof request.data === 'number') {
request.data = request.data.toString();
}
const contentType = getContentType(request.headers);
if (contentType && contentType.includes('application/json')) {
return { data: request.data.toString() };
}
const parsedQueryString = querystring.parse(request.data, { sort: false });
const keyCount = Object.keys(parsedQueryString).length;
const singleKeyOnly = keyCount === 1 && !parsedQueryString[Object.keys(parsedQueryString)[0]];
const singularData = request.isDataBinary || singleKeyOnly;
if (singularData) {
const data = {};
data[repr(request.data)] = '';
return { data: data };
} else {
return getMultipleDataString(request, parsedQueryString);
}
}
function getMultipleDataString(request, parsedQueryString) {
const data = {};
for (const key in parsedQueryString) {
const value = parsedQueryString[key];
if (Array.isArray(value)) {
data[repr(key)] = value;
} else {
data[repr(key)] = repr(value);
}
}
return { data: data };
}
function getFilesString(request) {
const data = {};
data.files = {};
data.data = {};
for (const multipartKey in request.multipartUploads) {
const multipartValue = request.multipartUploads[multipartKey];
if (multipartValue.startsWith('@')) {
const fileName = multipartValue.slice(1);
data.files[repr(multipartKey)] = repr(fileName);
} else {
data.data[repr(multipartKey)] = repr(multipartValue);
}
}
if (Object.keys(data.files).length === 0) {
delete data.files;
}
if (Object.keys(data.data).length === 0) {
delete data.data;
}
return data;
}
const curlToJson = (curlCommand) => {
const request = parseCurlCommand(curlCommand);
const requestJson = {};
// curl automatically prepends 'http' if the scheme is missing, but python fails and returns an error
// we tack it on here to mimic curl
if (!request.url.match(/https?:/)) {
request.url = 'http://' + request.url;
}
if (!request.urlWithoutQuery.match(/https?:/)) {
request.urlWithoutQuery = 'http://' + request.urlWithoutQuery;
}
requestJson.url = request.urlWithoutQuery.replace(/\/$/, '');
requestJson.raw_url = request.url;
requestJson.method = request.method;
if (request.cookies) {
const cookies = {};
for (const cookieName in request.cookies) {
cookies[repr(cookieName)] = repr(request.cookies[cookieName]);
}
requestJson.cookies = cookies;
}
if (request.headers) {
const headers = {};
for (const headerName in request.headers) {
headers[repr(headerName)] = repr(request.headers[headerName]);
}
requestJson.headers = headers;
}
if (request.query) {
requestJson.queries = getQueries(request);
}
if (typeof request.data === 'string' || typeof request.data === 'number') {
Object.assign(requestJson, getDataString(request));
} else if (request.multipartUploads) {
Object.assign(requestJson, getFilesString(request));
}
if (request.insecure) {
requestJson.insecure = false;
}
if (request.auth) {
const splitAuth = request.auth.split(':');
const user = splitAuth[0] || '';
const password = splitAuth[1] || '';
requestJson.auth = {
user: repr(user),
password: repr(password)
};
}
return Object.keys(requestJson).length ? requestJson : {};
};
export default curlToJson;

View File

@ -0,0 +1,62 @@
const { describe, it, expect } = require('@jest/globals');
import curlToJson from './curl-to-json';
describe('curlToJson', () => {
it('should return a parse a simple curl command', () => {
const curlCommand = 'curl https://www.usebruno.com';
const result = curlToJson(curlCommand);
expect(result).toEqual({
url: 'https://www.usebruno.com',
raw_url: 'https://www.usebruno.com',
method: 'get'
});
});
it('should return a parse a curl command with headers', () => {
const curlCommand = `curl https://www.usebruno.com
-H 'Accept: application/json, text/plain, */*'
-H 'Accept-Language: en-US,en;q=0.9,hi;q=0.8'
`;
const result = curlToJson(curlCommand);
expect(result).toEqual({
url: 'https://www.usebruno.com',
raw_url: 'https://www.usebruno.com',
method: 'get',
headers: {
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8'
}
});
});
it('should return a parse a curl with a post body', () => {
const curlCommand = `curl 'https://www.usebruno.com'
-H 'Accept: application/json, text/plain, */*'
-H 'Accept-Language: en-US,en;q=0.9,hi;q=0.8'
-H 'Content-Type: application/json;charset=utf-8'
-H 'Origin: https://www.usebruno.com'
-H 'Referer: https://www.usebruno.com/'
--data-raw '{"email":"test@usebruno.com","password":"test"}'
`;
const result = curlToJson(curlCommand);
expect(result).toEqual({
url: 'https://www.usebruno.com',
raw_url: 'https://www.usebruno.com',
method: 'post',
headers: {
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8',
'Content-Type': 'application/json;charset=utf-8',
Origin: 'https://www.usebruno.com',
Referer: 'https://www.usebruno.com/'
},
data: '{"email":"test@usebruno.com","password":"test"}'
});
});
});

View File

@ -0,0 +1,67 @@
import { forOwn } from 'lodash';
import { safeStringifyJSON } from 'utils/common';
import curlToJson from './curl-to-json';
export const getRequestFromCurlCommand = (curlCommand) => {
const parseFormData = (parsedBody) => {
const formData = [];
forOwn(parsedBody, (value, key) => {
formData.push({ name: key, value, enabled: true });
});
return formData;
};
try {
if (!curlCommand || typeof curlCommand !== 'string' || curlCommand.length === 0) {
return null;
}
const request = curlToJson(curlCommand);
const parsedHeaders = request?.headers;
const headers =
parsedHeaders &&
Object.keys(parsedHeaders).map((key) => ({ name: key, value: parsedHeaders[key], enabled: true }));
const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value;
const body = {
mode: 'none',
json: null,
text: null,
xml: null,
sparql: null,
multipartForm: null,
formUrlEncoded: null
};
const parsedBody = request.data;
if (parsedBody && contentType && typeof contentType === 'string') {
if (contentType.includes('application/json')) {
body.mode = 'json';
body.json = safeStringifyJSON(parsedBody);
} else if (contentType.includes('text/xml')) {
body.mode = 'xml';
body.xml = parsedBody;
} else if (contentType.includes('application/x-www-form-urlencoded')) {
body.mode = 'formUrlEncoded';
console.log(parsedBody);
console.log(parseFormData(parsedBody));
body.formUrlEncoded = parseFormData(parsedBody);
} else if (contentType.includes('multipart/form-data')) {
body.mode = 'multipartForm';
body.multipartForm = parsedBody;
} else if (contentType.includes('text/plain')) {
body.mode = 'text';
body.text = parsedBody;
}
}
return {
url: request.url,
method: request.method,
body,
headers: headers
};
} catch (error) {
console.error(error);
return null;
}
};

View File

@ -0,0 +1,238 @@
/**
* Copyright (c) 2014-2016 Nick Carneiro
* https://github.com/curlconverter/curlconverter
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import * as cookie from 'cookie';
import * as URL from 'url';
import * as querystring from 'query-string';
import yargs from 'yargs-parser';
const parseCurlCommand = (curlCommand) => {
// Remove newlines (and from continuations)
curlCommand = curlCommand.replace(/\\\r|\\\n/g, '');
// Remove extra whitespace
curlCommand = curlCommand.replace(/\s+/g, ' ');
// yargs parses -XPOST as separate arguments. just prescreen for it.
curlCommand = curlCommand.replace(/ -XPOST/, ' -X POST');
curlCommand = curlCommand.replace(/ -XGET/, ' -X GET');
curlCommand = curlCommand.replace(/ -XPUT/, ' -X PUT');
curlCommand = curlCommand.replace(/ -XPATCH/, ' -X PATCH');
curlCommand = curlCommand.replace(/ -XDELETE/, ' -X DELETE');
curlCommand = curlCommand.replace(/ -XOPTIONS/, ' -X OPTIONS');
// Safari adds `-Xnull` if is unable to determine the request type, it can be ignored
curlCommand = curlCommand.replace(/ -Xnull/, ' ');
curlCommand = curlCommand.trim();
const parsedArguments = yargs(curlCommand, {
boolean: ['I', 'head', 'compressed', 'L', 'k', 'silent', 's'],
alias: {
H: 'header',
A: 'user-agent'
}
});
let cookieString;
let cookies;
let url = parsedArguments._[1] || '';
// remove surrounding quotes if present
if (url && url.length) {
url = url.replace(/^['"]|['"]$/g, '');
}
// if url argument wasn't where we expected it, try to find it in the other arguments
if (!url) {
for (const argName in parsedArguments) {
if (typeof parsedArguments[argName] === 'string') {
if (parsedArguments[argName].indexOf('http') === 0 || parsedArguments[argName].indexOf('www.') === 0) {
url = parsedArguments[argName];
}
}
}
}
let headers;
if (parsedArguments.header) {
if (!headers) {
headers = {};
}
if (!Array.isArray(parsedArguments.header)) {
parsedArguments.header = [parsedArguments.header];
}
parsedArguments.header.forEach((header) => {
if (header.indexOf('Cookie') !== -1) {
cookieString = header;
} else {
const components = header.split(/:(.*)/);
if (components[1]) {
headers[components[0]] = components[1].trim();
}
}
});
}
if (parsedArguments['user-agent']) {
if (!headers) {
headers = {};
}
headers['User-Agent'] = parsedArguments['user-agent'];
}
if (parsedArguments.b) {
cookieString = parsedArguments.b;
}
if (parsedArguments.cookie) {
cookieString = parsedArguments.cookie;
}
let multipartUploads;
if (parsedArguments.F) {
multipartUploads = {};
if (!Array.isArray(parsedArguments.F)) {
parsedArguments.F = [parsedArguments.F];
}
parsedArguments.F.forEach((multipartArgument) => {
// input looks like key=value. value could be json or a file path prepended with an @
const splitArguments = multipartArgument.split('=', 2);
const key = splitArguments[0];
const value = splitArguments[1];
multipartUploads[key] = value;
});
}
if (cookieString) {
const cookieParseOptions = {
decode: function (s) {
return s;
}
};
// separate out cookie headers into separate data structure
// note: cookie is case insensitive
cookies = cookie.parse(cookieString.replace(/^Cookie: /gi, ''), cookieParseOptions);
}
let method;
if (parsedArguments.X === 'POST') {
method = 'post';
} else if (parsedArguments.X === 'PUT' || parsedArguments.T) {
method = 'put';
} else if (parsedArguments.X === 'PATCH') {
method = 'patch';
} else if (parsedArguments.X === 'DELETE') {
method = 'delete';
} else if (parsedArguments.X === 'OPTIONS') {
method = 'options';
} else if (
(parsedArguments.d ||
parsedArguments.data ||
parsedArguments['data-ascii'] ||
parsedArguments['data-binary'] ||
parsedArguments['data-raw'] ||
parsedArguments.F ||
parsedArguments.form) &&
!(parsedArguments.G || parsedArguments.get)
) {
method = 'post';
} else if (parsedArguments.I || parsedArguments.head) {
method = 'head';
} else {
method = 'get';
}
const compressed = !!parsedArguments.compressed;
const urlObject = URL.parse(url || '');
// if GET request with data, convert data to query string
// NB: the -G flag does not change the http verb. It just moves the data into the url.
if (parsedArguments.G || parsedArguments.get) {
urlObject.query = urlObject.query ? urlObject.query : '';
const option = 'd' in parsedArguments ? 'd' : 'data' in parsedArguments ? 'data' : null;
if (option) {
let urlQueryString = '';
if (url.indexOf('?') < 0) {
url += '?';
} else {
urlQueryString += '&';
}
if (typeof parsedArguments[option] === 'object') {
urlQueryString += parsedArguments[option].join('&');
} else {
urlQueryString += parsedArguments[option];
}
urlObject.query += urlQueryString;
url += urlQueryString;
delete parsedArguments[option];
}
}
if (urlObject.query && urlObject.query.endsWith('&')) {
urlObject.query = urlObject.query.slice(0, -1);
}
const query = querystring.parse(urlObject.query, { sort: false });
for (const param in query) {
if (query[param] === null) {
query[param] = '';
}
}
urlObject.search = null; // Clean out the search/query portion.
const request = {
url: url,
urlWithoutQuery: URL.format(urlObject)
};
if (compressed) {
request.compressed = true;
}
if (Object.keys(query).length > 0) {
request.query = query;
}
if (headers) {
request.headers = headers;
}
request.method = method;
if (cookies) {
request.cookies = cookies;
request.cookieString = cookieString.replace('Cookie: ', '');
}
if (multipartUploads) {
request.multipartUploads = multipartUploads;
}
if (parsedArguments.data) {
request.data = parsedArguments.data;
} else if (parsedArguments['data-binary']) {
request.data = parsedArguments['data-binary'];
request.isDataBinary = true;
} else if (parsedArguments.d) {
request.data = parsedArguments.d;
} else if (parsedArguments['data-ascii']) {
request.data = parsedArguments['data-ascii'];
} else if (parsedArguments['data-raw']) {
request.data = parsedArguments['data-raw'];
request.isDataRaw = true;
}
if (parsedArguments.u) {
request.auth = parsedArguments.u;
}
if (parsedArguments.user) {
request.auth = parsedArguments.user;
}
if (Array.isArray(request.data)) {
request.dataArray = request.data;
request.data = request.data.join('&');
}
if (parsedArguments.k || parsedArguments.insecure) {
request.insecure = true;
}
return request;
};
export default parseCurlCommand;

View File

@ -1,3 +1,4 @@
import jsyaml from 'js-yaml';
import each from 'lodash/each';
import get from 'lodash/get';
import fileDialog from 'file-dialog';
@ -8,7 +9,22 @@ import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } fr
const readFile = (files) => {
return new Promise((resolve, reject) => {
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.readAsText(files[0]);
});
@ -64,7 +80,8 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
auth: {
mode: 'none',
basic: null,
bearer: null
bearer: null,
digest: null
},
headers: [],
params: [],
@ -167,7 +184,7 @@ const parseInsomniaCollection = (data) => {
return new Promise((resolve, reject) => {
try {
const insomniaExport = JSON.parse(data);
const insomniaExport = data;
const insomniaResources = get(insomniaExport, 'resources', []);
const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace');
@ -213,7 +230,7 @@ const parseInsomniaCollection = (data) => {
const importCollection = () => {
return new Promise((resolve, reject) => {
fileDialog({ accept: 'application/json' })
fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' })
.then(readFile)
.then(parseInsomniaCollection)
.then(transformItemsInCollection)
@ -221,8 +238,8 @@ const importCollection = () => {
.then(validateSchema)
.then((collection) => resolve(collection))
.catch((err) => {
console.log(err);
reject(new BrunoError('Import collection failed'));
console.error(err);
reject(new BrunoError('Import collection failed: ' + err.message));
});
});
};

View File

@ -0,0 +1,388 @@
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,
digest: 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, visitedItems = new Set()) => {
if (!spec || typeof spec !== 'object') {
return spec;
}
if (Array.isArray(spec)) {
return spec.map((item) => resolveRefs(item, components, visitedItems));
}
if ('$ref' in spec) {
const refPath = spec.$ref;
if (visitedItems.has(refPath)) {
return spec;
} else {
visitedItems.add(refPath);
}
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, visitedItems);
} 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, visitedItems);
}
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 defaultSchemes = apiSpec.security || [];
let securitySchemes = get(apiSpec, 'components.securitySchemes', {});
if (Object.keys(securitySchemes) === 0) {
return {
supported: []
};
}
return {
supported: defaultSchemes.map((scheme) => {
var schemeName = Object.keys(scheme)[0];
return securitySchemes[schemeName];
}),
schemes: securitySchemes,
getScheme: (schemeName) => {
return securitySchemes[schemeName];
}
};
};
const parseOpenApiCollection = (data) => {
const brunoCollection = {
name: '',
uid: uuid(),
version: '1',
items: [],
environments: []
};
return new Promise((resolve, reject) => {
try {
const collectionData = resolveRefs(data);
if (!collectionData) {
reject(new BrunoError('Invalid OpenAPI collection. Failed to resolve refs.'));
return;
}
// Currently parsing of openapi spec is "do your best", that is
// allows "invalid" openapi spec
// assumes v3 if not defined. v2 no supported yet
if (collectionData.openapi && !collectionData.openapi.startsWith('3')) {
reject(new BrunoError('Only OpenAPI v3 is supported currently.'));
return;
}
// TODO what if info.title not defined?
brunoCollection.name = collectionData.info.title;
let servers = collectionData.servers || [];
let baseUrl = servers[0] ? getDefaultUrl(servers[0]) : '';
let securityConfig = getSecurity(collectionData);
let allRequests = Object.entries(collectionData.paths)
.map(([path, methods]) => {
return Object.entries(methods)
.filter(([method, op]) => {
return ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].includes(
method.toLowerCase()
);
})
.map(([method, operationObject]) => {
return {
method: method,
path: path,
operationObject: operationObject,
global: {
server: baseUrl,
security: securityConfig
}
};
});
})
.reduce((acc, val) => acc.concat(val), []); // flatten
let [groups, ungroupedRequests] = groupRequestsByTags(allRequests);
let brunoFolders = groups.map((group) => {
return {
uid: uuid(),
name: group.name,
type: 'folder',
items: group.requests.map(transformOpenapiRequestItem)
};
});
let ungroupedItems = ungroupedRequests.map(transformOpenapiRequestItem);
let brunoCollectionItems = brunoFolders.concat(ungroupedItems);
brunoCollection.items = brunoCollectionItems;
resolve(brunoCollection);
} catch (err) {
console.error(err);
reject(new BrunoError('An error occurred while parsing the OpenAPI collection'));
}
});
};
const importCollection = () => {
return new Promise((resolve, reject) => {
fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' })
.then(readFile)
.then(parseOpenApiCollection)
.then(transformItemsInCollection)
.then(hydrateSeqInCollection)
.then(validateSchema)
.then((collection) => resolve(collection))
.catch((err) => {
console.error(err);
reject(new BrunoError('Import collection failed: ' + err.message));
});
});
};
export default importCollection;

View File

@ -14,11 +14,46 @@ const readFile = (files) => {
});
};
const parseGraphQLRequest = (graphqlSource) => {
try {
let queryResultObject = {
query: '',
variables: ''
};
if (typeof graphqlSource === 'string') {
graphqlSource = JSON.parse(text);
}
if (graphqlSource.hasOwnProperty('variables') && graphqlSource.variables !== '') {
queryResultObject.variables = graphqlSource.variables;
}
if (graphqlSource.hasOwnProperty('query') && graphqlSource.query !== '') {
queryResultObject.query = graphqlSource.query;
}
return queryResultObject;
} catch (e) {
return {
query: '',
variables: ''
};
}
};
const isItemAFolder = (item) => {
return !item.request;
};
const importPostmanV2CollectionItem = (brunoParent, item) => {
const convertV21Auth = (array) => {
return array.reduce((accumulator, currentValue) => {
accumulator[currentValue.key] = currentValue.value;
return accumulator;
}, {});
};
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
brunoParent.items = brunoParent.items || [];
each(item, (i) => {
@ -31,7 +66,7 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
};
brunoParent.items.push(brunoFolderItem);
if (i.item && i.item.length) {
importPostmanV2CollectionItem(brunoFolderItem, i.item);
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth);
}
} else {
if (i.request) {
@ -49,6 +84,12 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
request: {
url: url,
method: i.request.method,
auth: {
mode: 'none',
basic: null,
bearer: null,
awsv4: null
},
headers: [],
params: [],
body: {
@ -62,6 +103,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)) {
brunoRequestItem.request.script.req = event.script.exec.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)) {
brunoRequestItem.request.tests = event.script.exec.map((line) => `// ${line}`).join('\n');
} else {
brunoRequestItem.request.tests = `// ${event.script.exec[0]} `;
}
}
});
}
const bodyMode = get(i, 'request.body.mode');
if (bodyMode) {
if (bodyMode === 'formdata') {
@ -108,6 +174,12 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
}
}
if (bodyMode === 'graphql') {
brunoRequestItem.type = 'graphql-request';
brunoRequestItem.request.body.mode = 'graphql';
brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql);
}
each(i.request.header, (header) => {
brunoRequestItem.request.headers.push({
uid: uuid(),
@ -118,6 +190,36 @@ const importPostmanV2CollectionItem = (brunoParent, item) => {
});
});
const auth = i.request.auth ?? parentAuth;
if (auth?.[auth.type] && auth.type !== 'noauth') {
let authValues = auth[auth.type];
if (Array.isArray(authValues)) {
authValues = convertV21Auth(authValues);
}
if (auth.type === 'basic') {
brunoRequestItem.request.auth.mode = 'basic';
brunoRequestItem.request.auth.basic = {
username: authValues.username,
password: authValues.password
};
} else if (auth.type === 'bearer') {
brunoRequestItem.request.auth.mode = 'bearer';
brunoRequestItem.request.auth.bearer = {
token: authValues.token
};
} else if (auth.type === 'awsv4') {
brunoRequestItem.request.auth.mode = 'awsv4';
brunoRequestItem.request.auth.awsv4 = {
accessKeyId: authValues.accessKey,
secretAccessKey: authValues.secretKey,
sessionToken: authValues.sessionToken,
service: authValues.service,
region: authValues.region,
profileName: ''
};
}
}
each(get(i, 'request.url.query'), (param) => {
brunoRequestItem.request.params.push({
uid: uuid(),
@ -158,7 +260,7 @@ const importPostmanV2Collection = (collection) => {
environments: []
};
importPostmanV2CollectionItem(brunoCollection, collection.item);
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth);
return brunoCollection;
};

View File

@ -1,5 +1,13 @@
# Changelog
## 1.1.0
- Upgraded axios to 1.5.1
## 1.0.0
- Announcing Stable Release
## 0.13.0
- feat(#306) Module whitelisting and filesystem access support

View File

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

View File

@ -3,7 +3,7 @@ const qs = require('qs');
const chalk = require('chalk');
const decomment = require('decomment');
const fs = require('fs');
const { forOwn, each, extend, get, compact } = require('lodash');
const { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lodash');
const FormData = require('form-data');
const prepareRequest = require('./prepare-request');
const interpolateVars = require('./interpolate-vars');
@ -12,11 +12,10 @@ const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime } = require('@use
const { stripExtension } = require('../utils/filesystem');
const { getOptions } = require('../utils/bru');
const https = require('https');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { HttpProxyAgent } = require('http-proxy-agent');
const { SocksProxyAgent } = require('socks-proxy-agent');
const { makeAxiosInstance } = require('../utils/axios-instance');
const { shouldUseProxy } = require('../utils/proxy-util');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
const runSingleRequest = async function (
filename,
@ -137,14 +136,15 @@ const runSingleRequest = async function (
const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
const socksEnabled = proxyProtocol.includes('socks');
let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;
let proxyUri;
if (proxyAuthEnabled) {
const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;
} else {
proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
}
if (socksEnabled) {
@ -152,7 +152,7 @@ const runSingleRequest = async function (
request.httpsAgent = socksProxyAgent;
request.httpAgent = socksProxyAgent;
} else {
request.httpsAgent = new HttpsProxyAgent(
request.httpsAgent = new PatchedHttpsProxyAgent(
proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);

View File

@ -1,4 +1,6 @@
const parseUrl = require('url').parse;
const { isEmpty } = require('lodash');
const { HttpsProxyAgent } = require('https-proxy-agent');
const DEFAULT_PORTS = {
ftp: 21,
@ -9,7 +11,7 @@ const DEFAULT_PORTS = {
wss: 443
};
/**
* check for proxy bypass, Copied form 'proxy-from-env'
* check for proxy bypass, copied form 'proxy-from-env'
*/
const shouldUseProxy = (url, proxyBypass) => {
if (proxyBypass === '*') {
@ -39,7 +41,6 @@ const shouldUseProxy = (url, proxyBypass) => {
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;
@ -61,6 +62,24 @@ const shouldUseProxy = (url, proxyBypass) => {
});
};
/**
* Patched version of HttpsProxyAgent to get around a bug that ignores
* options like ca and rejectUnauthorized when upgrading the socket to TLS:
* https://github.com/TooTallNate/proxy-agents/issues/194
*/
class PatchedHttpsProxyAgent extends HttpsProxyAgent {
constructor(proxy, opts) {
super(proxy, opts);
this.constructorOpts = opts;
}
async connect(req, opts) {
const combinedOpts = { ...this.constructorOpts, ...opts };
return super.connect(req, combinedOpts);
}
}
module.exports = {
shouldUseProxy
shouldUseProxy,
PatchedHttpsProxyAgent
};

View File

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

View File

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

View File

@ -14,16 +14,18 @@ const { loadWindowState, saveBounds, saveMaximized } = require('./utils/window')
const lastOpenedCollections = new LastOpenedCollections();
// Reference: https://content-security-policy.com/
const contentSecurityPolicy = [
isDev ? "default-src 'self' 'unsafe-inline' 'unsafe-eval'" : "default-src 'self'",
"connect-src 'self' https://api.github.com/repos/usebruno/bruno",
"font-src 'self' https://fonts.gstatic.com",
"default-src 'self'",
"script-src * 'unsafe-inline' 'unsafe-eval'",
"connect-src 'self' api.github.com",
"font-src 'self' https:",
"form-action 'none'",
"img-src 'self' blob: data:",
"style-src 'self' https://fonts.googleapis.com"
"img-src 'self' blob: data: https:",
"style-src 'self' 'unsafe-inline' https:"
];
setContentSecurityPolicy(contentSecurityPolicy.join(';'));
setContentSecurityPolicy(contentSecurityPolicy.join(';') + ';');
const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu);
@ -67,7 +69,21 @@ app.on('ready', async () => {
slashes: true
});
mainWindow.loadURL(url);
mainWindow.loadURL(url).catch((reason) => {
console.error(`Error: Failed to load URL: "${url}" (Electron shows a blank screen because of this).`);
console.error('Original message:', reason);
if (isDev) {
console.error(
'Could not connect to Next.Js dev server, is it running?' +
' Start the dev server using "npm run dev:web" and restart electron'
);
} else {
console.error(
'If you are using an official production build: the above error is most likely a bug! ' +
' Please report this under: https://github.com/usebruno/bruno/issues'
);
}
});
watcher = new Watcher();
const handleBoundsChange = () => {

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