Compare commits

..

1 Commits

Author SHA1 Message Date
ramki-bruno
5af8034c74 Added Playwright-codegen setup 2025-04-01 17:58:35 +05:30
456 changed files with 5882 additions and 32209 deletions

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @helloanoop @maintainer-bruno @lohit-bruno @naman-bruno

View File

@@ -27,9 +27,7 @@ body:
required: false
- label: annoying
required: false
- label: this feature was working in a previous version but is broken in the current release.
required: false
- type: input
attributes:
label: Bruno version

View File

@@ -28,11 +28,6 @@ jobs:
npm run build --workspace=packages/bruno-common
npm run build --workspace=packages/bruno-query
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build --workspace=packages/bruno-converters
npm run build --workspace=packages/bruno-requests
- name: Lint Check
run: npm run lint
# tests
- name: Test Package bruno-js
@@ -50,8 +45,6 @@ jobs:
run: npm run test --workspace=packages/bruno-app
- name: Test Package bruno-common
run: npm run test --workspace=packages/bruno-common
- name: Test Package bruno-converters
run: npm run test --workspace=packages/bruno-converters
- name: Test Package bruno-electron
run: npm run test --workspace=packages/bruno-electron
@@ -78,8 +71,6 @@ jobs:
npm run build --workspace=packages/bruno-query
npm run build --workspace=packages/bruno-common
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build --workspace=packages/bruno-converters
npm run build --workspace=packages/bruno-requests
- name: Run tests
run: |
@@ -91,47 +82,5 @@ jobs:
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
check_name: CLI Test Results
files: packages/bruno-tests/collection/junit.xml
comment_mode: always
e2e-test:
name: Playwright E2E Tests
timeout-minutes: 60
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: v22.11.x
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get --no-install-recommends install -y \
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
xvfb
npm ci --legacy-peer-deps
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
- name: Install dependencies for test collection environment
run: |
npm ci --prefix packages/bruno-tests/collection
- name: Build libraries
run: |
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
npm run build:bruno-converters
npm run build:bruno-requests
- name: Run Playwright tests
run: |
xvfb-run npm run test:e2e
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

6
.gitignore vendored
View File

@@ -46,8 +46,4 @@ yarn-error.log*
#dev editor
bruno.iml
.idea
.vscode
# Playwright
/blob-report/
.idea

View File

@@ -15,15 +15,15 @@
| [正體中文](docs/contributing/contributing_zhtw.md)
| [日本語](docs/contributing/contributing_ja.md)
| [हिंदी](docs/contributing/contributing_hi.md)
| [Dutch](docs/contributing/contributing_nl.md)
| [Nederlands](docs/contributing/contributing_nl.md)
## Let's make Bruno better, together!!
We are happy that you are looking to improve Bruno. Below are the guidelines to run 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 React and Electron.
Bruno is built using Next.js and React. We also use electron to ship a desktop version (that supports local collections)
Libraries we use
@@ -37,76 +37,38 @@ Libraries we use
- Filesystem Watcher - chokidar
- i18n - i18next
> [!IMPORTANT]
> You would need [Node v22.x or the latest LTS version](https://nodejs.org/en/). We use npm workspaces in the project
### Dependencies
You would need [Node v20.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
## Development
Bruno is a desktop app. Below are the instructions to run Bruno.
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.
> Note: We use React for the frontend and rsbuild for build and dev server.
## Install Dependencies
### Local Development
```bash
# use nodejs 22 version
# use nodejs 20 version
nvm use
# install deps
npm i --legacy-peer-deps
```
### Local Development
#### Build packages
##### Option 1
```bash
# build packages
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
# bundle js sandbox libraries
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
```
##### Option 2
```bash
# install dependencies and setup
npm run setup
```
#### Run the app
##### Option 1
```bash
# run react app (terminal 1)
# run next app (terminal 1)
npm run dev:web
# run electron app (terminal 2)
npm run dev:electron
```
##### Option 2
```bash
# run electron and react app concurrently
npm run dev
```
#### Customize Electron `userData` path
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
e.g.
```sh
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
```
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
### 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.
@@ -125,28 +87,7 @@ find . -type f -name "package-lock.json" -delete
```bash
# run bruno-schema tests
npm run test --workspace=packages/bruno-schema
# run bruno-query tests
npm run test --workspace=packages/bruno-query
# run bruno-common tests
npm run test --workspace=packages/bruno-common
# run bruno-converters tests
npm run test --workspace=packages/bruno-converters
# run bruno-app tests
npm run test --workspace=packages/bruno-app
# run bruno-electron tests
npm run test --workspace=packages/bruno-electron
# run bruno-lang tests
npm run test --workspace=packages/bruno-lang
# run bruno-toml tests
npm run test --workspace=packages/bruno-toml
npm test --workspace=packages/bruno-schema
# run tests over all workspaces
npm test --workspaces --if-present

View File

@@ -21,7 +21,7 @@ Bibliotheken die wir benutzen
### Abhängigkeiten
Du benötigst [Node v22.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
Du benötigst [Node v20.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
### Lass uns coden
@@ -42,12 +42,12 @@ Bruno wird als Desktop-Anwendung entwickelt. Um die App zu starten, musst Du zue
### Abhängigkeiten
- NodeJS v22
- NodeJS v18
### Lokales Entwickeln
```bash
# use nodejs 22 version
# use nodejs 18 version
nvm use
# install deps

View File

@@ -1,4 +1,4 @@
[Inglés](../../contributing.md)
[English](../../contributing.md)
## ¡Juntos, hagamos a Bruno mejor!
@@ -6,111 +6,58 @@ Estamos encantados de que quieras ayudar a mejorar Bruno. A continuación encont
### Tecnologías utilizadas
Bruno está construido con React y Electron
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 - TailwindCSS
- Editores de código - CodeMirror
- 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
- i18n (internacionalización) - i18next
### Dependencias
> [!IMPORTANT]
> Necesitarás [Node v22.x o la última versión LTS](https://nodejs.org/es/). Ten en cuenta que Bruno usa los espacios de trabajo de npm
Necesitarás [Node v20.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 es una aplicación de escritorio. A continuación se detallan las instrucciones paso a paso para ejecutar Bruno.
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.
> Nota: Utilizamos React para el frontend y rsbuild para el servidor de desarrollo.
### Dependencias
### Instalar dependencias
```bash
# Use la versión 22.x o LTS (Soporte a Largo Plazo) de Node.js
nvm use 22.11.0
# instalar las dependencias
npm i --legacy-peer-deps
```
> ¿Por qué `--legacy-peer-deps`?: Fuerza la instalación ignorando conflictos en dependencias “peer”, evitando errores de árbol de dependencias.
- NodeJS v18
### Desarrollo local
#### Construir paquetes
##### Opción 1
```bash
# construir paquetes
# 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
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
# empaquetar bibliotecas JavaScript del entorno de pruebas aislado
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
```
##### Opción 2
```bash
# instalar dependencias y configurar el entorno
npm run setup
```
#### Ejecutar la aplicación
```bash
# ejecutar aplicación react (terminal 1)
# Ejecuta la aplicación de nextjs (terminal 1)
npm run dev:web
# ejecutar aplicación electron (terminal 2)
# Ejecuta la aplicación de electron (terminal 2)
npm run dev:electron
```
##### Opción 1
```bash
# ejecutar aplicación react (terminal 1)
npm run dev:web
# ejecutar aplicación electron (terminal 2)
npm run dev:electron
```
##### Opción 2
```bash
# ejecutar aplicación electron y react de forma concurrente
npm run dev
```
#### Personalizar la ruta `userData` de Electron
Si la variable de entorno `ELECTRON_USER_DATA_PATH` está presente y se encuentra en modo de desarrollo, entonces la ruta `userData` se modifica en consecuencia.
ejemplo:
```sh
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
```
Esto creará una carpeta llamada `bruno-test` en tu escritorio y la usará como la ruta userData.
### Solución de problemas
Es posible que te encuentres con un error `Unsupported platform` cuando ejecutes `npm install`. Para solucionarlo, tendrás que eliminar las carpetas `node_modules` y el archivo `package-lock.json`, y luego volver a ejecutar `npm install`. Esto debería instalar todos los paquetes necesarios para que la aplicación funcione.
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.
```sh
```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"
@@ -122,42 +69,10 @@ find . -type f -name "package-lock.json" -delete
### Pruebas
#### Pruebas individuales
```bash
# ejecutar pruebas de bruno-app
npm run test --workspace=packages/bruno-app
# ejecutar pruebas de esquema bruno
npm test --workspace=packages/bruno-schema
# ejecutar pruebas de bruno-electron
npm run test --workspace=packages/bruno-electron
# ejecutar pruebas de bruno-cli
npm run test --workspace=packages/bruno-cli
# ejecutar pruebas de bruno-common
npm run test --workspace=packages/bruno-common
# ejecutar pruebas de bruno-converters
npm run test --workspace=packages/bruno-converters
# ejecutar pruebas de bruno-schema
npm run test --workspace=packages/bruno-schema
# ejecutar pruebas de bruno-query
npm run test --workspace=packages/bruno-query
# ejecutar pruebas de bruno-js
npm run test --workspace=packages/bruno-js
# ejecutar pruebas de bruno-lang
npm run test --workspace=packages/bruno-lang
# ejecutar pruebas de bruno-toml
npm run test --workspace=packages/bruno-toml
```
#### Pruebas en conjunto
```bash
# ejecutar pruebas en todos los espacios de trabajo
npm test --workspaces --if-present
```

View File

@@ -40,8 +40,6 @@ npm i --legacy-peer-deps
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
# Next.js ऐप चलाएँ (टर्मिनल 1 पर)
npm run dev:web

View File

@@ -40,8 +40,6 @@ npm i --legacy-peer-deps
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
# run next app (terminal 1)
npm run dev:web

View File

@@ -40,8 +40,6 @@ npm i --legacy-peer-deps
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
# next 앱 실행 (1번 터미널)
npm run dev:web

View File

@@ -40,8 +40,6 @@ npm i --legacy-peer-deps
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
# draai next app (terminal 1)
npm run dev:web

View File

@@ -42,8 +42,6 @@ npm i --legacy-peer-deps
npm run build:graphql-docs
npm run build:bruno-query
npm run build:bruno-common
npm run build:bruno-converters
npm run build:bruno-requests
# spustite ďalšiu aplikáciu (terminál 1)
npm run dev:web

View File

@@ -74,11 +74,12 @@ flatpak install com.usebruno.Bruno
# على نظام Linux عبر Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
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
```
### التشغيل عبر منصات متعددة 🖥️

View File

@@ -59,11 +59,12 @@ snap install bruno
# Apt এর মাধ্যমে লিনাক্সে
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
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
```
### একাধিক প্ল্যাটফর্মে চালান 🖥️

View File

@@ -63,11 +63,12 @@ snap install bruno
# 在 Linux 上用 Apt 安装
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
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
```
### 在 Mac 上通过 Homebrew 安装 🖥️

View File

@@ -78,11 +78,12 @@ flatpak install com.usebruno.Bruno
# Auf Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
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
```
### Einsatz auf verschiedensten Plattformen 🖥️

View File

@@ -75,11 +75,12 @@ flatpak install com.usebruno.Bruno
# En Linux con Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
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
```
### Ejecútalo en múltiples plataformas 🖥️

View File

@@ -63,11 +63,12 @@ snap install bruno
# Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
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
```
### Fonctionne sur de multiples plateformes 🖥️

View File

@@ -1,151 +0,0 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### ब्रूनो - API इंटरफेस (API) का अन्वेषण और परीक्षण करने के लिए एक ओपन-सोर्स विकास वातावरण।
[![GitHub संस्करण](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![कमिट गतिविधि](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)
[![वेबसाइट](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![डाउनलोड](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](./readme_fr.md)
| [Português (BR)](./readme_pt_br.md)
| [한국어](./readme_kr.md)
| [বাংলা](./readme_bn.md)
| [Español](./readme_es.md)
| [Italiano](./readme_it.md)
| [Română](./readme_ro.md)
| [Polski](./readme_pl.md)
| [简体中文](./readme_cn.md)
| [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md)
| [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
| **हिन्दी**
ब्रूनो एक नया और अभिनव API क्लाइंट है, जिसका उद्देश्य Postman और अन्य समान उपकरणों द्वारा प्रस्तुत स्थिति को बदलना है।
ब्रूनो आपकी कलेक्शनों को सीधे आपकी फाइल सिस्टम के एक फ़ोल्डर में संग्रहीत करता है। हम API अनुरोधों के बारे में जानकारी सहेजने के लिए एक सामान्य टेक्स्ट मार्कअप भाषा, Bru, का उपयोग करते हैं।
आप अपनी API कलेक्शनों पर सहयोग करने के लिए Git या अपनी पसंद के किसी भी संस्करण नियंत्रण प्रणाली का उपयोग कर सकते हैं।
ब्रूनो केवल ऑफ़लाइन उपयोग के लिए है। ब्रूनो में कभी भी क्लाउड-सिंक जोड़ने की कोई योजना नहीं है। हम आपके डेटा की गोपनीयता को महत्व देते हैं और मानते हैं कि इसे आपके डिवाइस पर ही रहना चाहिए। हमारी दीर्घकालिक दृष्टि [यहाँ](https://github.com/usebruno/bruno/discussions/269) पढ़ें।
📢 हमारे हालिया India FOSS 3.0 सम्मेलन में हमारे वार्तालाप को [यहाँ](https://www.youtube.com/watch?v=7bSMFpbcPiY) देखें।
![bruno](/assets/images/landing-2.png) <br /><br />
### गोल्डन संस्करण ✨
हमारी अधिकांश सुविधाएँ मुफ्त और ओपन-सोर्स हैं।
हम [पारदर्शिता और स्थिरता के सिद्धांतों](https://github.com/usebruno/bruno/discussions/269) के बीच एक सामंजस्यपूर्ण संतुलन प्राप्त करने का प्रयास करते हैं।
[गोल्डन संस्करण](https://www.usebruno.com/pricing) के लिए खरीदारी जल्द ही $9 की कीमत पर उपलब्ध होगी! <br/>
[यहाँ सदस्यता लें](https://usebruno.ck.page/4c65576bd4) ताकि आपको लॉन्च पर सूचनाएं मिलें।
### स्थापना
ब्रूनो Mac, Windows और Linux के लिए हमारे [वेबसाइट](https://www.usebruno.com/downloads) पर एक बाइनरी डाउनलोड के रूप में उपलब्ध है।
आप ब्रूनो को Homebrew, Chocolatey, Scoop, Snap, Flatpak और Apt जैसे पैकेज प्रबंधकों के माध्यम से भी स्थापित कर सकते हैं।
```sh
# Mac पर Homebrew के माध्यम से
brew install bruno
# Windows पर Chocolatey के माध्यम से
choco install bruno
# Windows पर Scoop के माध्यम से
scoop bucket add extras
scoop install bruno
# Linux पर Snap के माध्यम से
snap install bruno
# Linux पर Flatpak के माध्यम से
flatpak install com.usebruno.Bruno
# Linux पर 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
कई प्लेटफार्मों पर चलाएं 🖥️
<br /><br />
Git के माध्यम से सहयोग करें 👩‍💻🧑‍💻
या अपनी पसंद के किसी भी संस्करण नियंत्रण प्रणाली का उपयोग करें
<br /><br />
महत्वपूर्ण लिंक 📌
हमारी दीर्घकालिक दृष्टि
रोडमैप
प्रलेखन
Stack Overflow
वेबसाइट
मूल्य निर्धारण
डाउनलोड
GitHub प्रायोजक
प्रस्तुतियाँ 🎥
प्रशंसापत्र
ज्ञान केंद्र
Scriptmania
समर्थन ❤️
यदि आप ब्रूनो को पसंद करते हैं और हमारे ओपन-सोर्स कार्य का समर्थन करना चाहते हैं, तो कृपया GitHub प्रायोजक के माध्यम से हमें प्रायोजित करने पर विचार करें।
प्रशंसापत्र साझा करें 📣
यदि ब्रूनो ने आपके और आपकी टीमों के लिए काम में मदद की है, तो कृपया हमारे GitHub चर्चा में अपने प्रशंसापत्र साझा करना न भूलें
नए पैकेज प्रबंधकों में प्रकाशित करना
अधिक जानकारी के लिए कृपया यहाँ देखें।
हमसे संपर्क करें 🌐
𝕏 (ट्विटर) <br />
वेबसाइट <br />
डिस्कॉर्ड <br />
लिंक्डइन
ट्रेडमार्क
नाम
ब्रूनो एक ट्रेडमार्क है जो अनूप एम डी के स्वामित्व में है।
लोगो
लोगो OpenMoji से लिया गया है। लाइसेंस: CC BY-SA 4.0
योगदान 👩‍💻🧑‍💻
हमें खुशी है कि आप ब्रूनो को बेहतर बनाने में रुचि रखते हैं। कृपया योगदान गाइड देखें।
यदि आप सीधे कोड के माध्यम से योगदान नहीं कर सकते, तो भी कृपया बग्स की रिपोर्ट करने और उन सुविधाओं का अनुरोध करने में संकोच न करें जिन्हें आपकी स्थिति को हल करने के लिए लागू किया जाना चाहिए।
लेखक
<div align="center"> <a href="https://github.com/usebruno/bruno/graphs/contributors"> <img src="https://contrib.rocks/image?repo=usebruno/bruno" /> </a> </div>
लाइसेंस 📄
MIT

View File

@@ -59,11 +59,12 @@ snap install bruno
# Su Linux tramite Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
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 🖥️

View File

@@ -78,11 +78,12 @@ flatpak install com.usebruno.Bruno
# LinuxでAptを使ってインストール
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
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
```
### マルチプラットフォームでの実行に対応 🖥️

View File

@@ -59,11 +59,12 @@ snap install bruno
# On Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
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
```
### 여러 플랫폼에서 실행하세요. 🖥️

View File

@@ -69,11 +69,12 @@ flatpak install com.usebruno.Bruno
# On Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
sudo apt update
sudo apt install bruno
```
### Uruchom na wielu platformach 🖥️

View File

@@ -76,11 +76,12 @@ flatpak install com.usebruno.Bruno
# No Linux via Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
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 🖥️

View File

@@ -59,11 +59,12 @@ snap install bruno
# Pe Linux cu Apt
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
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
```
### Utilizați pe mai multe platforme 🖥️

View File

@@ -63,11 +63,12 @@ snap install bruno
# Apt aracılığıyla Linux'ta
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
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
```
### Birden fazla platformda çalıştırın 🖥️

View File

@@ -63,11 +63,12 @@ snap install bruno
# 在 Linux 上使用 Apt 安裝
sudo mkdir -p /etc/apt/keyrings
sudo apt update && sudo apt install gpg
sudo gpg --list-keys
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
echo "deb [arch=amd64 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
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
```
### 跨多個平台運行 🖥️

View File

@@ -1,5 +0,0 @@
import { test, expect } from '../../playwright';
test('Check if the logo on top left is visible', async ({ page }) => {
await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible();
});

View File

@@ -1,31 +0,0 @@
import { test, expect } from '../../playwright';
test('Create new collection and add a simple HTTP request', async ({ page, createTmpDir }) => {
await page.getByLabel('Create Collection').click();
await page.getByLabel('Name').click();
await page.getByLabel('Name').fill('test-collection');
await page.getByLabel('Name').press('Tab');
await page.getByLabel('Location').fill(await createTmpDir('test-collection'));
await page.getByRole('button', { name: 'Create', exact: true }).click();
await page.getByText('test-collection').click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
await page.locator('#create-new-tab').getByRole('img').click();
await page.getByPlaceholder('Request Name').fill('r1');
await page.getByPlaceholder('Request URL').click();
await page.getByPlaceholder('Request URL').fill('http://localhost:8081');
await page.getByRole('button', { name: 'Create' }).click();
await page.locator('pre').filter({ hasText: 'http://localhost:' }).click();
await page.locator('textarea').fill('/ping');
await page.locator('#send-request').getByRole('img').nth(2).click();
await expect(page.getByRole('main')).toContainText('200 OK');
await page.getByRole('tab', { name: 'GET r1' }).locator('circle').click();
await page.getByRole('button', { name: 'Save', exact: true }).click();
await page.getByText('GETr1').click();
await page.getByRole('button', { name: 'Clear response' }).click();
await page.locator('body').press('ControlOrMeta+Enter');
await expect(page.getByRole('main')).toContainText('200 OK');
});

View File

@@ -1,4 +0,0 @@
{
"maximized": true,
"lastOpenedCollections": ["{{projectRoot}}/packages/bruno-tests/collection"]
}

View File

@@ -1,49 +0,0 @@
import { test, expect } from '../../playwright';
test.describe.parallel('Run Testbench Requests', () => {
test('Run bruno-testbench in Developer Mode', async ({ pageWithUserData: page }) => {
test.setTimeout(2 * 60 * 1000);
await page.getByText('bruno-testbench').click();
await page.getByLabel('Developer Mode(use only if').check();
await page.getByRole('button', { name: 'Save' }).click();
await page.locator('.environment-selector').nth(1).click();
await page.locator('.dropdown-item').getByText('Prod').click();
await page.locator('.collection-actions').hover();
await page.locator('.collection-actions .icon').click();
await page.getByText('Run', { exact: true }).click();
await page.getByRole('button', { name: 'Run Collection' }).click();
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
const result = await page.getByText('Total Requests: ').innerText();
const [totalRequests, passed, failed, skipped] = result
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
.slice(1);
await expect(parseInt(failed)).toBe(0);
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed));
});
test.fixme('Run bruno-testbench in Safe Mode', async ({ pageWithUserData: page }) => {
test.setTimeout(2 * 60 * 1000);
await page.getByText('bruno-testbench').click();
await page.getByLabel('Safe Mode').check();
await page.getByRole('button', { name: 'Save' }).click();
await page.locator('.environment-selector').nth(1).click();
await page.locator('.dropdown-item').getByText('Prod').click();
await page.locator('.collection-actions').hover();
await page.locator('.collection-actions .icon').click();
await page.getByText('Run', { exact: true }).click();
await page.getByRole('button', { name: 'Run Collection' }).click();
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
const result = await page.getByText('Total Requests: ').innerText();
const [totalRequests, passed, failed, skipped] = result
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
.slice(1);
await expect(parseInt(failed)).toBe(0);
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed));
});
});

View File

@@ -1,27 +0,0 @@
import { test, expect } from '../../playwright';
test('Should verify all support links with correct URL in preference > Support tab', async ({ page }) => {
// Open Preferences
await page.getByLabel('Open Preferences').click();
// Verify Support tab
await page.getByRole('tab', { name: 'Support' }).click();
const locator_twitter = page.getByRole('link', { name: 'Twitter' });
expect(await locator_twitter.getAttribute('href')).toEqual('https://twitter.com/use_bruno');
const locator_github = page.getByRole('link', { name: 'GitHub', exact: true });
expect(await locator_github.getAttribute('href')).toEqual('https://github.com/usebruno/bruno');
const locator_discord = page.getByRole('link', { name: 'Discord', exact: true });
expect(await locator_discord.getAttribute('href')).toEqual('https://discord.com/invite/KgcZUncpjq');
const locator_reportissues = page.getByRole('link', { name: 'Report Issues', exact: true });
expect(await locator_reportissues.getAttribute('href')).toEqual('https://github.com/usebruno/bruno/issues');
const locator_documentation = page.getByRole('link', { name: 'Documentation', exact: true });
expect(await locator_documentation.getAttribute('href')).toEqual('https://docs.usebruno.com');
});

View File

@@ -1,54 +0,0 @@
// eslint.config.js
const { defineConfig } = require("eslint/config");
const globals = require("globals");
module.exports = defineConfig([
{
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
ignores: ["**/*.config.js"],
languageOptions: {
globals: {
...globals.browser,
...globals.jest,
global: false,
require: false,
Buffer: false,
process: false
},
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
rules: {
"no-undef": "error",
},
},
{
// It prevents lint errors when using CommonJS exports (module.exports) in Jest mocks.
files: ["packages/bruno-app/src/test-utils/mocks/codemirror.js"],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
},
rules: {
"no-undef": "error",
},
},
{
files: ["packages/bruno-electron/**/*.{js}"],
ignores: ["**/*.config.js"],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
},
rules: {
"no-undef": "error",
},
}
]);

5496
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,31 +6,24 @@
"packages/bruno-electron",
"packages/bruno-cli",
"packages/bruno-common",
"packages/bruno-converters",
"packages/bruno-schema",
"packages/bruno-query",
"packages/bruno-js",
"packages/bruno-lang",
"packages/bruno-tests",
"packages/bruno-toml",
"packages/bruno-graphql-docs",
"packages/bruno-requests"
"packages/bruno-graphql-docs"
],
"homepage": "https://usebruno.com",
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@jest/globals": "^29.2.0",
"@playwright/test": "^1.51.1",
"@playwright/test": "^1.27.1",
"@types/jest": "^29.5.11",
"@types/lodash-es": "^4.17.12",
"@types/node": "^22.14.1",
"concurrently": "^8.2.2",
"eslint": "^9.26.0",
"fs-extra": "^11.1.1",
"globals": "^16.1.0",
"husky": "^8.0.3",
"jest": "^29.2.0",
"lodash-es": "^4.17.21",
"playwright": "^1.51.1",
"pretty-quick": "^3.1.3",
"randomstring": "^1.2.2",
"rimraf": "^6.0.1",
@@ -38,17 +31,13 @@
},
"scripts": {
"setup": "node ./scripts/setup.js",
"watch:converters": "npm run watch --workspace=packages/bruno-converters",
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
"dev:watch": "node ./scripts/dev-hot-reload.js",
"dev:web": "npm run dev --workspace=packages/bruno-app",
"build:web": "npm run build --workspace=packages/bruno-app",
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
"build:bruno-requests": "npm run build --workspace=packages/bruno-requests",
"build:bruno-converters": "npm run build --workspace=packages/bruno-converters",
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
"build:electron": "node ./scripts/build-electron.js",
@@ -58,18 +47,13 @@
"build:electron:deb": "./scripts/build-electron.sh deb",
"build:electron:rpm": "./scripts/build-electron.sh rpm",
"build:electron:snap": "./scripts/build-electron.sh snap",
"watch:common": "npm run watch --workspace=packages/bruno-common",
"test:codegen": "node playwright/codegen.ts",
"test:e2e": "playwright test",
"test:codegen": "npm run dev:web & node ./scripts/playwright-codegen.js",
"test:e2e": "npx playwright test",
"test:report": "npx playwright show-report",
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
"lint": "node --max_old_space_size=4096 $(npx which eslint)"
"prepare": "husky install"
},
"overrides": {
"rollup": "3.29.5",
"electron-store": {
"conf": {
"json-schema-typed": "8.0.1"
}
}
"rollup": "3.29.5"
}
}
}

View File

@@ -1,4 +1,4 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"presets": ["@babel/preset-env"],
"plugins": [["styled-components", { "ssr": true }]]
}

View File

@@ -1,9 +0,0 @@
module.exports = {
presets: [
'@babel/preset-env',
['@babel/preset-react', {
runtime: 'automatic'
}]
],
plugins: ['babel-plugin-styled-components']
};

View File

@@ -1,11 +1,5 @@
module.exports = {
rootDir: '.',
transform: {
'^.+\\.[jt]sx?$': 'babel-jest',
},
transformIgnorePatterns: [
"/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/",
],
moduleNameMapper: {
'^assets/(.*)$': '<rootDir>/src/assets/$1',
'^components/(.*)$': '<rootDir>/src/components/$1',
@@ -14,17 +8,9 @@ module.exports = {
'^api/(.*)$': '<rootDir>/src/api/$1',
'^pageComponents/(.*)$': '<rootDir>/src/pageComponents/$1',
'^providers/(.*)$': '<rootDir>/src/providers/$1',
'^utils/(.*)$': '<rootDir>/src/utils/$1',
'^test-utils/(.*)$': '<rootDir>/src/test-utils/$1'
'^utils/(.*)$': '<rootDir>/src/utils/$1'
},
clearMocks: true,
moduleDirectories: ['node_modules', 'src'],
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@testing-library/jest-dom'],
setupFiles: [
'<rootDir>/jest.setup.js',
],
testMatch: [
'<rootDir>/src/**/*.spec.[jt]s?(x)'
]
};
testEnvironment: 'node'
};

View File

@@ -1,11 +0,0 @@
jest.mock('nanoid', () => {
return {
nanoid: () => {}
};
});
jest.mock('strip-json-comments', () => {
return {
stripJsonComments: (str) => str
};
});

View File

@@ -6,7 +6,6 @@
"baseUrl": "./",
"paths": {
"assets/*": ["src/assets/*"],
"ui/*": ["src/ui/*"],
"components/*": ["src/components/*"],
"hooks/*": ["src/hooks/*"],
"themes/*": ["src/themes/*"],

View File

@@ -1,7 +1,6 @@
{
"name": "@usebruno/app",
"version": "2.0.0",
"license": "MIT",
"private": true,
"scripts": {
"dev": "rsbuild dev",
@@ -12,6 +11,7 @@
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
},
"dependencies": {
"@babel/preset-env": "^7.26.0",
"@fontsource/inter": "^5.0.15",
"@prantlf/jsonlint": "^16.0.0",
"@reduxjs/toolkit": "^1.8.0",
@@ -73,37 +73,28 @@
"react-tooltip": "^5.5.2",
"sass": "^1.46.0",
"semver": "^7.7.1",
"shell-quote": "^1.8.3",
"strip-json-comments": "^5.0.1",
"styled-components": "^5.3.3",
"system": "^2.0.1",
"url": "^0.11.3",
"xml-formatter": "^3.5.0",
"yargs-parser": "^21.1.1",
"yup": "^0.32.11"
},
"devDependencies": {
"@babel/core": "^7.27.1",
"@babel/preset-env": "^7.27.2",
"@babel/preset-react": "^7.27.1",
"@rsbuild/core": "^1.1.2",
"@rsbuild/plugin-babel": "^1.0.3",
"@rsbuild/plugin-node-polyfill": "^1.2.0",
"@rsbuild/plugin-react": "^1.0.7",
"@rsbuild/plugin-sass": "^1.1.0",
"@rsbuild/plugin-styled-components": "1.1.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"autoprefixer": "10.4.20",
"babel-jest": "^29.7.0",
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
"babel-plugin-styled-components": "^2.1.4",
"cross-env": "^7.0.3",
"css-loader": "7.1.2",
"file-loader": "^6.2.0",
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.5.0",
"jest-environment-jsdom": "^29.7.0",
"mini-css-extract-plugin": "^2.4.5",
"postcss": "8.4.47",
"style-loader": "^3.3.1",

View File

@@ -19,12 +19,7 @@ export default defineConfig({
})
],
source: {
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file,
exclude: [
'**/test-utils/**',
'**/*.test.*',
'**/*.spec.*'
]
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file
},
html: {
title: 'Bruno'
@@ -39,16 +34,6 @@ export default defineConfig({
},
},
},
ignoreWarnings: [
(warning) => warning.message.includes('Critical dependency: the request of a dependency is an expression') && warning?.moduleDescriptor?.name?.includes('flow-parser')
],
// Add externals configuration to exclude Node.js libraries
externals: {
// List specific Node.js modules you want to exclude
// Format: 'module-name': 'commonjs module-name'
'worker_threads': 'commonjs worker_threads',
// 'path': 'commonjs path'
}
},
}
});

View File

@@ -1,40 +0,0 @@
import React, { useMemo } from 'react';
import CodeEditor from 'components/CodeEditor';
import { useTheme } from 'providers/Theme';
import { useSelector } from 'react-redux';
import { parseBulkKeyValue, serializeBulkKeyValue } from 'utils/common/bulkKeyValueUtils';
const BulkEditor = ({ params, onChange, onToggle, onSave, onRun }) => {
const preferences = useSelector((state) => state.app.preferences);
const { displayedTheme } = useTheme();
const parsedParams = useMemo(() => serializeBulkKeyValue(params), [params]);
const handleEdit = (value) => {
const parsed = parseBulkKeyValue(value);
onChange(parsed);
};
return (
<>
<div className="h-[200px]">
<CodeEditor
mode="text/plain"
theme={displayedTheme}
font={preferences.codeFont || 'default'}
value={parsedParams}
onEdit={handleEdit}
onSave={onSave}
onRun={onRun}
/>
</div>
<div className="flex btn-action justify-between items-center mt-3">
<button className="text-link select-none ml-auto" onClick={onToggle}>
Key/Value Edit
</button>
</div>
</>
);
};
export default BulkEditor;

View File

@@ -102,13 +102,6 @@ const StyledWrapper = styled.div`
.cm-s-default span.cm-variable {
color: #397d13 !important;
}
//matching bracket fix
.CodeMirror-matchingbracket {
background: #5cc0b48c !important;
text-decoration:unset;
}
`;
export default StyledWrapper;

View File

@@ -8,19 +8,116 @@
import React from 'react';
import { isEqual, escapeRegExp } from 'lodash';
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
import StyledWrapper from './StyledWrapper';
import * as jsonlint from '@prantlf/jsonlint';
import { JSHINT } from 'jshint';
import stripJsonComments from 'strip-json-comments';
import { getAllVariables } from 'utils/collections';
const CodeMirror = require('codemirror');
window.jsonlint = jsonlint;
window.JSHINT = JSHINT;
let CodeMirror;
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
const TAB_SIZE = 2;
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.responseTime',
'res.getStatus()',
'res.getStatusText()',
'res.getHeader(name)',
'res.getHeaders()',
'res.getBody()',
'res.getResponseTime()',
'req',
'req.url',
'req.method',
'req.headers',
'req.body',
'req.timeout',
'req.getUrl()',
'req.setUrl(url)',
'req.getMethod()',
'req.getAuthMode()',
'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)',
'req.getExecutionMode()',
'bru',
'bru.cwd()',
'bru.getEnvName()',
'bru.getProcessEnv(key)',
'bru.hasEnvVar(key)',
'bru.getEnvVar(key)',
'bru.getFolderVar(key)',
'bru.getCollectionVar(key)',
'bru.setEnvVar(key,value)',
'bru.deleteEnvVar(key)',
'bru.hasVar(key)',
'bru.getVar(key)',
'bru.setVar(key,value)',
'bru.deleteVar(key)',
'bru.deleteAllVars()',
'bru.setNextRequest(requestName)',
'req.disableParsingResponseJson()',
'bru.getRequestVar(key)',
'bru.runRequest(requestPathName)',
'bru.getAssertionResults()',
'bru.getTestResults()',
'bru.sleep(ms)',
'bru.getGlobalEnvVar(key)',
'bru.setGlobalEnvVar(key, value)',
'bru.runner',
'bru.runner.setNextRequest(requestName)',
'bru.runner.skipRequest()',
'bru.runner.stopExecution()'
];
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('.')?.at(-1) : h);
}
});
result.list?.sort();
}
return result;
});
CodeMirror.commands.autocomplete = (cm, hint, options) => {
cm.showHint({ hint, ...options });
};
}
export default class CodeEditor extends React.Component {
constructor(props) {
super(props);
@@ -40,17 +137,12 @@ export default class CodeEditor extends React.Component {
}
componentDidMount() {
const variables = getAllVariables(this.props.collection, this.props.item);
const editor = (this.editor = CodeMirror(this._node, {
value: this.props.value || '',
lineNumbers: true,
lineWrapping: true,
tabSize: TAB_SIZE,
mode: this.props.mode || 'application/ld+json',
brunoVarInfo: {
variables
},
keyMap: 'sublime',
autoCloseBrackets: true,
matchBrackets: true,
@@ -182,24 +274,30 @@ export default class CodeEditor extends React.Component {
}
return found;
});
if (editor) {
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
editor.on('change', this._onEdit);
this.addOverlay();
// Setup AutoComplete Helper for all modes
const autoCompleteOptions = {
showHintsFor: this.props.showHintsFor
};
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
this.brunoAutoCompleteCleanup = setupAutoComplete(
editor,
getVariables,
autoCompleteOptions
);
}
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|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Meta|Alt|Home|End\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 });
}
});
}
}
@@ -240,9 +338,6 @@ export default class CodeEditor extends React.Component {
}
this._unbindSearchHandler();
if (this.brunoAutoCompleteCleanup) {
this.brunoAutoCompleteCleanup();
}
}
render() {
@@ -267,7 +362,7 @@ export default class CodeEditor extends React.Component {
let variables = getAllVariables(this.props.collection, this.props.item);
this.variables = variables;
defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting);
defineCodeMirrorBrunoVariablesMode(variables, mode);
this.editor.setOption('mode', 'brunovariables');
};

View File

@@ -1,51 +0,0 @@
import React from 'react';
import { render, act } from '@testing-library/react';
import CodeEditor from './index';
import { ThemeProvider } from 'styled-components';
jest.mock('codemirror', () => {
const codemirror = require('test-utils/mocks/codemirror');
return codemirror;
});
const MOCK_THEME = {
codemirror: {
bg: "#1e1e1e",
border: "#333",
},
textLink: "#007acc",
};
const setupEditorState = (editor, { value, cursorPosition }) => {
editor._currentValue = value;
editor.getCursor.mockReturnValue({ line: 0, ch: cursorPosition });
editor.getRange.mockImplementation((from, to) => {
if (from.line === 0 && from.ch === 0 && to.line === 0 && to.ch === cursorPosition) {
return value;
}
return editor._currentValue.slice(from.ch, to.ch);
});
editor.state = {
completionActive: null,
}
};
const setupEditorWithRef = () => {
const ref = React.createRef();
const { rerender } = render(
<ThemeProvider theme={MOCK_THEME}>
<CodeEditor ref={ref} />
</ThemeProvider>
);
return { ref, rerender };
};
describe('CodeEditor', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetModules();
});
it("add CodeEditor related tests here", () => {});
});

View File

@@ -21,12 +21,12 @@ const AwsV4Auth = ({ collection }) => {
mode: 'awsv4',
collectionUid: collection.uid,
content: {
accessKeyId: accessKeyId || '',
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: awsv4Auth.service || '',
region: awsv4Auth.region || '',
profileName: awsv4Auth.profileName || ''
accessKeyId: accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: awsv4Auth.service,
region: awsv4Auth.region,
profileName: awsv4Auth.profileName
}
})
);
@@ -38,12 +38,12 @@ const AwsV4Auth = ({ collection }) => {
mode: 'awsv4',
collectionUid: collection.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: awsv4Auth.service || '',
region: awsv4Auth.region || '',
profileName: awsv4Auth.profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: awsv4Auth.service,
region: awsv4Auth.region,
profileName: awsv4Auth.profileName
}
})
);
@@ -55,12 +55,12 @@ const AwsV4Auth = ({ collection }) => {
mode: 'awsv4',
collectionUid: collection.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: sessionToken || '',
service: awsv4Auth.service || '',
region: awsv4Auth.region || '',
profileName: awsv4Auth.profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: sessionToken,
service: awsv4Auth.service,
region: awsv4Auth.region,
profileName: awsv4Auth.profileName
}
})
);
@@ -72,12 +72,12 @@ const AwsV4Auth = ({ collection }) => {
mode: 'awsv4',
collectionUid: collection.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: service || '',
region: awsv4Auth.region || '',
profileName: awsv4Auth.profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: service,
region: awsv4Auth.region,
profileName: awsv4Auth.profileName
}
})
);
@@ -89,12 +89,12 @@ const AwsV4Auth = ({ collection }) => {
mode: 'awsv4',
collectionUid: collection.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: awsv4Auth.service || '',
region: region || '',
profileName: awsv4Auth.profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: awsv4Auth.service,
region: region,
profileName: awsv4Auth.profileName
}
})
);
@@ -106,12 +106,12 @@ const AwsV4Auth = ({ collection }) => {
mode: 'awsv4',
collectionUid: collection.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: awsv4Auth.service || '',
region: awsv4Auth.region || '',
profileName: profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: awsv4Auth.service,
region: awsv4Auth.region,
profileName: profileName
}
})
);

View File

@@ -21,8 +21,8 @@ const BasicAuth = ({ collection }) => {
mode: 'basic',
collectionUid: collection.uid,
content: {
username: username || '',
password: basicAuth.password || ''
username: username,
password: basicAuth.password
}
})
);
@@ -34,8 +34,8 @@ const BasicAuth = ({ collection }) => {
mode: 'basic',
collectionUid: collection.uid,
content: {
username: basicAuth.username || '',
password: password || ''
username: basicAuth.username,
password: password
}
})
);

View File

@@ -21,8 +21,8 @@ const DigestAuth = ({ collection }) => {
mode: 'digest',
collectionUid: collection.uid,
content: {
username: username || '',
password: digestAuth.password || ''
username: username,
password: digestAuth.password
}
})
);
@@ -34,8 +34,8 @@ const DigestAuth = ({ collection }) => {
mode: 'digest',
collectionUid: collection.uid,
content: {
username: digestAuth.username || '',
password: password || ''
username: digestAuth.username,
password: password
}
})
);

View File

@@ -28,9 +28,9 @@ const NTLMAuth = ({ collection }) => {
mode: 'ntlm',
collectionUid: collection.uid,
content: {
username: username || '',
password: ntlmAuth.password || '',
domain: ntlmAuth.domain || ''
username: username,
password: ntlmAuth.password,
domain: ntlmAuth.domain
}
})
@@ -43,9 +43,9 @@ const NTLMAuth = ({ collection }) => {
mode: 'ntlm',
collectionUid: collection.uid,
content: {
username: ntlmAuth.username || '',
password: password || '',
domain: ntlmAuth.domain || ''
username: ntlmAuth.username,
password: password,
domain: ntlmAuth.domain
}
})
);
@@ -57,9 +57,9 @@ const NTLMAuth = ({ collection }) => {
mode: 'ntlm',
collectionUid: collection.uid,
content: {
username: ntlmAuth.username || '',
password: ntlmAuth.password || '',
domain: domain || ''
username: ntlmAuth.username,
password: ntlmAuth.password,
domain: domain
}
})
);

View File

@@ -21,8 +21,8 @@ const WsseAuth = ({ collection }) => {
mode: 'wsse',
collectionUid: collection.uid,
content: {
username: username || '',
password: wsseAuth.password || ''
username,
password: wsseAuth.password
}
})
);
@@ -34,8 +34,8 @@ const WsseAuth = ({ collection }) => {
mode: 'wsse',
collectionUid: collection.uid,
content: {
username: wsseAuth.username || '',
password: password || ''
username: wsseAuth.username,
password
}
})
);

View File

@@ -72,7 +72,7 @@ const Info = ({ collection }) => {
</div>
</div>
</div>
{showShareCollectionModal && <ShareCollection collectionUid={collection.uid} onClose={handleToggleShowShareCollectionModal(false)} />}
{showShareCollectionModal && <ShareCollection collection={collection} onClose={handleToggleShowShareCollectionModal(false)} />}
</div>
</div>
</div>

View File

@@ -53,7 +53,6 @@ const Script = ({ collection }) => {
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'bru']}
/>
</div>
<div className="flex-1 mt-6">
@@ -67,7 +66,6 @@ const Script = ({ collection }) => {
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'res', 'bru']}
/>
</div>

View File

@@ -37,7 +37,6 @@ const Tests = ({ collection }) => {
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'res', 'bru']}
/>
<div className="mt-6">

View File

@@ -49,7 +49,7 @@ const CollectionSettings = ({ collection }) => {
const requestVars = get(collection, 'root.request.vars.req', []);
const responseVars = get(collection, 'root.request.vars.res', []);
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
const auth = get(collection, 'root.request.auth', {}).mode;
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
@@ -155,7 +155,7 @@ const CollectionSettings = ({ collection }) => {
</div>
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
Auth
{authMode !== 'none' && <ContentIndicator />}
{auth !== 'none' && <ContentIndicator />}
</div>
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
Script

View File

@@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
@@ -13,18 +13,11 @@ import { variableNameRegex } from 'utils/common/regex';
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import { Tooltip } from 'react-tooltip';
import { getGlobalEnvironmentVariables } from 'utils/collections';
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const addButtonRef = useRef(null);
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
let _collection = cloneDeep(collection);
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
const formik = useFormik({
enableReinitialize: true,
@@ -167,7 +160,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
<div className="overflow-hidden grow w-full relative">
<SingleLineEditor
theme={storedTheme}
collection={_collection}
collection={collection}
name={`${index}.value`}
value={variable.value}
isSecret={variable.secret}

View File

@@ -28,10 +28,7 @@ const ImportEnvironment = ({ collection, onClose }) => {
.then(() => {
toast.success('Environment imported successfully');
})
.catch((error) => {
toast.error('An error occurred while importing the environment');
console.error(error);
});
.catch(() => toast.error('An error occurred while importing the environment'));
});
})
.then(() => {

View File

@@ -11,12 +11,6 @@ const Wrapper = styled.div`
border: solid 1px ${(props) => props.theme.input.border};
background-color: ${(props) => props.theme.input.bg};
}
.inherit-mode-text {
color: ${(props) => props.theme.colors.text.yellow};
}
.auth-mode-label {
color: ${(props) => props.theme.colors.text.yellow};
}
`;
export default Wrapper;

View File

@@ -9,14 +9,6 @@ import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/Passwo
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
import AuthMode from '../AuthMode';
import BasicAuth from 'components/RequestPane/Auth/BasicAuth';
import BearerAuth from 'components/RequestPane/Auth/BearerAuth';
import DigestAuth from 'components/RequestPane/Auth/DigestAuth';
import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth';
import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
import { findItemInCollection, findParentItemInCollection, humanizeRequestAuthMode } from 'utils/collections/index';
const GrantTypeComponentMap = ({ collection, folder }) => {
const dispatch = useDispatch();
@@ -45,132 +37,12 @@ const Auth = ({ collection, folder }) => {
let request = get(folder, 'root.request', {});
const authMode = get(folder, 'root.request.auth.mode');
const getTreePathFromCollectionToFolder = (collection, _folder) => {
let path = [];
let item = findItemInCollection(collection, _folder?.uid);
while (item) {
path.unshift(item);
item = findParentItemInCollection(collection, item?.uid);
}
return path;
};
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
const collectionAuth = get(collection, 'root.request.auth');
let effectiveSource = {
type: 'collection',
name: 'Collection',
auth: collectionAuth
};
// Get path from collection to current folder
const folderTreePath = getTreePathFromCollectionToFolder(collection, folder);
// Check parent folders to find closest auth configuration
// Skip the last item which is the current folder
for (let i = 0; i < folderTreePath.length - 1; i++) {
const parentFolder = folderTreePath[i];
if (parentFolder.type === 'folder') {
const folderAuth = get(parentFolder, 'root.request.auth');
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
effectiveSource = {
type: 'folder',
name: parentFolder.name,
auth: folderAuth
};
break;
}
}
}
return effectiveSource;
};
const handleSave = () => {
dispatch(saveFolderRoot(collection.uid, folder.uid));
};
const getAuthView = () => {
switch (authMode) {
case 'basic': {
return (
<BasicAuth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'bearer': {
return (
<BearerAuth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'digest': {
return (
<DigestAuth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'ntlm': {
return (
<NTLMAuth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'wsse': {
return (
<WsseAuth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'apikey': {
return (
<ApiKeyAuth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'awsv4': {
return (
<AwsV4Auth
collection={collection}
item={folder}
updateAuth={updateFolderAuth}
request={request}
save={() => handleSave()}
/>
);
}
case 'oauth2': {
return (
<>
@@ -184,17 +56,6 @@ const Auth = ({ collection, folder }) => {
</>
);
}
case 'inherit': {
const source = getEffectiveAuthSource();
return (
<>
<div className="flex flex-row w-full mt-2 gap-2">
<div>Auth inherited from {source.name}: </div>
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
</div>
</>
);
}
case 'none': {
return null;
}
@@ -203,7 +64,6 @@ const Auth = ({ collection, folder }) => {
}
};
return (
<StyledWrapper className="w-full">
<div className="text-xs mb-4 text-muted">

View File

@@ -35,51 +35,6 @@ const AuthMode = ({ collection, folder }) => {
<StyledWrapper>
<div className="inline-flex items-center cursor-pointer">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('awsv4');
}}
>
AWS Sig v4
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('basic');
}}
>
Basic Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('bearer');
}}
>
Bearer Token
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('digest');
}}
>
Digest Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('ntlm');
}}
>
NTLM Auth
</div>
<div
className="dropdown-item"
onClick={() => {
@@ -89,33 +44,6 @@ const AuthMode = ({ collection, folder }) => {
>
OAuth 2.0
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('wsse');
}}
>
WSSE Auth
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('apikey');
}}
>
API Key
</div>
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef.current.hide();
onModeChange('inherit');
}}
>
Inherit
</div>
<div
className="dropdown-item"
onClick={() => {

View File

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

View File

@@ -55,7 +55,6 @@ const Script = ({ collection, folder }) => {
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'bru']}
/>
</div>
<div className="flex flex-col flex-1 mt-2 gap-y-2">
@@ -69,7 +68,6 @@ const Script = ({ collection, folder }) => {
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'res', 'bru']}
/>
</div>

View File

@@ -38,7 +38,6 @@ const Tests = ({ collection, folder }) => {
onSave={handleSave}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
showHintsFor={['req', 'res', 'bru']}
/>
<div className="mt-6">

View File

@@ -28,7 +28,7 @@ const FolderSettings = ({ collection, folder }) => {
tab = folderLevelSettingsSelectedTab[folder?.uid];
}
const folderRoot = folder?.root;
const folderRoot = collection?.items.find((item) => item.uid === folder?.uid)?.root;
const hasScripts = folderRoot?.request?.script?.res || folderRoot?.request?.script?.req;
const hasTests = folderRoot?.request?.tests;

View File

@@ -34,10 +34,7 @@ const ImportEnvironment = ({ onClose }) => {
.then(() => {
toast.success('Global Environment imported successfully');
})
.catch((error) => {
toast.error('An error occurred while importing the environment');
console.error(error);
});
.catch(() => toast.error('An error occurred while importing the environment'));
});
})
.then(() => {

View File

@@ -8,7 +8,7 @@ const DotIcon = ({ width }) => {
className='inline-block'
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" strokeWidth="0" fill="currentColor" />
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor" />
</svg>
);
};

View File

@@ -2,10 +2,14 @@ import React, { Component } from 'react';
import isEqual from 'lodash/isEqual';
import { getAllVariables } from 'utils/collections';
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
import StyledWrapper from './StyledWrapper';
const CodeMirror = require('codemirror');
let CodeMirror;
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
}
class MultiLineEditor extends Component {
constructor(props) {
@@ -74,21 +78,14 @@ class MultiLineEditor extends Component {
'Shift-Tab': false
}
});
// Setup AutoComplete Helper
const autoCompleteOptions = {
showHintsFor: ['variables'],
anywordAutocompleteHints: this.props.autocomplete
};
const getVariables = () => getAllVariables(this.props.collection, this.props.item);
this.brunoAutoCompleteCleanup = setupAutoComplete(
this.editor,
getVariables,
autoCompleteOptions
);
if (this.props.autocomplete) {
this.editor.on('keyup', (cm, event) => {
if (!cm.state.completionActive /*Enables keyboard navigation in autocomplete list*/ && event.keyCode != 13) {
/*Enter - do not open autocomplete list just after item has been selected in it*/
CodeMirror.commands.autocomplete(cm, CodeMirror.hint.anyword, { autocomplete: this.props.autocomplete });
}
});
}
this.editor.setValue(String(this.props.value) || '');
this.editor.on('change', this._onEdit);
this.addOverlay(variables);
@@ -128,15 +125,12 @@ class MultiLineEditor extends Component {
}
componentWillUnmount() {
if (this.brunoAutoCompleteCleanup) {
this.brunoAutoCompleteCleanup();
}
this.editor.getWrapperElement().remove();
}
addOverlay = (variables) => {
this.variables = variables;
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', false, true);
defineCodeMirrorBrunoVariablesMode(variables, 'text/plain');
this.editor.setOption('mode', 'brunovariables');
};

View File

@@ -3,7 +3,6 @@ import get from 'lodash/get';
import { useSelector, useDispatch } from 'react-redux';
import { savePreferences } from 'providers/ReduxStore/slices/app';
import StyledWrapper from './StyledWrapper';
import toast from 'react-hot-toast';
const Font = ({ close }) => {
const dispatch = useDispatch();
@@ -32,10 +31,7 @@ const Font = ({ close }) => {
}
})
).then(() => {
toast.success('Preferences saved successfully')
close();
}).catch(() => {
toast.error('Failed to save preferences')
});
};

View File

@@ -80,9 +80,9 @@ const General = ({ close }) => {
storeCookies: newPreferences.storeCookies,
sendCookies: newPreferences.sendCookies
}
}))
})
)
.then(() => {
toast.success('Preferences saved successfully')
close();
})
.catch((err) => console.log(err) && toast.error('Failed to update preferences'));
@@ -125,7 +125,7 @@ const General = ({ close }) => {
className="mousetrap mr-0"
/>
<label className="block ml-2 select-none" htmlFor="customCaCertificateEnabled">
Use Custom CA Certificate
Use custom CA Certificate
</label>
</div>
{formik.values.customCaCertificate.filePath ? (
@@ -183,7 +183,7 @@ const General = ({ close }) => {
className={`block ml-2 select-none ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
htmlFor="keepDefaultCaCertificatesEnabled"
>
Keep Default CA Certificates
Keep default CA Certificates
</label>
</div>
<div className="flex items-center mt-2">

View File

@@ -84,10 +84,7 @@ const ProxySettings = ({ close }) => {
proxy: validatedProxy
})
).then(() => {
toast.success('Preferences saved successfully')
close();
}).catch(() => {
toast.error('Failed to save preferences')
});
})
.catch((error) => {

View File

@@ -5,23 +5,21 @@ import { IconCaretDown } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { useTheme } from 'providers/Theme';
import SingleLineEditor from 'components/SingleLineEditor';
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
import { humanizeRequestAPIKeyPlacement } from 'utils/collections';
const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
const ApiKeyAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const apikeyAuth = get(request, 'auth.apikey', {});
const apikeyAuth = item.draft ? get(item, 'draft.request.auth.apikey', {}) : get(item, 'request.auth.apikey', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => {
save();
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const Icon = forwardRef((props, ref) => {
return (
@@ -92,7 +90,7 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef?.current?.hide();
dropdownTippyRef.current.hide();
handleAuthChange('placement', 'header');
}}
>
@@ -101,11 +99,11 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
<div
className="dropdown-item"
onClick={() => {
dropdownTippyRef?.current?.hide();
dropdownTippyRef.current.hide();
handleAuthChange('placement', 'queryparams');
}}
>
Query Param
Query Params
</div>
</Dropdown>
</div>

View File

@@ -8,17 +8,14 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
import StyledWrapper from './StyledWrapper';
import { update } from 'lodash';
const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
const AwsV4Auth = ({ onTokenChange, item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const awsv4Auth = get(request, 'auth.awsv4', {});
const awsv4Auth = item.draft ? get(item, 'draft.request.auth.awsv4', {}) : get(item, 'request.auth.awsv4', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => {
save();
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleAccessKeyIdChange = (accessKeyId) => {
dispatch(
@@ -28,11 +25,11 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
itemUid: item.uid,
content: {
accessKeyId: accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: awsv4Auth.service || '',
region: awsv4Auth.region || '',
profileName: awsv4Auth.profileName || ''
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: awsv4Auth.service,
region: awsv4Auth.region,
profileName: awsv4Auth.profileName
}
})
);
@@ -45,12 +42,12 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: awsv4Auth.service || '',
region: awsv4Auth.region || '',
profileName: awsv4Auth.profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: awsv4Auth.service,
region: awsv4Auth.region,
profileName: awsv4Auth.profileName
}
})
);
@@ -63,12 +60,12 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: sessionToken || '',
service: awsv4Auth.service || '',
region: awsv4Auth.region || '',
profileName: awsv4Auth.profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: sessionToken,
service: awsv4Auth.service,
region: awsv4Auth.region,
profileName: awsv4Auth.profileName
}
})
);
@@ -81,12 +78,12 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: service || '',
region: awsv4Auth.region || '',
profileName: awsv4Auth.profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: service,
region: awsv4Auth.region,
profileName: awsv4Auth.profileName
}
})
);
@@ -99,12 +96,12 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: awsv4Auth.service || '',
region: region || '',
profileName: awsv4Auth.profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: awsv4Auth.service,
region: region,
profileName: awsv4Auth.profileName
}
})
);
@@ -117,12 +114,12 @@ const AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
accessKeyId: awsv4Auth.accessKeyId || '',
secretAccessKey: awsv4Auth.secretAccessKey || '',
sessionToken: awsv4Auth.sessionToken || '',
service: awsv4Auth.service || '',
region: awsv4Auth.region || '',
profileName: profileName || ''
accessKeyId: awsv4Auth.accessKeyId,
secretAccessKey: awsv4Auth.secretAccessKey,
sessionToken: awsv4Auth.sessionToken,
service: awsv4Auth.service,
region: awsv4Auth.region,
profileName: profileName
}
})
);

View File

@@ -7,17 +7,14 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
const BasicAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const basicAuth = get(request, 'auth.basic', {});
const basicAuth = item.draft ? get(item, 'draft.request.auth.basic', {}) : get(item, 'request.auth.basic', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => {
save();
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleUsernameChange = (username) => {
dispatch(
@@ -26,8 +23,8 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: username || '',
password: basicAuth.password || ''
username: username,
password: basicAuth.password
}
})
);
@@ -40,8 +37,8 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: basicAuth.username || '',
password: password || ''
username: basicAuth.username,
password: password
}
})
);

View File

@@ -7,18 +7,16 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
const BearerAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
// Use the request prop directly like OAuth2ClientCredentials does
const bearerToken = get(request, 'auth.bearer.token', '');
const bearerToken = item.draft
? get(item, 'draft.request.auth.bearer.token', '')
: get(item, 'request.auth.bearer.token', '');
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => {
save();
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleTokenChange = (token) => {
dispatch(

View File

@@ -3,20 +3,18 @@ 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, updateAuth, request, save }) => {
const DigestAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const digestAuth = get(request, 'auth.digest', {});
const digestAuth = item.draft ? get(item, 'draft.request.auth.digest', {}) : get(item, 'request.auth.digest', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => {
save();
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleUsernameChange = (username) => {
dispatch(
@@ -25,8 +23,8 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: username || '',
password: digestAuth.password || ''
username: username,
password: digestAuth.password
}
})
);
@@ -39,8 +37,8 @@ const DigestAuth = ({ item, collection, updateAuth, request, save }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: digestAuth.username || '',
password: password || ''
username: digestAuth.username,
password: password
}
})
);

View File

@@ -7,17 +7,14 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
const NTLMAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const ntlmAuth = get(request, 'auth.ntlm', {});
const ntlmAuth = item.draft ? get(item, 'draft.request.auth.ntlm', {}) : get(item, 'request.auth.ntlm', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => {
save();
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleUsernameChange = (username) => {
dispatch(
@@ -26,9 +23,10 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: username || '',
password: ntlmAuth.password || '',
domain: ntlmAuth.domain || ''
username: username,
password: ntlmAuth.password,
domain: ntlmAuth.domain
}
})
);
@@ -41,9 +39,9 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: ntlmAuth.username || '',
password: password || '',
domain: ntlmAuth.domain || ''
username: ntlmAuth.username,
password: password,
domain: ntlmAuth.domain
}
})
);
@@ -56,9 +54,9 @@ const NTLMAuth = ({ item, collection, request, save, updateAuth }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: ntlmAuth.username || '',
password: ntlmAuth.password || '',
domain: domain || ''
username: ntlmAuth.username,
password: ntlmAuth.password,
domain: domain
}
})
);

View File

@@ -239,6 +239,17 @@ const OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAu
</div>
</div>
<div className="flex items-center gap-4 w-full mb-4">
<label className="block min-w-[140px]">Auto-refresh token</label>
<input
type="checkbox"
className="cursor-pointer w-4 h-4 accent-indigo-600"
checked={get(request, 'auth.oauth2.autoRefreshToken', false)}
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
/>
<span className="text-xs text-gray-500">Automatically refresh the token when it expires</span>
</div>
<div className="flex items-center gap-2.5 mt-4">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />

View File

@@ -3,7 +3,8 @@ import { useDispatch } from "react-redux";
import toast from 'react-hot-toast';
import { cloneDeep, find } from 'lodash';
import { IconLoader2 } from '@tabler/icons';
import { interpolate } from '@usebruno/common';
import brunoCommon from '@usebruno/common';
const { interpolate } = brunoCommon;
import { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials } from 'providers/ReduxStore/slices/collections/actions';
import { getAllVariables } from "utils/collections/index";
@@ -34,14 +35,14 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
toast.success('token fetched successfully!');
}
else {
toast.error('An error occurred while fetching token!');
toast.error('An error occured while fetching token!');
}
}
catch (error) {
console.error('could not fetch the token!');
console.error(error);
toggleFetchingToken(false);
toast.error('An error occurred while fetching token!');
toast.error('An error occured while fetching token!');
}
}
@@ -57,13 +58,13 @@ const Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, c
toast.success('token refreshed successfully!');
}
else {
toast.error('An error occurred while refreshing token!');
toast.error('An error occured while refreshing token!');
}
}
catch(error) {
console.error(error);
toggleRefreshingToken(false);
toast.error('An error occurred while refreshing token!');
toast.error('An error occured while refreshing token!');
}
};

View File

@@ -1,9 +1,10 @@
import { useState, useEffect, useMemo } from "react";
import { find } from "lodash";
import StyledWrapper from "./StyledWrapper";
import { useState, useEffect } from "react";
import { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons';
import { getAllVariables } from 'utils/collections/index';
import { interpolate } from '@usebruno/common';
import brunoCommon from '@usebruno/common';
const { interpolate } = brunoCommon;
const TokenSection = ({ title, token }) => {
if (!token) return null;

View File

@@ -242,6 +242,17 @@ const OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, update
</div>
</div>
<div className="flex items-center gap-4 w-full mb-4">
<label className="block min-w-[140px]">Auto-refresh token</label>
<input
type="checkbox"
className="cursor-pointer w-4 h-4 accent-indigo-600"
checked={get(request, 'auth.oauth2.autoRefreshToken', false)}
onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}
/>
<span className="text-xs text-gray-500">Automatically refresh the token when it expires</span>
</div>
<div className="flex items-center gap-2.5 mt-4">
<div className="flex items-center px-2.5 py-1.5 bg-indigo-50/50 dark:bg-indigo-500/10 rounded-md">
<IconSettings size={14} className="text-indigo-500 dark:text-indigo-400" />

View File

@@ -7,17 +7,14 @@ import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import StyledWrapper from './StyledWrapper';
const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
const WsseAuth = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const wsseAuth = get(request, 'auth.wsse', {});
const wsseAuth = item.draft ? get(item, 'draft.request.auth.wsse', {}) : get(item, 'request.auth.wsse', {});
const handleRun = () => dispatch(sendRequest(item, collection.uid));
const handleSave = () => {
save();
};
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
const handleUserChange = (username) => {
dispatch(
@@ -26,8 +23,8 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: username || '',
password: wsseAuth.password || ''
username,
password: wsseAuth.password
}
})
);
@@ -40,8 +37,8 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
collectionUid: collection.uid,
itemUid: item.uid,
content: {
username: wsseAuth.username || '',
password: password || ''
username: wsseAuth.username,
password
}
})
);
@@ -58,7 +55,6 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
onChange={(val) => handleUserChange(val)}
onRun={handleRun}
collection={collection}
item={item}
/>
</div>
@@ -71,8 +67,6 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
onChange={(val) => handlePasswordChange(val)}
onRun={handleRun}
collection={collection}
item={item}
isSecret={true}
/>
</div>
</StyledWrapper>

View File

@@ -7,8 +7,6 @@ import BasicAuth from './BasicAuth';
import DigestAuth from './DigestAuth';
import WsseAuth from './WsseAuth';
import NTLMAuth from './NTLMAuth';
import { updateAuth } from 'providers/ReduxStore/slices/collections';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import ApiKeyAuth from './ApiKeyAuth';
import StyledWrapper from './StyledWrapper';
@@ -29,16 +27,6 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
const Auth = ({ item, collection }) => {
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
// Create a request object to pass to the auth components
const request = item.draft
? get(item, 'draft.request', {})
: get(item, 'request', {});
// Save function for request level
const save = () => {
return saveRequest(item.uid, collection.uid);
};
const getEffectiveAuthSource = () => {
if (authMode !== 'inherit') return null;
@@ -71,28 +59,28 @@ const Auth = ({ item, collection }) => {
const getAuthView = () => {
switch (authMode) {
case 'awsv4': {
return <AwsV4Auth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <AwsV4Auth collection={collection} item={item} />;
}
case 'basic': {
return <BasicAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <BasicAuth collection={collection} item={item} />;
}
case 'bearer': {
return <BearerAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <BearerAuth collection={collection} item={item} />;
}
case 'digest': {
return <DigestAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <DigestAuth collection={collection} item={item} />;
}
case 'ntlm': {
return <NTLMAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <NTLMAuth collection={collection} item={item} />;
}
case 'oauth2': {
return <OAuth2 collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <OAuth2 collection={collection} item={item} />;
}
case 'wsse': {
return <WsseAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <WsseAuth collection={collection} item={item} />;
}
case 'apikey': {
return <ApiKeyAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;
return <ApiKeyAuth collection={collection} item={item} />;
}
case 'inherit': {
const source = getEffectiveAuthSource();

View File

@@ -18,9 +18,8 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
import StyledWrapper from './StyledWrapper';
import Documentation from 'components/Documentation/index';
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
import HeightBoundContainer from 'ui/HeightBoundContainer';
const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
@@ -67,6 +66,7 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
collection={collection}
theme={displayedTheme}
schema={schema}
width={leftPaneWidth}
onSave={onSave}
value={query}
onRun={onRun}
@@ -154,9 +154,7 @@ const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handle
</div>
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
</div>
<section className="flex w-full mt-5 flex-1 relative">
<HeightBoundContainer>{getTabPanel(focusedTab.requestPaneTab)}</HeightBoundContainer>
</section>
<section className="flex w-full mt-5 flex-1 relative">{getTabPanel(focusedTab.requestPaneTab)}</section>
</StyledWrapper>
);
};

View File

@@ -7,10 +7,8 @@ import Dropdown from '../../Dropdown';
const GraphQLSchemaActions = ({ item, collection, onSchemaLoad, toggleDocs }) => {
const url = item.draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', '');
const pathname = item.draft ? get(item, 'draft.pathname', '') : get(item, 'pathname', '');
const uid = item.draft ? get(item, 'draft.uid', '') : get(item, 'uid', '');
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
const request = item.draft ? { ...item.draft.request, pathname, uid } : { ...item.request, pathname, uid };
const request = item.draft ? item.draft.request : item.request;
let {
schema,

View File

@@ -64,11 +64,9 @@ const GraphQLVariables = ({ variables, item, collection }) => {
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
onEdit={onEdit}
mode="application/json"
mode="javascript"
onRun={onRun}
onSave={onSave}
enableVariableHighlighting={true}
showHintsFor={['variables']}
/>
</>
);

View File

@@ -15,7 +15,6 @@ import Tests from 'components/RequestPane/Tests';
import StyledWrapper from './StyledWrapper';
import { find, get } from 'lodash';
import Documentation from 'components/Documentation/index';
import HeightBoundContainer from 'ui/HeightBoundContainer';
import { useEffect } from 'react';
const ContentIndicator = () => {
@@ -34,7 +33,7 @@ const ErrorIndicator = () => {
);
};
const HttpRequestPane = ({ item, collection }) => {
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs);
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
@@ -181,9 +180,7 @@ const HttpRequestPane = ({ item, collection }) => {
'mt-5': !isMultipleContentTab
})}
>
<HeightBoundContainer>
{getTabPanel(focusedTab.requestPaneTab)}
</HeightBoundContainer>
{getTabPanel(focusedTab.requestPaneTab)}
</section>
</StyledWrapper>
);

View File

@@ -18,7 +18,12 @@ import { IconWand } from '@tabler/icons';
import onHasCompletion from './onHasCompletion';
const CodeMirror = require('codemirror');
let CodeMirror;
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
if (!SERVER_RENDERED) {
CodeMirror = require('codemirror');
}
const md = new MD();
const AUTO_COMPLETE_AFTER_KEY = /^[a-zA-Z0-9_@(]$/;

View File

@@ -31,7 +31,7 @@ const Wrapper = styled.div`
}
}
.btn-action {
.btn-add-param {
font-size: 0.8125rem;
&:hover span {
text-decoration: underline;

View File

@@ -1,17 +1,16 @@
import React, { useState } from 'react';
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import InfoTip from 'components/InfoTip';
import { IconTrash } from '@tabler/icons';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import {
addQueryParam,
updateQueryParam,
deleteQueryParam,
moveQueryParam,
updatePathParam,
setQueryParams
updatePathParam
} from 'providers/ReduxStore/slices/collections';
import SingleLineEditor from 'components/SingleLineEditor';
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
@@ -19,7 +18,6 @@ import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collection
import StyledWrapper from './StyledWrapper';
import Table from 'components/Table/index';
import ReorderTable from 'components/ReorderTable';
import BulkEditor from '../../BulkEditor';
const QueryParams = ({ item, collection }) => {
const dispatch = useDispatch();
@@ -27,8 +25,6 @@ const QueryParams = ({ item, collection }) => {
const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');
const queryParams = params.filter((param) => param.type === 'query');
const pathParams = params.filter((param) => param.type === 'path');
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
const handleAddQueryParam = () => {
dispatch(
@@ -117,31 +113,8 @@ const QueryParams = ({ item, collection }) => {
);
};
const toggleBulkEditMode = () => {
setIsBulkEditMode(!isBulkEditMode);
};
const handleBulkParamsChange = (newParams) => {
const paramsWithType = newParams.map((item) => ({ ...item, type: 'query' }));
dispatch(setQueryParams({ collectionUid: collection.uid, itemUid: item.uid, params: paramsWithType }));
};
if (isBulkEditMode) {
return (
<StyledWrapper className="w-full mt-3">
<BulkEditor
params={queryParams}
onChange={handleBulkParamsChange}
onToggle={toggleBulkEditMode}
onSave={onSave}
onRun={handleRun}
/>
</StyledWrapper>
);
}
return (
<StyledWrapper className="w-full flex flex-col">
<StyledWrapper className="w-full flex flex-col absolute">
<div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Query</div>
<Table
@@ -198,14 +171,9 @@ const QueryParams = ({ item, collection }) => {
</ReorderTable>
</Table>
<div className="flex justify-between mt-2">
<button className="btn-action text-link pr-2 py-3 select-none" onClick={handleAddQueryParam}>
+&nbsp;<span>Add Param</span>
</button>
<button className="btn-action text-link select-none" onClick={toggleBulkEditMode}>
Bulk Edit
</button>
</div>
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddQueryParam}>
+&nbsp;<span>Add Param</span>
</button>
<div className="mb-2 title text-xs flex items-stretch">
<span>Path</span>
<InfoTip infotipId="path-param-InfoTip">

View File

@@ -140,7 +140,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
</div>
</div>
{generateCodeItemModalOpen && (
<GenerateCodeItem collectionUid={collection.uid} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
<GenerateCodeItem collection={collection} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
)}
</StyledWrapper>
);

View File

@@ -21,7 +21,7 @@ const RequestBodyMode = ({ item, collection }) => {
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none selected-body-mode">
{humanizeRequestBodyMode(bodyMode)} <IconCaretDown className="caret ml-2" size={14} strokeWidth={2} />
{humanizeRequestBodyMode(bodyMode)} <IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
</div>
);
});
@@ -149,7 +149,7 @@ const RequestBodyMode = ({ item, collection }) => {
</Dropdown>
</div>
{(bodyMode === 'json' || bodyMode === 'xml') && (
<button className="ml-2" onClick={onPrettify}>
<button className="ml-1" onClick={onPrettify}>
Prettify
</button>
)}

View File

@@ -49,7 +49,7 @@ const RequestBody = ({ item, collection }) => {
<StyledWrapper className="w-full">
<CodeEditor
collection={collection}
item={item}
item={item}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
@@ -58,15 +58,13 @@ const RequestBody = ({ item, collection }) => {
onRun={onRun}
onSave={onSave}
mode={codeMirrorMode[bodyMode]}
enableVariableHighlighting={true}
showHintsFor={['variables']}
/>
</StyledWrapper>
);
}
if (bodyMode === 'file') {
return <FileBody item={item} collection={collection} />;
return <FileBody item={item} collection={collection}/>
}
if (bodyMode === 'formUrlEncoded') {
@@ -79,4 +77,4 @@ const RequestBody = ({ item, collection }) => {
return <StyledWrapper className="w-full">No Body</StyledWrapper>;
};
export default RequestBody;
export default RequestBody;

View File

@@ -22,11 +22,8 @@ const Wrapper = styled.div`
}
}
.btn-action {
.btn-add-header {
font-size: 0.8125rem;
&:hover span {
text-decoration: underline;
}
}
input[type='text'] {

View File

@@ -1,10 +1,10 @@
import React, { useState } from 'react';
import React from 'react';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme';
import { addRequestHeader, updateRequestHeader, deleteRequestHeader, moveRequestHeader, setRequestHeaders } from 'providers/ReduxStore/slices/collections';
import { addRequestHeader, updateRequestHeader, deleteRequestHeader, moveRequestHeader } from 'providers/ReduxStore/slices/collections';
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper';
@@ -12,16 +12,12 @@ import { headers as StandardHTTPHeaders } from 'know-your-http-well';
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
import Table from 'components/Table/index';
import ReorderTable from 'components/ReorderTable/index';
import BulkEditor from '../../BulkEditor';
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
const RequestHeaders = ({ item, collection }) => {
const dispatch = useDispatch();
const { storedTheme } = useTheme();
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
const addHeader = () => {
dispatch(
@@ -79,28 +75,6 @@ const RequestHeaders = ({ item, collection }) => {
);
};
const toggleBulkEditMode = () => {
setIsBulkEditMode(!isBulkEditMode);
};
const handleBulkHeadersChange = (newHeaders) => {
dispatch(setRequestHeaders({ collectionUid: collection.uid, itemUid: item.uid, headers: newHeaders }));
};
if (isBulkEditMode) {
return (
<StyledWrapper className="w-full mt-3">
<BulkEditor
params={headers}
onChange={handleBulkHeadersChange}
onToggle={toggleBulkEditMode}
onSave={onSave}
onRun={handleRun}
/>
</StyledWrapper>
);
}
return (
<StyledWrapper className="w-full">
<Table
@@ -179,14 +153,9 @@ const RequestHeaders = ({ item, collection }) => {
: null}
</ReorderTable>
</Table>
<div className="flex justify-between mt-2">
<button className="btn-action text-link pr-2 py-3 select-none" onClick={addHeader}>
+ Add Header
</button>
<button className="btn-action text-link select-none" onClick={toggleBulkEditMode}>
Bulk Edit
</button>
</div>
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
+ Add Header
</button>
</StyledWrapper>
);
};

View File

@@ -52,7 +52,6 @@ const Script = ({ item, collection }) => {
mode="javascript"
onRun={onRun}
onSave={onSave}
showHintsFor={['req', 'bru']}
/>
</div>
<div className="flex flex-col flex-1 mt-2 gap-y-2">
@@ -67,7 +66,6 @@ const Script = ({ item, collection }) => {
mode="javascript"
onRun={onRun}
onSave={onSave}
showHintsFor={['req', 'res', 'bru']}
/>
</div>
</StyledWrapper>

View File

@@ -37,7 +37,6 @@ const Tests = ({ item, collection }) => {
mode="javascript"
onRun={onRun}
onSave={onSave}
showHintsFor={['req', 'res', 'bru']}
/>
);
};

View File

@@ -1,42 +0,0 @@
import React, { useEffect, useState, useCallback } from 'react';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux';
const FolderNotFound = ({ folderUid }) => {
const dispatch = useDispatch();
const [showErrorMessage, setShowErrorMessage] = useState(false);
const closeTab = useCallback(() => {
dispatch(
closeTabs({
tabUids: [folderUid]
})
);
}, [dispatch, folderUid]);
useEffect(() => {
setTimeout(() => {
setShowErrorMessage(true);
}, 300);
}, []);
if (!showErrorMessage) {
return null;
}
return (
<div className="mt-6 px-6">
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700">
<div>Folder no longer exists.</div>
<div className="mt-2">
This can happen when the folder was renamed or deleted on your filesystem.
</div>
</div>
<button className="btn btn-md btn-secondary mt-6" onClick={closeTab}>
Close Tab
</button>
</div>
);
};
export default FolderNotFound;

View File

@@ -3,13 +3,9 @@ import styled from 'styled-components';
const StyledWrapper = styled.div`
&.dragging {
cursor: col-resize;
&.vertical-layout {
cursor: row-resize;
}
}
div.dragbar-wrapper {
div.drag-request {
display: flex;
align-items: center;
justify-content: center;
@@ -19,47 +15,18 @@ const StyledWrapper = styled.div`
cursor: col-resize;
background: transparent;
div.dragbar-handle {
div.drag-request-border {
display: flex;
height: 100%;
width: 1px;
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};
}
&:hover div.dragbar-handle {
&:hover div.drag-request-border {
border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};
}
}
&.vertical-layout {
.request-pane {
padding-bottom: 0.5rem;
}
.response-pane {
padding-top: 0.5rem;
}
div.dragbar-wrapper {
width: 100%;
height: 10px;
cursor: row-resize;
padding: 0 1rem;
div.dragbar-handle {
width: 100%;
height: 1px;
border-left: none;
border-top: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};
}
&:hover div.dragbar-handle {
border-left: none;
border-top: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};
}
}
}
div.graphql-docs-explorer-container {
background: white;
outline: none;

View File

@@ -25,12 +25,10 @@ import { produce } from 'immer';
import CollectionOverview from 'components/CollectionSettings/Overview';
import RequestNotLoaded from './RequestNotLoaded';
import RequestIsLoading from './RequestIsLoading';
import FolderNotFound from './FolderNotFound';
const MIN_LEFT_PANE_WIDTH = 300;
const MIN_RIGHT_PANE_WIDTH = 350;
const MIN_TOP_PANE_HEIGHT = 150;
const MIN_BOTTOM_PANE_HEIGHT = 150;
const DEFAULT_PADDING = 5;
const RequestTabPanel = () => {
if (typeof window == 'undefined') {
@@ -42,8 +40,6 @@ const RequestTabPanel = () => {
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
const _collections = useSelector((state) => state.collections.collections);
const preferences = useSelector((state) => state.app.preferences);
const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';
// merge `globalEnvironmentVariables` into the active collection and rebuild `collections` immer proxy object
let collections = produce(_collections, (draft) => {
@@ -67,15 +63,13 @@ const RequestTabPanel = () => {
let asideWidth = useSelector((state) => state.app.leftSidebarWidth);
const [leftPaneWidth, setLeftPaneWidth] = useState(
focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2
); // 2.2 is intentional to make both panes appear to be of equal width
const [topPaneHeight, setTopPaneHeight] = useState(focusedTab?.requestPaneHeight || MIN_TOP_PANE_HEIGHT);
); // 2.2 so that request pane is relatively smaller
const [rightPaneWidth, setRightPaneWidth] = useState(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
const [dragging, setDragging] = useState(false);
const dragOffset = useRef({ x: 0, y: 0 });
// Not a recommended pattern here to have the child component
// make a callback to set state, but treating this as an exception
const docExplorerRef = useRef(null);
const mainSectionRef = useRef(null);
const [schema, setSchema] = useState(null);
const [showGqlDocs, setShowGqlDocs] = useState(false);
const onSchemaLoad = (schema) => setSchema(schema);
@@ -90,72 +84,43 @@ const RequestTabPanel = () => {
};
useEffect(() => {
// Initialize vertical heights when switching to vertical layout
if (mainSectionRef.current) {
const mainRect = mainSectionRef.current.getBoundingClientRect();
if (isVerticalLayout) {
const initialHeight = mainRect.height / 2;
setTopPaneHeight(initialHeight);
// In vertical mode, set leftPaneWidth to full container width
setLeftPaneWidth(mainRect.width);
} else {
// In horizontal mode, set to roughly half width
setLeftPaneWidth((screenWidth - asideWidth) / 2.2);
}
}
}, [isVerticalLayout, screenWidth, asideWidth]);
const leftPaneWidth = (screenWidth - asideWidth) / 2.2;
setLeftPaneWidth(leftPaneWidth);
}, [screenWidth]);
useEffect(() => {
setRightPaneWidth(screenWidth - asideWidth - leftPaneWidth - DEFAULT_PADDING);
}, [screenWidth, asideWidth, leftPaneWidth]);
const handleMouseMove = (e) => {
if (dragging && mainSectionRef.current) {
if (dragging) {
e.preventDefault();
const mainRect = mainSectionRef.current.getBoundingClientRect();
if (isVerticalLayout) {
const newHeight = e.clientY - mainRect.top - dragOffset.current.y;
if (newHeight < MIN_TOP_PANE_HEIGHT || newHeight > mainRect.height - MIN_BOTTOM_PANE_HEIGHT) {
return;
}
setTopPaneHeight(newHeight);
} else {
const newWidth = e.clientX - mainRect.left - dragOffset.current.x;
if (newWidth < MIN_LEFT_PANE_WIDTH || newWidth > mainRect.width - MIN_RIGHT_PANE_WIDTH) {
return;
}
setLeftPaneWidth(newWidth);
let leftPaneXPosition = e.clientX + 2;
if (
leftPaneXPosition < asideWidth + DEFAULT_PADDING + MIN_LEFT_PANE_WIDTH ||
leftPaneXPosition > screenWidth - MIN_RIGHT_PANE_WIDTH
) {
return;
}
setLeftPaneWidth(leftPaneXPosition - asideWidth);
setRightPaneWidth(screenWidth - e.clientX - DEFAULT_PADDING);
}
};
const handleMouseUp = (e) => {
if (dragging && mainSectionRef.current) {
if (dragging) {
e.preventDefault();
setDragging(false);
if (!isVerticalLayout) {
const mainRect = mainSectionRef.current.getBoundingClientRect();
dispatch(
updateRequestPaneTabWidth({
uid: activeTabUid,
requestPaneWidth: e.clientX - mainRect.left
})
);
}
dispatch(
updateRequestPaneTabWidth({
uid: activeTabUid,
requestPaneWidth: e.clientX - asideWidth - DEFAULT_PADDING
})
);
}
};
const handleDragbarMouseDown = (e) => {
e.preventDefault();
setDragging(true);
if (isVerticalLayout) {
const dragBar = e.currentTarget;
const dragBarRect = dragBar.getBoundingClientRect();
dragOffset.current.y = e.clientY - dragBarRect.top;
} else {
const dragBar = e.currentTarget;
const dragBarRect = dragBar.getBoundingClientRect();
dragOffset.current.x = e.clientX - dragBarRect.left;
}
};
useEffect(() => {
@@ -166,7 +131,7 @@ const RequestTabPanel = () => {
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
};
}, [dragging]);
}, [dragging, asideWidth]);
if (!activeTabUid) {
return <Welcome />;
@@ -198,10 +163,6 @@ const RequestTabPanel = () => {
if (focusedTab.type === 'folder-settings') {
const folder = findItemInCollection(collection, focusedTab.folderUid);
if (!folder) {
return <FolderNotFound folderUid={focusedTab.folderUid} />;
}
return <FolderSettings collection={collection} folder={folder} />;
}
@@ -231,19 +192,15 @@ const RequestTabPanel = () => {
};
return (
<StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''} ${isVerticalLayout ? 'vertical-layout' : ''}`}>
<StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''}`}>
<div className="pt-4 pb-3 px-4">
<QueryUrl item={item} collection={collection} handleRun={handleRun} />
</div>
<section ref={mainSectionRef} className={`main flex ${isVerticalLayout ? 'flex-col' : ''} flex-grow pb-4 relative`}>
<section className="main flex flex-grow pb-4 relative">
<section className="request-pane">
<div
className="px-4 h-full"
style={isVerticalLayout ? {
height: `${Math.max(topPaneHeight, MIN_TOP_PANE_HEIGHT)}px`,
minHeight: `${MIN_TOP_PANE_HEIGHT}px`,
width: '100%'
} : {
style={{
width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`
}}
>
@@ -251,6 +208,7 @@ const RequestTabPanel = () => {
<GraphQLRequestPane
item={item}
collection={collection}
leftPaneWidth={leftPaneWidth}
onSchemaLoad={onSchemaLoad}
toggleDocs={toggleDocs}
handleGqlClickReference={handleGqlClickReference}
@@ -258,17 +216,17 @@ const RequestTabPanel = () => {
) : null}
{item.type === 'http-request' ? (
<HttpRequestPane item={item} collection={collection} />
<HttpRequestPane item={item} collection={collection} leftPaneWidth={leftPaneWidth} />
) : null}
</div>
</section>
<div className="dragbar-wrapper" onMouseDown={handleDragbarMouseDown}>
<div className="dragbar-handle" />
<div className="drag-request" onMouseDown={handleDragbarMouseDown}>
<div className="drag-request-border" />
</div>
<section className="response-pane flex-grow overflow-x-auto">
<ResponsePane item={item} collection={collection} response={item.response} />
<section className="response-pane flex-grow">
<ResponsePane item={item} collection={collection} rightPaneWidth={rightPaneWidth} response={item.response} />
</section>
</section>

View File

@@ -76,9 +76,7 @@ const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUi
className={`flex items-center justify-between tab-container px-1 ${tab.preview ? "italic" : ""}`}
onMouseUp={handleMouseUp} // Add middle-click behavior here
>
{tab.type === 'folder-settings' && !folder ? (
<RequestTabNotFound handleCloseClick={handleCloseClick} />
) : tab.type === 'folder-settings' ? (
{tab.type === 'folder-settings' ? (
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} />
) : (
<SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} />
@@ -263,13 +261,13 @@ function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, col
return (
<Fragment>
{showAddNewRequestModal && (
<NewRequest collectionUid={collection.uid} onClose={() => setShowAddNewRequestModal(false)} />
<NewRequest collection={collection} onClose={() => setShowAddNewRequestModal(false)} />
)}
{showCloneRequestModal && (
<CloneCollectionItem
item={currentTabItem}
collectionUid={collection.uid}
collection={collection}
onClose={() => setShowCloneRequestModal(false)}
/>
)}

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