Merge branch 'main' into fix/json-serialization-issues

This commit is contained in:
Anoop M D 2024-08-27 14:09:10 +05:30 committed by GitHub
commit 00e98451d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
197 changed files with 7187 additions and 20047 deletions

View File

@ -25,8 +25,14 @@ jobs:
run: | run: |
npm run build --workspace=packages/bruno-common npm run build --workspace=packages/bruno-common
npm run build --workspace=packages/bruno-query npm run build --workspace=packages/bruno-query
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
# tests
- name: Test Package bruno-js
run: npm run test --workspace=packages/bruno-js
- name: Test Package bruno-cli
run: npm run test --workspace=packages/bruno-cli
# test
- name: Test Package bruno-query - name: Test Package bruno-query
run: npm run test --workspace=packages/bruno-query run: npm run test --workspace=packages/bruno-query
- name: Test Package bruno-lang - name: Test Package bruno-lang
@ -35,12 +41,8 @@ jobs:
run: npm run test --workspace=packages/bruno-schema run: npm run test --workspace=packages/bruno-schema
- name: Test Package bruno-app - name: Test Package bruno-app
run: npm run test --workspace=packages/bruno-app run: npm run test --workspace=packages/bruno-app
- name: Test Package bruno-js
run: npm run test --workspace=packages/bruno-js
- name: Test Package bruno-common - name: Test Package bruno-common
run: npm run test --workspace=packages/bruno-common run: npm run test --workspace=packages/bruno-common
- name: Test Package bruno-cli
run: npm run test --workspace=packages/bruno-cli
- name: Test Package bruno-electron - name: Test Package bruno-electron
run: npm run test --workspace=packages/bruno-electron run: npm run test --workspace=packages/bruno-electron
@ -62,6 +64,7 @@ jobs:
run: | run: |
npm run build --workspace=packages/bruno-query npm run build --workspace=packages/bruno-query
npm run build --workspace=packages/bruno-common npm run build --workspace=packages/bruno-common
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
- name: Run tests - name: Run tests
run: | run: |

View File

@ -34,10 +34,11 @@ Libraries we use
- Schema Validation - Yup - Schema Validation - Yup
- Request Client - axios - Request Client - axios
- Filesystem Watcher - chokidar - Filesystem Watcher - chokidar
- i18n - i18next
### Dependencies ### Dependencies
You would need [Node v18.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project You would need [Node v20.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
## Development ## Development
@ -57,6 +58,9 @@ npm run build:graphql-docs
npm run build:bruno-query npm run build:bruno-query
npm run build:bruno-common npm run build:bruno-common
# bundle js sandbox libraries
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
# run next app (terminal 1) # run next app (terminal 1)
npm run dev:web npm run dev:web

View File

@ -37,7 +37,7 @@ Bruno 基于 NextJs 和 React 构建。我们使用 Electron 来封装桌面版
### 依赖项 ### 依赖项
您需要 [Node v18.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我们在这个项目中也使用 npm 工作区_npm workspaces_ 您需要 [Node v20.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我们在这个项目中也使用 npm 工作区_npm workspaces_
## 开发 ## 开发

View File

@ -37,7 +37,7 @@ Bibliotheken die wir benutzen
### Abhängigkeiten ### Abhängigkeiten
Du benötigst [Node v18.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt. 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 ### Lass uns coden

View File

@ -37,7 +37,7 @@ Librerías que utilizamos:
### Dependencias ### Dependencias
Necesitarás [Node v18.x o la última versión LTS](https://nodejs.org/es) y npm 8.x. Ten en cuenta que utilizamos espacios de trabajo de npm en el proyecto. 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 ## Desarrollo

View File

@ -37,7 +37,7 @@ Les librairies que nous utilisons :
### Dépendances ### Dépendances
Vous aurez besoin de [Node v18.x ou la dernière version LTS](https://nodejs.org/en/) et npm 8.x. Nous utilisons aussi les espaces de travail npm (_npm workspaces_) dans ce projet. Vous aurez besoin de [Node v20.x ou la dernière version LTS](https://nodejs.org/en/) et npm 8.x. Nous utilisons aussi les espaces de travail npm (_npm workspaces_) dans ce projet.
## Développement ## Développement

View File

@ -37,7 +37,7 @@ Libraries जिनका हम उपयोग करते हैं
### निर्भरताएँ ### निर्भरताएँ
आपको [Node v18.x या नवीनतम LTS संस्करण](https://nodejs.org/en/) और npm 8.x की आवश्यकता होगी। हम प्रोजेक्ट में npm वर्कस्पेस का उपयोग करते हैं आपको [Node v20.x या नवीनतम LTS संस्करण](https://nodejs.org/en/) और npm 8.x की आवश्यकता होगी। हम प्रोजेक्ट में npm वर्कस्पेस का उपयोग करते हैं
## डेवलपमेंट ## डेवलपमेंट

View File

@ -37,7 +37,7 @@ Le librerie che utilizziamo sono:
### Dependences ### Dependences
Hai bisogno di [Node v18.x o dell'ultima versione LTS](https://nodejs.org/en/) di npm 8.x. Utilizziamo gli spazi di lavoro npm (_npm workspaces_) in questo progetto. Hai bisogno di [Node v20.x o dell'ultima versione LTS](https://nodejs.org/en/) di npm 8.x. Utilizziamo gli spazi di lavoro npm (_npm workspaces_) in questo progetto.
### Iniziamo a codificare ### Iniziamo a codificare

View File

@ -37,7 +37,7 @@ Bruno は Next.js と React で作られています。デスクトップアプ
### 依存関係 ### 依存関係
[Node v18.x もしくは最新の LTS バージョン](https://nodejs.org/en/)と npm 8.x が必要です。プロジェクトに npm ワークスペースを使用しています。 [Node v20.x もしくは最新の LTS バージョン](https://nodejs.org/en/)と npm 8.x が必要です。プロジェクトに npm ワークスペースを使用しています。
## 開発 ## 開発

View File

@ -37,7 +37,7 @@ Bruno는 Next.js와 React로 구축되었습니다. 또한, (로컬 컬렉션을
### 의존성 ### 의존성
[Node v18.x 혹은 최신 LTS version](https://nodejs.org/en/)과 npm 8.x 버전이 필요합니다. 우리는 이 프로젝트에서 npm workspaces를 사용합니다. [Node v20.x 혹은 최신 LTS version](https://nodejs.org/en/)과 npm 8.x 버전이 필요합니다. 우리는 이 프로젝트에서 npm workspaces를 사용합니다.
## 개발 ## 개발

View File

@ -37,7 +37,7 @@ Biblioteki, których używamy
### Zależności ### Zależności
Będziesz potrzebować [Node v18.x lub najnowszej wersji LTS](https://nodejs.org/en/) oraz npm 8.x. W projekcie używamy npm workspaces Będziesz potrzebować [Node v20.x lub najnowszej wersji LTS](https://nodejs.org/en/) oraz npm 8.x. W projekcie używamy npm workspaces
## Rozwój ## Rozwój

View File

@ -37,7 +37,7 @@ Bibliotecas que utilizamos:
### Dependências ### Dependências
Você precisará do [Node v18.x (ou da versão LTS mais recente)](https://nodejs.org/en/) e do npm na versão 8.x. Nós utilizamos npm workspaces no projeto. Você precisará do [Node v20.x (ou da versão LTS mais recente)](https://nodejs.org/en/) e do npm na versão 8.x. Nós utilizamos npm workspaces no projeto.
## Desenvolvimento ## Desenvolvimento

View File

@ -37,7 +37,7 @@ Bibliotecile pe care le folosim
### Dependențele ### Dependențele
Veți avea nevoie de [Node v18.x sau cea mai recentă versiune LTS](https://nodejs.org/en/) și npm 8.x. Noi folosim spații de lucru npm în proiect Veți avea nevoie de [Node v20.x sau cea mai recentă versiune LTS](https://nodejs.org/en/) și npm 8.x. Noi folosim spații de lucru npm în proiect
## Dezvoltarea ## Dezvoltarea

View File

@ -37,7 +37,7 @@ Bruno построен с использованием Next.js и React. Мы т
### Зависимости ### Зависимости
Вам потребуется [Node v18.x или последняя версия LTS](https://nodejs.org/en/) и npm 8.x. В проекте мы используем рабочие пространства npm Вам потребуется [Node v20.x или последняя версия LTS](https://nodejs.org/en/) и npm 8.x. В проекте мы используем рабочие пространства npm
### Приступим к коду ### Приступим к коду

View File

@ -37,7 +37,7 @@ Kullandığımız kütüphaneler
### Bağımlılıklar ### Bağımlılıklar
[Node v18.x veya en son LTS sürümüne](https://nodejs.org/en/) ve npm 8.x'e ihtiyacınız olacaktır. Projede npm çalışma alanlarını kullanıyoruz [Node v20.x veya en son LTS sürümüne](https://nodejs.org/en/) ve npm 8.x'e ihtiyacınız olacaktır. Projede npm çalışma alanlarını kullanıyoruz
## Gelişim ## Gelişim

View File

@ -37,7 +37,7 @@ Bruno побудований на Next.js та React. Також для деск
### Залежності ### Залежності
Вам знадобиться [Node v18.x або остання LTS версія](https://nodejs.org/en/) та npm 8.x. Ми використовуєм npm workspaces в цьому проекті Вам знадобиться [Node v20.x або остання LTS версія](https://nodejs.org/en/) та npm 8.x. Ми використовуєм npm workspaces в цьому проекті
### Починаєм писати код ### Починаєм писати код

View File

@ -37,7 +37,7 @@ Bruno 使用 Next.js 和 React 構建。我們使用 Electron 來封裝及發佈
### 依賴關係 ### 依賴關係
您需要使用 [Node v18.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我們在這個專案中使用 npm 工作區_npm workspaces_ 您需要使用 [Node v20.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我們在這個專案中使用 npm 工作區_npm workspaces_
## 開發 ## 開發

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| **العربية** | **العربية**
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
برونو هو عميل API جديد ومبتكر، يهدف إلى ثورة الحالة الحالية التي يمثلها برنامج Postman وأدوات مماثلة هناك. برونو هو عميل API جديد ومبتكر، يهدف إلى ثورة الحالة الحالية التي يمثلها برنامج Postman وأدوات مماثلة هناك.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
ব্রুনো হল একটি নতুন এবং উদ্ভাবনী API ক্লায়েন্ট, যার লক্ষ্য পোস্টম্যান এবং অনুরূপ সরঞ্জাম দ্বারা প্রতিনিধিত্ব করা স্থিতাবস্থায় বিপ্লব ঘটানো। ব্রুনো হল একটি নতুন এবং উদ্ভাবনী API ক্লায়েন্ট, যার লক্ষ্য পোস্টম্যান এবং অনুরূপ সরঞ্জাম দ্বারা প্রতিনিধিত্ব করা স্থিতাবস্থায় বিপ্লব ঘটানো।

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno 是一款全新且创新的 API 客户端,旨在颠覆 Postman 和其他类似工具。 Bruno 是一款全新且创新的 API 客户端,旨在颠覆 Postman 和其他类似工具。

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno ist ein neuer und innovativer API-Client, der den Status Quo von Postman und ähnlichen Tools revolutionieren soll. Bruno ist ein neuer und innovativer API-Client, der den Status Quo von Postman und ähnlichen Tools revolutionieren soll.

View File

@ -27,6 +27,8 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno es un cliente de APIs nuevo e innovador, creado con el objetivo de revolucionar el panorama actual representado por Postman y otras herramientas similares. Bruno es un cliente de APIs nuevo e innovador, creado con el objetivo de revolucionar el panorama actual representado por Postman y otras herramientas similares.
Bruno almacena tus colecciones directamente en una carpeta de tu sistema de archivos. Usamos un lenguaje de marcado de texto plano, llamado Bru, para guardar información sobre las peticiones a tus APIs. Bruno almacena tus colecciones directamente en una carpeta de tu sistema de archivos. Usamos un lenguaje de marcado de texto plano, llamado Bru, para guardar información sobre las peticiones a tus APIs.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno est un nouveau client API, innovant, qui a pour but de révolutionner le _statu quo_ que représentent Postman et les autres outils. Bruno est un nouveau client API, innovant, qui a pour but de révolutionner le _statu quo_ que représentent Postman et les autres outils.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno è un nuovo ed innovativo API client, mirato a rivoluzionare lo status quo rappresentato da Postman e strumenti simili disponibili. Bruno è un nuovo ed innovativo API client, mirato a rivoluzionare lo status quo rappresentato da Postman e strumenti simili disponibili.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| **日本語** | **日本語**
| [ქართული](./readme_ka.md)
Bruno は革新的な API クライアントです。Postman を代表する API クライアントツールの現状に一石を投じることを目指しています。 Bruno は革新的な API クライアントです。Postman を代表する API クライアントツールの現状に一石を投じることを目指しています。

176
docs/readme/readme_ka.md Normal file
View File

@ -0,0 +1,176 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### ბრუნო - ღია წყაროების IDE API-ების შესწავლისა და ტესტირებისათვის.
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](../../readme.md)
| [Українська](./readme_ua.md)
| [Русский](./readme_ru.md)
| [Türkçe](./readme_tr.md)
| [Deutsch](./readme_de.md)
| [Français](./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)
| **ქართული**
ბრუნო არის ახალი და ინოვაციური API კლიენტი, რომელიც მიზნად ისახავს პოსტმანისა და მსგავსი ინსტრუმენტების არსებული მდგომარეობის რევოლუციას.
ბრუნო თქვენი კოლექციების შენახვას უშუალოდ თქვენს ფაილური სისტემის ერთ-ერთ საქაღალოში ახდენს. ჩვენ ვხმარობთ უბრალო ტექსტურ მარკაპ ენის, Bru-ს, API მოთხოვნების შესახებ ინფორმაციის შენახვისთვის.
თქვენ შეგიძლიათ გამოიყენოთ Git ან ნებისმიერი ვერსიის კონტროლის სისტემა თქვენი API კოლექციების გასაზიარებლად.
ბრუნო მხოლოდ ოფლაინ რეჟიმში მუშაობს. ბრუნოში ღრუბლური სინქრონიზაციის დამატების გეგმები არ არის. ჩვენ ვაფასებთ თქვენი მონაცემების პრივატობას და creemos, რომ ისინი თქვენს მოწყობილობაში უნდა დარჩეს. წაიკითხეთ ჩვენი გრძელვადიანი ხედვა [აქ](https://github.com/usebruno/bruno/discussions/269)
[დამატებით ბრუნო](https://www.usebruno.com/downloads)
📢 შეიტყვეთ ჩვენი უახლესი საუბრის შესახებ 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) ერთჯერადი გადახდით **19 დოლარად**! <br/>
### ინსტალაცია
ბრუნო ხელმისაწვდომია როგორც ბინარული ჩამოტვირთვა [ჩვენ的网站上](https://www.usebruno.com/downloads) Mac-ის, Windows-ისა და Linux-ისთვის.
თქვენ ასევე შეგიძლიათ დააინსტალიროთ ბრუნო პაკეტის მენეჯერების საშუალებით, როგორიცაა 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
# Windows-ზე winget-ის საშუალებით
winget install Bruno.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 [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
```
### პლატფორმებს შორის მუშაობა 🖥️
![bruno](../../assets/images/run-anywhere.png) <br /><br />
### თანამშრომლობა Git-ის საშუალებით 👩‍💻🧑‍💻
ან ნებისმიერი ვერსიის კონტროლის სისტემის საშუალებით
![bruno](../../assets/images/version-control.png) <br /><br />
### სპონსორები
#### ოქროს სპონსორები
<img src="../../assets/images/sponsors/samagata.png" width="150"/>
#### ვერცხლის სპონსორები
<img src="../../assets/images/sponsors/commit-company.png" width="70"/>
#### ბრინჯის სპონსორები
<a href="https://zuplo.link/bruno">
<img src="../../assets/images/sponsors/zuplo.png" width="120"/>
</a>
### მნიშვნელოვანი ბმულები 📌
- [ჩვენი გრძელვადიანი ხედვა](https://github.com/usebruno/bruno/discussions/269)
- [გეგმა](https://github.com/usebruno/bruno/discussions/384)
- [დოკუმენტაცია](https://docs.usebruno.com)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
- [ვებსაიტი](https://www.usebruno.com)
- [ფასები](https://www.usebruno.com/pricing)
- [დამატება](https://www.usebruno.com/downloads)
- [GitHub სპონსორები](https://github.com/sponsors/helloanoop).
### ვიტრინა 🎥
- [მოწონებები](https://github.com/usebruno/bruno/discussions/343)
- [მეცნიერების ჰაბი](https://github.com/usebruno/bruno/discussions/386)
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
### მხარდაჭერა ❤️
თუ გიყვართ ბრუნო და გინდათ მხარი დაუჭიროთ ჩვენს ღია წყაროების მუშაობას, გაითვალისწინეთ ჩვენი დახმარება [GitHub სპონსორების საშუალებით](https://github.com/sponsors/helloanoop).
### გააზიარეთ მოწმობები 📣
თუ ბრუნო დაგეხმარათ თქვენს სამუშაოში და გუნდებში, გთხოვთ, არ დაგავიწყდეთ ჩვენი [მოწონებების გაზიარება ჩვენს GitHub განხილვაში](https://github.com/usebruno/bruno/discussions/343)
### ახალი პაკეტის მენეჯერებში გამოქვეყნება
იხილეთ [აქ](../../publishing.md) მეტი ინფორმაციისათვის.
### დაინტერესდით 🌐
[𝕎 (Twitter)](https://twitter.com/use_bruno) <br />
[ვებსაიტი](https://www.usebruno.com) <br />
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
[LinkedIn](https://www.linkedin.com/company/usebruno)
### სავაჭრო ნიშანი
**სახელი**
`ბრუნო` არის სავაჭრო ნიშანი, რომელსაც ფლობს [ანუპ მ. დ.](https://www.helloanoop.com/)
**ლოგო**
ლოგო არის [OpenMoji](https://openmoji.org/library/emoji-1F436/) სურათებიდან. ლიცენზია: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
### თანამშრომლობა 👩‍💻🧑‍💻
მიხარია, რომ დაინტერესებული ხართ ბრუნოს გაუმჯობესებით. გთხოვთ, გადახედეთ [თანამშრომლობის სახელმძღვანელოს](../../contributing.md)
თუნდაც ვერ მოახერხოთ კოდის საშუალებით კონტრიბუცია, ნუ ინანებთ პრობლემების და ფუნქციის მოთხოვნების ჩაწერას, რომლებიც უნდა განხორციელდეს თქვენი შემთხვევის გადასაჭრელად.
### ავტორები
<div align="center">
<a href="https://github.com/usebruno/bruno/graphs/contributors">
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
</a>
</div>
### ლიცენზია 📄
[MIT](../../license.md)

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno는 새롭고 혁신적인 API 클라이언트로, Postman과 유사한 툴들을 혁신하는 것을 목표로 합니다. Bruno는 새롭고 혁신적인 API 클라이언트로, Postman과 유사한 툴들을 혁신하는 것을 목표로 합니다.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno to nowy i innowacyjny klient API, którego celem jest zrewolucjonizowanie status quo reprezentowanego przez narzędzia takie jak Postman. Bruno to nowy i innowacyjny klient API, którego celem jest zrewolucjonizowanie status quo reprezentowanego przez narzędzia takie jak Postman.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno é um novo e inovador cliente de API, com o objetivo de revolucionar o status quo representado por ferramentas como o Postman e outras semelhantes. Bruno é um novo e inovador cliente de API, com o objetivo de revolucionar o status quo representado por ferramentas como o Postman e outras semelhantes.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno este un client API nou și inovativ, care vizează să revoluționeze status quo-ul reprezentat de Postman și alte instrumente similare. Bruno este un client API nou și inovativ, care vizează să revoluționeze status quo-ul reprezentat de Postman și alte instrumente similare.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами. Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno, Postman ve benzeri araçlar tarafından temsil edilen statükoda devrim yaratmayı amaçlayan yeni ve yenilikçi bir API istemcisidir. Bruno, Postman ve benzeri araçlar tarafından temsil edilen statükoda devrim yaratmayı amaçlayan yeni ve yenilikçi bir API istemcisidir.

View File

@ -27,6 +27,7 @@
| [正體中文](./readme_zhtw.md) | [正體中文](./readme_zhtw.md)
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman. Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman.

View File

@ -27,6 +27,7 @@
| **正體中文** | **正體中文**
| [العربية](./readme_ar.md) | [العربية](./readme_ar.md)
| [日本語](./readme_ja.md) | [日本語](./readme_ja.md)
| [ქართული](./readme_ka.md)
Bruno 是一個全新且有創新性的 API 用戶端,目的在徹底改變以 Postman 和其他類似工具的現況。 Bruno 是一個全新且有創新性的 API 用戶端,目的在徹底改變以 Postman 和其他類似工具的現況。

22998
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,14 +20,17 @@
"@jest/globals": "^29.2.0", "@jest/globals": "^29.2.0",
"@playwright/test": "^1.27.1", "@playwright/test": "^1.27.1",
"@types/jest": "^29.5.11", "@types/jest": "^29.5.11",
"concurrently": "^8.2.2",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"husky": "^8.0.3", "husky": "^8.0.3",
"jest": "^29.2.0", "jest": "^29.2.0",
"pretty-quick": "^3.1.3", "pretty-quick": "^3.1.3",
"randomstring": "^1.2.2", "randomstring": "^1.2.2",
"rimraf": "^6.0.1",
"ts-jest": "^29.0.5" "ts-jest": "^29.0.5"
}, },
"scripts": { "scripts": {
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
"dev:web": "npm run dev --workspace=packages/bruno-app", "dev:web": "npm run dev --workspace=packages/bruno-app",
"build:web": "npm run build --workspace=packages/bruno-app", "build:web": "npm run build --workspace=packages/bruno-app",
"prettier:web": "npm run prettier --workspace=packages/bruno-app", "prettier:web": "npm run prettier --workspace=packages/bruno-app",
@ -47,9 +50,8 @@
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app", "test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
"prepare": "husky install" "prepare": "husky install"
}, },
"overrides": { "overrides": {
"rollup": "3.2.5" "rollup":"3.29.4"
}, },
"dependencies": { "dependencies": {
"json-bigint": "^1.0.0", "json-bigint": "^1.0.0",

View File

@ -12,6 +12,7 @@
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\"" "prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
}, },
"dependencies": { "dependencies": {
"@fontsource/inter": "^5.0.15",
"@fortawesome/fontawesome-svg-core": "^1.2.36", "@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.16", "@fortawesome/react-fontawesome": "^0.1.16",
@ -35,7 +36,8 @@
"graphiql": "^1.5.9", "graphiql": "^1.5.9",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"graphql-request": "^3.7.0", "graphql-request": "^3.7.0",
"httpsnippet": "^3.0.1", "httpsnippet": "^3.0.6",
"i18next": "^23.14.0",
"idb": "^7.0.0", "idb": "^7.0.0",
"immer": "^9.0.15", "immer": "^9.0.15",
"jsesc": "^3.0.2", "jsesc": "^3.0.2",
@ -47,6 +49,7 @@
"know-your-http-well": "^0.5.0", "know-your-http-well": "^0.5.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-it": "^13.0.2", "markdown-it": "^13.0.2",
"markdown-it-replace-link": "^1.2.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"next": "12.3.3", "next": "12.3.3",
@ -64,6 +67,7 @@
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-github-btn": "^1.4.0", "react-github-btn": "^1.4.0",
"react-hot-toast": "^2.4.0", "react-hot-toast": "^2.4.0",
"react-i18next": "^15.0.1",
"react-inspector": "^6.0.2", "react-inspector": "^6.0.2",
"react-pdf": "^7.5.1", "react-pdf": "^7.5.1",
"react-redux": "^7.2.6", "react-redux": "^7.2.6",

View File

@ -14,6 +14,24 @@ const StyledWrapper = styled.div`
background: #d2d7db; background: #d2d7db;
} }
.CodeMirror-dialog {
overflow: visible;
}
#search-results-count {
display: inline-block;
position: absolute;
top: calc(100% + 1px);
right: 0;
border-width: 0 0 1px 1px;
border-style: solid;
border-color: ${(props) => props.theme.codemirror.border};
padding: 0.1em 0.8em;
background-color: ${(props) => props.theme.codemirror.bg};
color: rgb(102, 102, 102);
white-space: nowrap;
}
textarea.cm-editor { textarea.cm-editor {
position: relative; position: relative;
} }

View File

@ -68,6 +68,8 @@ if (!SERVER_RENDERED) {
'bru.deleteVar(key)', 'bru.deleteVar(key)',
'bru.setNextRequest(requestName)', 'bru.setNextRequest(requestName)',
'req.disableParsingResponseJson()' 'req.disableParsingResponseJson()'
'bru.getRequestVar(key)',
'bru.sleep(ms)'
]; ];
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => { CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
const cursor = editor.getCursor(); const cursor = editor.getCursor();
@ -110,6 +112,7 @@ export default class CodeEditor extends React.Component {
// unnecessary updates during the update lifecycle. // unnecessary updates during the update lifecycle.
this.cachedValue = props.value || ''; this.cachedValue = props.value || '';
this.variables = {}; this.variables = {};
this.searchResultsCountElementId = 'search-results-count';
this.lintOptions = { this.lintOptions = {
esversion: 11, esversion: 11,
@ -156,8 +159,16 @@ export default class CodeEditor extends React.Component {
this.props.onSave(); this.props.onSave();
} }
}, },
'Cmd-F': 'findPersistent', 'Cmd-F': (cm) => {
'Ctrl-F': 'findPersistent', cm.execCommand('findPersistent');
this._bindSearchHandler();
this._appendSearchResultsCount();
},
'Ctrl-F': (cm) => {
cm.execCommand('findPersistent');
this._bindSearchHandler();
this._appendSearchResultsCount();
},
'Cmd-H': 'replace', 'Cmd-H': 'replace',
'Ctrl-H': 'replace', 'Ctrl-H': 'replace',
Tab: function (cm) { Tab: function (cm) {
@ -309,6 +320,8 @@ export default class CodeEditor extends React.Component {
this.editor.off('change', this._onEdit); this.editor.off('change', this._onEdit);
this.editor = null; this.editor = null;
} }
this._unbindSearchHandler();
} }
render() { render() {
@ -345,4 +358,62 @@ export default class CodeEditor extends React.Component {
} }
} }
}; };
/**
* Bind handler to search input to count number of search results
*/
_bindSearchHandler = () => {
const searchInput = document.querySelector('.CodeMirror-search-field');
if (searchInput) {
searchInput.addEventListener('input', this._countSearchResults);
}
};
/**
* Unbind handler to search input to count number of search results
*/
_unbindSearchHandler = () => {
const searchInput = document.querySelector('.CodeMirror-search-field');
if (searchInput) {
searchInput.removeEventListener('input', this._countSearchResults);
}
};
/**
* Append search results count to search dialog
*/
_appendSearchResultsCount = () => {
const dialog = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
if (dialog) {
const searchResultsCount = document.createElement('span');
searchResultsCount.id = this.searchResultsCountElementId;
dialog.appendChild(searchResultsCount);
this._countSearchResults();
}
};
/**
* Count search results and update state
*/
_countSearchResults = () => {
let count = 0;
const searchInput = document.querySelector('.CodeMirror-search-field');
if (searchInput && searchInput.value.length > 0) {
const text = new RegExp(searchInput.value, 'gi');
const matches = this.editor.getValue().match(text);
count = matches ? matches.length : 0;
}
const searchResultsCountElement = document.querySelector(`#${this.searchResultsCountElementId}`);
if (searchResultsCountElement) {
searchResultsCountElement.innerText = `${count} results`;
}
};
} }

View File

@ -48,7 +48,7 @@ const Docs = ({ collection }) => {
font={get(preferences, 'font.codeFont', 'default')} font={get(preferences, 'font.codeFont', 'default')}
/> />
) : ( ) : (
<Markdown onDoubleClick={toggleViewMode} content={docs} /> <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
)} )}
</StyledWrapper> </StyledWrapper>
); );

View File

@ -74,6 +74,7 @@ const PresetsSettings = ({ collection }) => {
id="request-url" id="request-url"
type="text" type="text"
name="requestUrl" name="requestUrl"
placeholder='Request URL'
className="block textbox" className="block textbox"
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"

View File

@ -53,7 +53,7 @@ const Documentation = ({ item, collection }) => {
mode="application/text" mode="application/text"
/> />
) : ( ) : (
<Markdown onDoubleClick={toggleViewMode} content={docs} /> <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
)} )}
</StyledWrapper> </StyledWrapper>
); );

View File

@ -40,10 +40,15 @@ const Wrapper = styled.div`
color: ${(props) => props.theme.dropdown.iconColor}; color: ${(props) => props.theme.dropdown.iconColor};
} }
&:hover { &:hover:not(:disabled) {
background-color: ${(props) => props.theme.dropdown.hoverBg}; background-color: ${(props) => props.theme.dropdown.hoverBg};
} }
&:disabled {
cursor: not-allowed;
color: gray;
}
&.border-top { &.border-top {
border-top: solid 1px ${(props) => props.theme.dropdown.separator}; border-top: solid 1px ${(props) => props.theme.dropdown.separator};
} }

View File

@ -44,8 +44,8 @@ const Script = ({ collection, folder }) => {
<div className="text-xs mb-4 text-muted"> <div className="text-xs mb-4 text-muted">
Pre and post-request scripts that will run before and after any request inside this folder is sent. Pre and post-request scripts that will run before and after any request inside this folder is sent.
</div> </div>
<div className="flex-1 mt-2"> <div className="flex flex-col flex-1 mt-2 gap-y-2">
<div className="mb-1 title text-xs">Pre Request</div> <div className="title text-xs">Pre Request</div>
<CodeEditor <CodeEditor
collection={collection} collection={collection}
value={requestScript || ''} value={requestScript || ''}
@ -56,8 +56,8 @@ const Script = ({ collection, folder }) => {
font={get(preferences, 'font.codeFont', 'default')} font={get(preferences, 'font.codeFont', 'default')}
/> />
</div> </div>
<div className="flex-1 mt-6"> <div className="flex flex-col flex-1 mt-2 gap-y-2">
<div className="mt-1 mb-1 title text-xs">Post Response</div> <div className="title text-xs">Post Response</div>
<CodeEditor <CodeEditor
collection={collection} collection={collection}
value={responseScript || ''} value={responseScript || ''}

View File

@ -19,8 +19,8 @@ const FolderSettings = ({ collection, folder }) => {
const setTab = (tab) => { const setTab = (tab) => {
dispatch( dispatch(
updatedFolderSettingsSelectedTab({ updatedFolderSettingsSelectedTab({
collectionUid: collection.uid, collectionUid: collection?.uid,
folderUid: folder.uid, folderUid: folder?.uid,
tab tab
}) })
); );
@ -50,7 +50,7 @@ const FolderSettings = ({ collection, folder }) => {
}; };
return ( return (
<StyledWrapper> <StyledWrapper className="flex flex-col h-full">
<div className="flex flex-col h-full relative px-4 py-4"> <div className="flex flex-col h-full relative px-4 py-4">
<div className="flex flex-wrap items-center tabs" role="tablist"> <div className="flex flex-wrap items-center tabs" role="tablist">
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}> <div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>

View File

@ -0,0 +1,16 @@
import React from 'react';
const DotIcon = ({ width }) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={width}
viewBox="0 0 24 24" strokeWidth="1.5"
stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round"
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" stroke-width="0" fill="currentColor" />
</svg>
);
};
export default DotIcon;

View File

@ -69,6 +69,7 @@ const StyledMarkdownBodyWrapper = styled.div`
pre { pre {
background: ${(props) => props.theme.sidebar.bg}; background: ${(props) => props.theme.sidebar.bg};
color: ${(props) => props.theme.text};
} }
table { table {

View File

@ -1,10 +1,15 @@
import MarkdownIt from 'markdown-it'; import MarkdownIt from 'markdown-it';
import * as MarkdownItReplaceLink from 'markdown-it-replace-link';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import React from 'react'; import React from 'react';
const md = new MarkdownIt(); const Markdown = ({ collectionPath, onDoubleClick, content }) => {
const markdownItOptions = {
replaceLink: function (link, env) {
return link.replace(/^\./, collectionPath);
}
};
const Markdown = ({ onDoubleClick, content }) => {
const handleOnClick = (event) => { const handleOnClick = (event) => {
const target = event.target; const target = event.target;
if (target.tagName === 'A') { if (target.tagName === 'A') {
@ -23,6 +28,8 @@ const Markdown = ({ onDoubleClick, content }) => {
} }
}; };
const md = new MarkdownIt(markdownItOptions).use(MarkdownItReplaceLink);
const htmlFromMarkdown = md.render(content || ''); const htmlFromMarkdown = md.render(content || '');
return ( return (

View File

@ -1,10 +1,10 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
const ModalHeader = ({ title, handleCancel, customHeader }) => ( const ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (
<div className="bruno-modal-header"> <div className="bruno-modal-header">
{customHeader ? customHeader : <>{title ? <div className="bruno-modal-header-title">{title}</div> : null}</>} {customHeader ? customHeader : <>{title ? <div className="bruno-modal-header-title">{title}</div> : null}</>}
{handleCancel ? ( {handleCancel && !hideClose ? (
<div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null}> <div className="close cursor-pointer" onClick={handleCancel ? () => handleCancel() : null}>
× ×
</div> </div>
@ -63,6 +63,7 @@ const Modal = ({
confirmDisabled, confirmDisabled,
hideCancel, hideCancel,
hideFooter, hideFooter,
hideClose,
disableCloseOnOutsideClick, disableCloseOnOutsideClick,
disableEscapeKey, disableEscapeKey,
onClick, onClick,
@ -100,7 +101,12 @@ const Modal = ({
return ( return (
<StyledWrapper className={classes} onClick={onClick ? (e) => onClick(e) : null}> <StyledWrapper className={classes} onClick={onClick ? (e) => onClick(e) : null}>
<div className={`bruno-modal-card modal-${size}`}> <div className={`bruno-modal-card modal-${size}`}>
<ModalHeader title={title} handleCancel={() => closeModal({ type: 'icon' })} customHeader={customHeader} /> <ModalHeader
title={title}
hideClose={hideClose}
handleCancel={() => closeModal({ type: 'icon' })}
customHeader={customHeader}
/>
<ModalContent>{children}</ModalContent> <ModalContent>{children}</ModalContent>
<ModalFooter <ModalFooter
confirmText={confirmText} confirmText={confirmText}

View File

@ -17,7 +17,7 @@ const StyledWrapper = styled.div`
overflow: hidden !important; overflow: hidden !important;
${'' /* padding-bottom: 50px !important; */} ${'' /* padding-bottom: 50px !important; */}
position: relative; position: relative;
display: contents; display: block;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
} }

View File

@ -1,39 +1,42 @@
import React from 'react'; import React from 'react';
import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons'; import { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import { useTranslation } from 'react-i18next';
const Support = () => { const Support = () => {
const { t } = useTranslation();
return ( return (
<StyledWrapper> <StyledWrapper>
<div className="rows"> <div className="rows">
<div className="mt-2"> <div className="mt-2">
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end"> <a href="https://docs.usebruno.com" target="_blank" className="flex items-end">
<IconBook size={18} strokeWidth={2} /> <IconBook size={18} strokeWidth={2} />
<span className="label ml-2">Documentation</span> <span className="label ml-2">{t('COMMON.DOCUMENTATION')}</span>
</a> </a>
</div> </div>
<div className="mt-2"> <div className="mt-2">
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end"> <a href="https://github.com/usebruno/bruno/issues" target="_blank" className="flex items-end">
<IconSpeakerphone size={18} strokeWidth={2} /> <IconSpeakerphone size={18} strokeWidth={2} />
<span className="label ml-2">Report Issues</span> <span className="label ml-2">{t('COMMON.REPORT_ISSUES')}</span>
</a> </a>
</div> </div>
<div className="mt-2"> <div className="mt-2">
<a href="https://discord.com/invite/KgcZUncpjq" target="_blank" className="flex items-end"> <a href="https://discord.com/invite/KgcZUncpjq" target="_blank" className="flex items-end">
<IconBrandDiscord size={18} strokeWidth={2} /> <IconBrandDiscord size={18} strokeWidth={2} />
<span className="label ml-2">Discord</span> <span className="label ml-2">{t('COMMON.DISCORD')}</span>
</a> </a>
</div> </div>
<div className="mt-2"> <div className="mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end"> <a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-end">
<IconBrandGithub size={18} strokeWidth={2} /> <IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">GitHub</span> <span className="label ml-2">{t('COMMON.GITHUB')}</span>
</a> </a>
</div> </div>
<div className="mt-2"> <div className="mt-2">
<a href="https://twitter.com/use_bruno" target="_blank" className="flex items-end"> <a href="https://twitter.com/use_bruno" target="_blank" className="flex items-end">
<IconBrandTwitter size={18} strokeWidth={2} /> <IconBrandTwitter size={18} strokeWidth={2} />
<span className="label ml-2">Twitter</span> <span className="label ml-2">{t('COMMON.TWITTER')}</span>
</a> </a>
</div> </div>
</div> </div>

View File

@ -1,7 +1,4 @@
import React from 'react'; import React from 'react';
import { useTheme } from 'providers/Theme/index';
import darkTheme from 'themes/dark';
import lightTheme from 'themes/light';
/** /**
* Assertion operators * Assertion operators
@ -81,16 +78,10 @@ const AssertionOperator = ({ operator, onChange }) => {
} }
}; };
const { storedTheme } = useTheme();
return ( return (
<select value={operator} onChange={handleChange} className="mousetrap"> <select value={operator} onChange={handleChange} className="mousetrap">
{operators.map((operator) => ( {operators.map((operator) => (
<option <option key={operator} value={operator}>
style={{ backgroundColor: storedTheme === 'dark' ? darkTheme.bg : lightTheme.bg }}
key={operator}
value={operator}
>
{getLabel(operator)} {getLabel(operator)}
</option> </option>
))} ))}

View File

@ -55,6 +55,9 @@ const Wrapper = styled.div`
position: relative; position: relative;
top: 1px; top: 1px;
} }
option {
background-color: ${(props) => props.theme.bg};
}
`; `;
export default Wrapper; export default Wrapper;

View File

@ -23,6 +23,10 @@ const StyledWrapper = styled.div`
color: ${(props) => props.theme.tabs.active.color} !important; color: ${(props) => props.theme.tabs.active.color} !important;
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important; border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
} }
.content-indicator {
color: ${(props) => props.theme.text}
}
} }
} }
`; `;

View File

@ -7,7 +7,7 @@ import RequestHeaders from 'components/RequestPane/RequestHeaders';
import RequestBody from 'components/RequestPane/RequestBody'; import RequestBody from 'components/RequestPane/RequestBody';
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode'; import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
import Auth from 'components/RequestPane/Auth'; import Auth from 'components/RequestPane/Auth';
import AuthMode from 'components/RequestPane/Auth/AuthMode'; import DotIcon from 'components/Icons/Dot';
import Vars from 'components/RequestPane/Vars'; import Vars from 'components/RequestPane/Vars';
import Assertions from 'components/RequestPane/Assertions'; import Assertions from 'components/RequestPane/Assertions';
import Script from 'components/RequestPane/Script'; import Script from 'components/RequestPane/Script';
@ -16,6 +16,12 @@ import StyledWrapper from './StyledWrapper';
import { find, get } from 'lodash'; import { find, get } from 'lodash';
import Documentation from 'components/Documentation/index'; import Documentation from 'components/Documentation/index';
const ContentIndicator = () => {
return <sup className="ml-[.125rem] opacity-80 font-medium">
<DotIcon width="10"></DotIcon>
</sup>
};
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => { const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const tabs = useSelector((state) => state.tabs.tabs); const tabs = useSelector((state) => state.tabs.tabs);
@ -82,12 +88,17 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
const isMultipleContentTab = ['params', 'script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab); const isMultipleContentTab = ['params', 'script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab);
// get the length of active params, headers, asserts and vars // get the length of active params, headers, asserts and vars as well as the contents of the body, tests and script
const params = item.draft ? get(item, 'draft.request.params', []) : get(item, 'request.params', []); const getPropertyFromDraftOrRequest = (propertyKey) =>
const headers = item.draft ? get(item, 'draft.request.headers', []) : get(item, 'request.headers', []); item.draft ? get(item, `draft.${propertyKey}`, []) : get(item, propertyKey, []);
const assertions = item.draft ? get(item, 'draft.request.assertions', []) : get(item, 'request.assertions', []); const params = getPropertyFromDraftOrRequest('request.params');
const requestVars = item.draft ? get(item, 'draft.request.vars.req', []) : get(item, 'request.vars.req', []); const body = getPropertyFromDraftOrRequest('request.body');
const responseVars = item.draft ? get(item, 'draft.request.vars.res', []) : get(item, 'request.vars.res', []); const headers = getPropertyFromDraftOrRequest('request.headers');
const script = getPropertyFromDraftOrRequest('request.script');
const assertions = getPropertyFromDraftOrRequest('request.assertions');
const tests = getPropertyFromDraftOrRequest('request.tests');
const requestVars = getPropertyFromDraftOrRequest('request.vars.req');
const responseVars = getPropertyFromDraftOrRequest('request.vars.res');
const activeParamsLength = params.filter((param) => param.enabled).length; const activeParamsLength = params.filter((param) => param.enabled).length;
const activeHeadersLength = headers.filter((header) => header.enabled).length; const activeHeadersLength = headers.filter((header) => header.enabled).length;
@ -105,10 +116,11 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
</div> </div>
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}> <div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
Body Body
{body.mode !== 'none' && <ContentIndicator />}
</div> </div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}> <div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers Headers
{activeHeadersLength > 0 && <sup className="ml-1 font-medium">{activeHeadersLength}</sup>} {activeHeadersLength > 0 && <sup className="ml-[.125rem] font-medium">{activeHeadersLength}</sup>}
</div> </div>
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}> <div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
Auth Auth
@ -119,6 +131,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
</div> </div>
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}> <div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
Script Script
{(script.req || script.res) && <ContentIndicator />}
</div> </div>
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}> <div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
Assert Assert
@ -126,6 +139,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
</div> </div>
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}> <div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
Tests Tests
{tests && <ContentIndicator />}
</div> </div>
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}> <div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
Docs Docs

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import get from 'lodash/get'; import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import has from 'lodash/has'; import Tooltip from 'components/Tooltip';
import { IconTrash } from '@tabler/icons'; import { IconTrash } from '@tabler/icons';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useTheme } from 'providers/Theme'; import { useTheme } from 'providers/Theme';
@ -103,7 +103,7 @@ const QueryParams = ({ item, collection }) => {
return ( return (
<StyledWrapper className="w-full flex flex-col"> <StyledWrapper className="w-full flex flex-col">
<div className="flex-1 mt-2"> <div className="flex-1 mt-2">
<div className="mb-1 title text-xs">Query</div> <div className="mb-2 title text-xs">Query</div>
<table> <table>
<thead> <thead>
<tr> <tr>
@ -173,7 +173,22 @@ const QueryParams = ({ item, collection }) => {
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddQueryParam}> <button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddQueryParam}>
+&nbsp;<span>Add Param</span> +&nbsp;<span>Add Param</span>
</button> </button>
<div className="mb-1 title text-xs">Path</div> <div className="mb-2 title text-xs flex items-stretch">
<span>Path</span>
<Tooltip
text={`
<div>
Path variables are automatically added whenever the
<code className="font-mono mx-2">:name</code>
template is used in the URL. <br/> For example:
<code className="font-mono mx-2">
https://example.com/v1/users/<span>:id</span>
</code>
</div>
`}
tooltipId="path-param-tooltip"
/>
</div>
<table> <table>
<thead> <thead>
<tr> <tr>
@ -224,6 +239,11 @@ const QueryParams = ({ item, collection }) => {
: null} : null}
</tbody> </tbody>
</table> </table>
{!(pathParams && pathParams.length) ?
<div className="title pr-2 py-3 mt-2 text-xs">
</div>
: null}
</div> </div>
</StyledWrapper> </StyledWrapper>
); );

View File

@ -74,7 +74,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
/> />
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}> <div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
<div <div
className="tooltip mr-3" className="tooltip mx-3"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (!item.draft) return; if (!item.draft) return;

View File

@ -30,7 +30,7 @@ const RequestNotFound = ({ itemUid }) => {
return ( return (
<div className="mt-6 px-6"> <div className="mt-6 px-6">
<div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700 bg-yellow-100 p-4"> <div className="p-4 bg-orange-100 border-l-4 border-yellow-500 text-yellow-700">
<div>Request no longer exists.</div> <div>Request no longer exists.</div>
<div className="mt-2"> <div className="mt-2">
This can happen when the .bru file associated with this request was deleted on your filesystem. This can happen when the .bru file associated with this request was deleted on your filesystem.

View File

@ -18,6 +18,7 @@ import CollectionSettings from 'components/CollectionSettings';
import { DocExplorer } from '@usebruno/graphql-docs'; import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import SecuritySettings from 'components/SecuritySettings';
import FolderSettings from 'components/FolderSettings'; import FolderSettings from 'components/FolderSettings';
const MIN_LEFT_PANE_WIDTH = 300; const MIN_LEFT_PANE_WIDTH = 300;
@ -137,6 +138,10 @@ const RequestTabPanel = () => {
return <FolderSettings collection={collection} folder={folder} />; return <FolderSettings collection={collection} folder={folder} />;
} }
if (focusedTab.type === 'security-settings') {
return <SecuritySettings collection={collection} />;
}
const item = findItemInCollection(collection, activeTabUid); const item = findItemInCollection(collection, activeTabUid);
if (!item || !item.uid) { if (!item || !item.uid) {
return <RequestNotFound itemUid={activeTabUid} />; return <RequestNotFound itemUid={activeTabUid} />;

View File

@ -5,6 +5,7 @@ import EnvironmentSelector from 'components/Environments/EnvironmentSelector';
import { addTab } from 'providers/ReduxStore/slices/tabs'; import { addTab } from 'providers/ReduxStore/slices/tabs';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import JsSandboxMode from 'components/SecuritySettings/JsSandboxMode';
const CollectionToolBar = ({ collection }) => { const CollectionToolBar = ({ collection }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -47,6 +48,9 @@ const CollectionToolBar = ({ collection }) => {
<span className="ml-2 mr-4 font-semibold">{collection?.name}</span> <span className="ml-2 mr-4 font-semibold">{collection?.name}</span>
</div> </div>
<div className="flex flex-1 items-center justify-end"> <div className="flex flex-1 items-center justify-end">
<span className="mr-2">
<JsSandboxMode collection={collection} />
</span>
<span className="mr-2"> <span className="mr-2">
<IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} /> <IconRun className="cursor-pointer" size={20} strokeWidth={1.5} onClick={handleRun} />
</span> </span>

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { IconVariable, IconSettings, IconRun, IconFolder } from '@tabler/icons'; import { IconVariable, IconSettings, IconRun, IconFolder, IconShieldLock } from '@tabler/icons';
const SpecialTab = ({ handleCloseClick, type, tabName }) => { const SpecialTab = ({ handleCloseClick, type, tabName }) => {
const getTabInfo = (type, tabName) => { const getTabInfo = (type, tabName) => {
@ -12,6 +12,14 @@ const SpecialTab = ({ handleCloseClick, type, tabName }) => {
</> </>
); );
} }
case 'security-settings': {
return (
<>
<IconShieldLock size={18} strokeWidth={1.5} className="text-yellow-600" />
<span className="ml-1">Security</span>
</>
)
}
case 'folder-settings': { case 'folder-settings': {
return ( return (
<div className="flex items-center flex-nowrap overflow-hidden"> <div className="flex items-center flex-nowrap overflow-hidden">

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useState, useRef, Fragment } from 'react';
import get from 'lodash/get'; import get from 'lodash/get';
import { closeTabs } from 'providers/ReduxStore/slices/tabs'; import { closeTabs } from 'providers/ReduxStore/slices/tabs';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
@ -12,12 +12,18 @@ import ConfirmRequestClose from './ConfirmRequestClose';
import RequestTabNotFound from './RequestTabNotFound'; import RequestTabNotFound from './RequestTabNotFound';
import SpecialTab from './SpecialTab'; import SpecialTab from './SpecialTab';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import Dropdown from 'components/Dropdown';
import CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';
import NewRequest from 'components/Sidebar/NewRequest/index';
const RequestTab = ({ tab, collection, folderUid }) => { const RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { storedTheme } = useTheme(); const { storedTheme } = useTheme();
const [showConfirmClose, setShowConfirmClose] = useState(false); const [showConfirmClose, setShowConfirmClose] = useState(false);
const dropdownTippyRef = useRef();
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
const handleCloseClick = (event) => { const handleCloseClick = (event) => {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
@ -28,6 +34,19 @@ const RequestTab = ({ tab, collection, folderUid }) => {
); );
}; };
const handleRightClick = (_event) => {
const menuDropdown = dropdownTippyRef.current;
if (!menuDropdown) {
return;
}
if (menuDropdown.state.isShown) {
menuDropdown.hide();
} else {
menuDropdown.show();
}
};
const handleMouseUp = (e) => { const handleMouseUp = (e) => {
if (e.button === 1) { if (e.button === 1) {
e.stopPropagation(); e.stopPropagation();
@ -43,45 +62,11 @@ const RequestTab = ({ tab, collection, folderUid }) => {
const getMethodColor = (method = '') => { const getMethodColor = (method = '') => {
const theme = storedTheme === 'dark' ? darkTheme : lightTheme; const theme = storedTheme === 'dark' ? darkTheme : lightTheme;
return theme.request.methods[method.toLocaleLowerCase()];
let color = '';
method = method.toLocaleLowerCase();
switch (method) {
case 'get': {
color = theme.request.methods.get;
break;
}
case 'post': {
color = theme.request.methods.post;
break;
}
case 'put': {
color = theme.request.methods.put;
break;
}
case 'delete': {
color = theme.request.methods.delete;
break;
}
case 'patch': {
color = theme.request.methods.patch;
break;
}
case 'options': {
color = theme.request.methods.options;
break;
}
case 'head': {
color = theme.request.methods.head;
break;
}
}
return color;
}; };
const folder = folderUid ? findItemInCollection(collection, folderUid) : null; const folder = folderUid ? findItemInCollection(collection, folderUid) : null;
if (['collection-settings', 'folder-settings', 'variables', 'collection-runner'].includes(tab.type)) { if (['collection-settings', 'folder-settings', 'variables', 'collection-runner', 'security-settings'].includes(tab.type)) {
return ( return (
<StyledWrapper className="flex items-center justify-between tab-container px-1"> <StyledWrapper className="flex items-center justify-between tab-container px-1">
{tab.type === 'folder-settings' ? ( {tab.type === 'folder-settings' ? (
@ -143,6 +128,7 @@ const RequestTab = ({ tab, collection, folderUid }) => {
)} )}
<div <div
className="flex items-baseline tab-label pl-2" className="flex items-baseline tab-label pl-2"
onContextMenu={handleRightClick}
onMouseUp={(e) => { onMouseUp={(e) => {
if (!item.draft) return handleMouseUp(e); if (!item.draft) return handleMouseUp(e);
@ -159,6 +145,15 @@ const RequestTab = ({ tab, collection, folderUid }) => {
<span className="ml-1 tab-name" title={item.name}> <span className="ml-1 tab-name" title={item.name}>
{item.name} {item.name}
</span> </span>
<RequestTabMenu
onDropdownCreate={onDropdownCreate}
tabIndex={tabIndex}
collectionRequestTabs={collectionRequestTabs}
tabItem={item}
collection={collection}
dropdownTippyRef={dropdownTippyRef}
dispatch={dispatch}
/>
</div> </div>
<div <div
className="flex px-2 close-icon-container" className="flex px-2 close-icon-container"
@ -195,4 +190,124 @@ const RequestTab = ({ tab, collection, folderUid }) => {
); );
}; };
function RequestTabMenu({ onDropdownCreate, collectionRequestTabs, tabIndex, collection, dropdownTippyRef, dispatch }) {
const [showCloneRequestModal, setShowCloneRequestModal] = useState(false);
const [showAddNewRequestModal, setShowAddNewRequestModal] = useState(false);
const totalTabs = collectionRequestTabs.length || 0;
const currentTabUid = collectionRequestTabs[tabIndex]?.uid;
const currentTabItem = findItemInCollection(collection, currentTabUid);
const hasLeftTabs = tabIndex !== 0;
const hasRightTabs = totalTabs > tabIndex + 1;
const hasOtherTabs = totalTabs > 1;
async function handleCloseTab(event, tabUid) {
event.stopPropagation();
dropdownTippyRef.current.hide();
if (!tabUid) {
return;
}
try {
const item = findItemInCollection(collection, tabUid);
// silently save unsaved changes before closing the tab
if (item.draft) {
await dispatch(saveRequest(item.uid, collection.uid, true));
}
dispatch(closeTabs({ tabUids: [tabUid] }));
} catch (err) {}
}
function handleCloseOtherTabs(event) {
dropdownTippyRef.current.hide();
const otherTabs = collectionRequestTabs.filter((_, index) => index !== tabIndex);
otherTabs.forEach((tab) => handleCloseTab(event, tab.uid));
}
function handleCloseTabsToTheLeft(event) {
dropdownTippyRef.current.hide();
const leftTabs = collectionRequestTabs.filter((_, index) => index < tabIndex);
leftTabs.forEach((tab) => handleCloseTab(event, tab.uid));
}
function handleCloseTabsToTheRight(event) {
dropdownTippyRef.current.hide();
const rightTabs = collectionRequestTabs.filter((_, index) => index > tabIndex);
rightTabs.forEach((tab) => handleCloseTab(event, tab.uid));
}
function handleCloseSavedTabs(event) {
event.stopPropagation();
const savedTabs = collection.items.filter((item) => !item.draft);
const savedTabIds = savedTabs.map((item) => item.uid) || [];
dispatch(closeTabs({ tabUids: savedTabIds }));
}
function handleCloseAllTabs(event) {
collectionRequestTabs.forEach((tab) => handleCloseTab(event, tab.uid));
}
return (
<Fragment>
{showAddNewRequestModal && (
<NewRequest collection={collection} onClose={() => setShowAddNewRequestModal(false)} />
)}
{showCloneRequestModal && (
<CloneCollectionItem
item={currentTabItem}
collection={collection}
onClose={() => setShowCloneRequestModal(false)}
/>
)}
<Dropdown onCreate={onDropdownCreate} icon={<span></span>} placement="bottom-start">
<button
className="dropdown-item w-full"
onClick={() => {
dropdownTippyRef.current.hide();
setShowAddNewRequestModal(true);
}}
>
New Request
</button>
<button
className="dropdown-item w-full"
onClick={() => {
dropdownTippyRef.current.hide();
setShowCloneRequestModal(true);
}}
>
Clone Request
</button>
<button className="dropdown-item w-full" onClick={(e) => handleCloseTab(e, currentTabUid)}>
Close
</button>
<button disabled={!hasOtherTabs} className="dropdown-item w-full" onClick={handleCloseOtherTabs}>
Close Others
</button>
<button disabled={!hasLeftTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheLeft}>
Close to the Left
</button>
<button disabled={!hasRightTabs} className="dropdown-item w-full" onClick={handleCloseTabsToTheRight}>
Close to the Right
</button>
<button className="dropdown-item w-full" onClick={handleCloseSavedTabs}>
Close Saved
</button>
<button className="dropdown-item w-full" onClick={handleCloseAllTabs}>
Close All
</button>
</Dropdown>
</Fragment>
);
}
export default RequestTab; export default RequestTab;

View File

@ -7,7 +7,6 @@ const Wrapper = styled.div`
padding: 0; padding: 0;
margin: 0; margin: 0;
display: flex; display: flex;
position: relative;
overflow: scroll; overflow: scroll;
&::-webkit-scrollbar { &::-webkit-scrollbar {

View File

@ -110,7 +110,14 @@ const RequestTabs = () => {
role="tab" role="tab"
onClick={() => handleClick(tab)} onClick={() => handleClick(tab)}
> >
<RequestTab key={tab.uid} tab={tab} collection={activeCollection} folderUid={tab.folderUid} /> <RequestTab
collectionRequestTabs={collectionRequestTabs}
tabIndex={index}
key={tab.uid}
tab={tab}
collection={activeCollection}
folderUid={tab.folderUid}
/>
</li> </li>
); );
}) })

View File

@ -3,6 +3,7 @@ import styled from 'styled-components';
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
position: absolute; position: absolute;
height: 100%; height: 100%;
width: calc(100% - 0.75rem);
z-index: 1; z-index: 1;
background-color: ${(props) => props.theme.requestTabPanel.responseOverlayBg}; background-color: ${(props) => props.theme.requestTabPanel.responseOverlayBg};

View File

@ -13,7 +13,7 @@ const ResponseLoadingOverlay = ({ item, collection }) => {
}; };
return ( return (
<StyledWrapper className="px-3 w-full"> <StyledWrapper className="w-full">
<div className="overlay"> <div className="overlay">
<div style={{ marginBottom: 15, fontSize: 26 }}> <div style={{ marginBottom: 15, fontSize: 26 }}>
<div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}> <div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}>

View File

@ -46,7 +46,7 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
return ( return (
<div <div
className={ className={
'response-filter absolute bottom-2 w-full justify-end right-0 flex flex-row items-center gap-2 py-4 px-2' 'response-filter absolute bottom-2 w-full justify-end right-0 flex flex-row items-center gap-2 py-4 px-2 pointer-events-none'
} }
> >
{tooltipText && !isExpanded && <ReactTooltip anchorId={'request-filter-icon'} html={tooltipText} />} {tooltipText && !isExpanded && <ReactTooltip anchorId={'request-filter-icon'} html={tooltipText} />}
@ -61,11 +61,11 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
autoCapitalize="off" autoCapitalize="off"
spellCheck="false" spellCheck="false"
className={`block ml-14 p-2 py-1 sm:text-sm transition-all duration-200 ease-in-out border border-gray-300 rounded-md ${ className={`block ml-14 p-2 py-1 sm:text-sm transition-all duration-200 ease-in-out border border-gray-300 rounded-md ${
isExpanded ? 'w-full opacity-100' : 'w-[0] opacity-0' isExpanded ? 'w-full opacity-100 pointer-events-auto' : 'w-[0] opacity-0'
}`} }`}
onChange={onChange} onChange={onChange}
/> />
<div className="text-gray-500 sm:text-sm cursor-pointer" id="request-filter-icon" onClick={handleFilterClick}> <div className="text-gray-500 sm:text-sm cursor-pointer pointer-events-auto" id="request-filter-icon" onClick={handleFilterClick}>
{isExpanded ? <IconX size={20} strokeWidth={1.5} /> : <IconFilter size={20} strokeWidth={1.5} />} {isExpanded ? <IconX size={20} strokeWidth={1.5} /> : <IconFilter size={20} strokeWidth={1.5} />}
</div> </div>
</div> </div>

View File

@ -97,6 +97,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
}); });
}; };
const responseHeadersCount = typeof response.headers === 'object' ? Object.entries(response.headers).length : 0;
return ( return (
<StyledWrapper className="flex flex-col h-full relative"> <StyledWrapper className="flex flex-col h-full relative">
<div className="flex flex-wrap items-center pl-3 pr-4 tabs" role="tablist"> <div className="flex flex-wrap items-center pl-3 pr-4 tabs" role="tablist">
@ -105,7 +107,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
</div> </div>
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}> <div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
Headers Headers
{response.headers?.length > 0 && <sup className="ml-1 font-medium">{response.headers.length}</sup>} {responseHeadersCount > 0 && <sup className="ml-1 font-medium">{responseHeadersCount}</sup>}
</div> </div>
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}> <div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
Timeline Timeline

View File

@ -1,6 +1,19 @@
import styled from 'styled-components'; import styled from 'styled-components';
const Wrapper = styled.div` const Wrapper = styled.div`
.textbox {
border: 1px solid #ccc;
padding: 0.2rem 0.5rem;
box-shadow: none;
border-radius: 0px;
outline: none;
box-shadow: none;
transition: border-color ease-in-out 0.1s;
border-radius: 3px;
background-color: ${(props) => props.theme.modal.input.bg};
border: 1px solid ${(props) => props.theme.modal.input.border};
}
.item-path { .item-path {
.link { .link {
color: ${(props) => props.theme.textLink}; color: ${(props) => props.theme.textLink};

View File

@ -23,6 +23,7 @@ const getRelativePath = (fullPath, pathname) => {
export default function RunnerResults({ collection }) { export default function RunnerResults({ collection }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [selectedItem, setSelectedItem] = useState(null); const [selectedItem, setSelectedItem] = useState(null);
const [delay, setDelay] = useState(null);
// ref for the runner output body // ref for the runner output body
const runnerBodyRef = useRef(); const runnerBodyRef = useRef();
@ -78,11 +79,11 @@ export default function RunnerResults({ collection }) {
.filter(Boolean); .filter(Boolean);
const runCollection = () => { const runCollection = () => {
dispatch(runCollectionFolder(collection.uid, null, true)); dispatch(runCollectionFolder(collection.uid, null, true, Number(delay)));
}; };
const runAgain = () => { const runAgain = () => {
dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive)); dispatch(runCollectionFolder(collection.uid, runnerInfo.folderUid, runnerInfo.isRecursive, Number(delay)));
}; };
const resetRunner = () => { const resetRunner = () => {
@ -116,6 +117,20 @@ export default function RunnerResults({ collection }) {
You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection. You have <span className="font-medium">{totalRequestsInCollection}</span> requests in this collection.
</div> </div>
<div className="mt-6">
<label>Delay (in ms)</label>
<input
type="number"
className="block textbox mt-2 py-5"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={delay}
onChange={(e) => setDelay(e.target.value)}
/>
</div>
<button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}> <button type="submit" className="submit btn btn-sm btn-secondary mt-6" onClick={runCollection}>
Run Collection Run Collection
</button> </button>
@ -167,10 +182,14 @@ export default function RunnerResults({ collection }) {
</span> </span>
{item.status !== 'error' && item.status !== 'completed' ? ( {item.status !== 'error' && item.status !== 'completed' ? (
<IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} /> <IconRefresh className="animate-spin ml-1" size={18} strokeWidth={1.5} />
) : ( ) : item.responseReceived?.status ? (
<span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}> <span className="text-xs link cursor-pointer" onClick={() => setSelectedItem(item)}>
(<span className="mr-1">{get(item.responseReceived, 'status')}</span> (<span className="mr-1">{item.responseReceived?.status}</span>
<span>{get(item.responseReceived, 'statusText')}</span>) <span>{item.responseReceived?.statusText}</span>)
</span>
) : (
<span className="danger text-xs cursor-pointer" onClick={() => setSelectedItem(item)}>
(request failed)
</span> </span>
)} )}
</div> </div>

View File

@ -0,0 +1,16 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.safe-mode {
padding: 0.15rem 0.3rem;
color: ${(props) => props.theme.colors.text.green};
border: solid 1px ${(props) => props.theme.colors.text.green} !important;
}
.developer-mode {
padding: 0.15rem 0.3rem;
color: ${(props) => props.theme.colors.text.yellow};
border: solid 1px ${(props) => props.theme.colors.text.yellow} !important;
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,45 @@
import { useDispatch } from 'react-redux';
import { IconShieldLock } from '@tabler/icons';
import { addTab } from 'providers/ReduxStore/slices/tabs';
import { uuid } from 'utils/common/index';
import JsSandboxModeModal from '../JsSandboxModeModal';
import StyledWrapper from './StyledWrapper';
const JsSandboxMode = ({ collection }) => {
const jsSandboxMode = collection?.securityConfig?.jsSandboxMode;
const dispatch = useDispatch();
const viewSecuritySettings = () => {
dispatch(
addTab({
uid: uuid(),
collectionUid: collection.uid,
type: 'security-settings'
})
);
};
return (
<StyledWrapper className='flex'>
{jsSandboxMode === 'safe' && (
<div
className="flex items-center border rounded-md text-xs cursor-pointer safe-mode"
onClick={viewSecuritySettings}
>
Safe Mode
</div>
)}
{jsSandboxMode === 'developer' && (
<div
className="flex items-center border rounded-md text-xs cursor-pointer developer-mode"
onClick={viewSecuritySettings}
>
Developer Mode
</div>
)}
{!jsSandboxMode ? <JsSandboxModeModal collection={collection} /> : null}
</StyledWrapper>
);
};
export default JsSandboxMode;

View File

@ -0,0 +1,22 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
max-width: 800px;
span.beta-tag {
display: flex;
align-items: center;
padding: 0.1rem 0.25rem;
font-size: 0.75rem;
border-radius: 0.25rem;
color: ${(props) => props.theme.colors.text.green};
border: solid 1px ${(props) => props.theme.colors.text.green} !important;
}
span.developer-mode-warning {
font-weight: 400;
color: ${(props) => props.theme.colors.text.yellow};
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,98 @@
import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import toast from 'react-hot-toast';
import { useState } from 'react';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import StyledWrapper from './StyledWrapper';
const JsSandboxModeModal = ({ collection }) => {
const dispatch = useDispatch();
const [jsSandboxMode, setJsSandboxMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe');
const handleChange = (e) => {
setJsSandboxMode(e.target.value);
};
const handleSave = () => {
dispatch(
saveCollectionSecurityConfig(collection?.uid, {
jsSandboxMode: jsSandboxMode
})
)
.then(() => {
toast.success('Sandbox mode updated successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to update sandbox mode'));
};
return (
<Portal>
<Modal
size="sm"
title={'JavaScript Sandbox'}
confirmText="Save"
handleConfirm={handleSave}
hideCancel={true}
hideClose={true}
disableCloseOnOutsideClick={true}
disableEscapeKey={true}
>
<StyledWrapper>
<div>
The collection might include JavaScript code in Variables, Scripts, Tests, and Assertions.
</div>
<div className='text-muted mt-6'>
Please choose the security level for the JavaScript code execution.
</div>
<div className="flex flex-col mt-4">
<label htmlFor="safe" className="flex flex-row items-center gap-2 cursor-pointer">
<input
type="radio"
id="safe"
name="jsSandboxMode"
value="safe"
checked={jsSandboxMode === 'safe'}
onChange={handleChange}
className="cursor-pointer"
/>
<span className={jsSandboxMode === 'safe' ? 'font-medium' : 'font-normal'}>
Safe Mode
</span>
<span className='beta-tag'>BETA</span>
</label>
<p className='text-sm text-muted mt-1'>
JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands.
</p>
<label htmlFor="developer" className="flex flex-row gap-2 mt-6 cursor-pointer">
<input
type="radio"
id="developer"
name="jsSandboxMode"
value="developer"
checked={jsSandboxMode === 'developer'}
onChange={handleChange}
className="cursor-pointer"
/>
<span className={jsSandboxMode === 'developer' ? 'font-medium' : 'font-normal'}>
Developer Mode
<span className='ml-1 developer-mode-warning'>(use only if you trust the collections authors)</span>
</span>
</label>
<p className='text-sm text-muted mt-1'>
JavaScript code has access to the filesystem, can execute system commands and access sensitive information.
</p>
<small className='text-muted mt-6'>
* SAFE mode has been introduced v1.26 onwards and is in beta. Please report any issues on github.
</small>
</div>
</StyledWrapper>
</Modal>
</Portal>
);
};
export default JsSandboxModeModal;

View File

@ -0,0 +1,22 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
max-width: 800px;
span.beta-tag {
display: flex;
align-items: center;
padding: 0.1rem 0.25rem;
font-size: 0.75rem;
border-radius: 0.25rem;
color: ${(props) => props.theme.colors.text.green};
border: solid 1px ${(props) => props.theme.colors.text.green} !important;
}
span.developer-mode-warning {
font-weight: 400;
color: ${(props) => props.theme.colors.text.yellow};
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,86 @@
import { useState } from 'react';
import { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast';
import StyledWrapper from './StyledWrapper';
import { useDispatch } from 'react-redux';
const SecuritySettings = ({ collection }) => {
const dispatch = useDispatch();
const [jsSandboxMode, setJsSandboxMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe');
const handleChange = (e) => {
setJsSandboxMode(e.target.value);
};
const handleSave = () => {
dispatch(
saveCollectionSecurityConfig(collection?.uid, {
jsSandboxMode: jsSandboxMode
})
)
.then(() => {
toast.success('Sandbox mode updated successfully');
})
.catch((err) => console.log(err) && toast.error('Failed to update sandbox mode'));
};
return (
<StyledWrapper className="flex flex-col h-full relative px-4 py-4">
<div className='font-semibold mt-2'>JavaScript Sandbox</div>
<div className='mt-4'>
The collection might include JavaScript code in Variables, Scripts, Tests, and Assertions.
</div>
<div className="flex flex-col mt-4">
<div className="flex flex-col">
<label htmlFor="safe" className="flex flex-row items-center gap-2 cursor-pointer">
<input
type="radio"
id="safe"
name="jsSandboxMode"
value="safe"
checked={jsSandboxMode === 'safe'}
onChange={handleChange}
className="cursor-pointer"
/>
<span className={jsSandboxMode === 'safe' ? 'font-medium' : 'font-normal'}>
Safe Mode
</span>
<span className='beta-tag'>BETA</span>
</label>
<p className='text-sm text-muted mt-1'>
JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands.
</p>
<label htmlFor="developer" className="flex flex-row gap-2 mt-6 cursor-pointer">
<input
type="radio"
id="developer"
name="jsSandboxMode"
value="developer"
checked={jsSandboxMode === 'developer'}
onChange={handleChange}
className="cursor-pointer"
/>
<span className={jsSandboxMode === 'developer' ? 'font-medium' : 'font-normal'}>
Developer Mode
<span className='ml-1 developer-mode-warning'>(use only if you trust the collections authors)</span>
</span>
</label>
<p className='text-sm text-muted mt-1'>
JavaScript code has access to the filesystem, can execute system commands and access sensitive information.
</p>
</div>
<button onClick={handleSave} className="submit btn btn-sm btn-secondary w-fit mt-6">
Save
</button>
<small className='text-muted mt-6'>
* SAFE mode has been introduced v1.26 onwards and is in beta. Please report any issues on github.
</small>
</div>
</StyledWrapper>
);
};
export default SecuritySettings;

View File

@ -58,6 +58,7 @@ const CloneCollectionItem = ({ collection, item, onClose }) => {
id="collection-item-name" id="collection-item-name"
type="text" type="text"
name="name" name="name"
placeholder='Enter Item name'
ref={inputRef} ref={inputRef}
className="block textbox mt-2 w-full" className="block textbox mt-2 w-full"
autoComplete="off" autoComplete="off"

View File

@ -2,6 +2,7 @@ import styled from 'styled-components';
const StyledWrapper = styled.div` const StyledWrapper = styled.div`
position: relative; position: relative;
height: 100%;
.copy-to-clipboard { .copy-to-clipboard {
position: absolute; position: absolute;

View File

@ -23,7 +23,9 @@ const RequestMethod = ({ item }) => {
return ( return (
<StyledWrapper> <StyledWrapper>
<div className={getClassname(item.request.method)}> <div className={getClassname(item.request.method)}>
<span className="uppercase">{item.request.method}</span> <span className="uppercase">
{item.request.method.length > 5 ? item.request.method.substring(0, 3) : item.request.method}
</span>
</div> </div>
</StyledWrapper> </StyledWrapper>
); );

View File

@ -189,16 +189,28 @@ const CollectionItem = ({ item, collection, searchText }) => {
toast.error('URL is required'); toast.error('URL is required');
} }
}; };
const viewFolderSettings = () => { const viewFolderSettings = () => {
dispatch( if (isItemAFolder(item)) {
addTab({ if (itemIsOpenedInTabs(item, tabs)) {
uid: uuid(), dispatch(
collectionUid: collection.uid, focusTab({
folderUid: item.uid, uid: item.uid
type: 'folder-settings' })
}) );
); return;
}
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
type: 'folder-settings'
})
);
return;
}
}; };
const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i))); const requestItems = sortRequestItems(filter(item.items, (i) => isItemARequest(i)));
const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i))); const folderItems = sortFolderItems(filter(item.items, (i) => isItemAFolder(i)));

View File

@ -91,13 +91,13 @@ const Collections = () => {
<input <input
type="text" type="text"
name="search" name="search"
placeholder="search"
id="search" id="search"
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
spellCheck="false" spellCheck="false"
className="block w-full pl-7 py-1 sm:text-sm" className="block w-full pl-7 py-1 sm:text-sm"
placeholder="search"
value={searchText} value={searchText}
onChange={(e) => setSearchText(e.target.value.toLowerCase())} onChange={(e) => setSearchText(e.target.value.toLowerCase())}
/> />
@ -115,7 +115,7 @@ const Collections = () => {
)} )}
</div> </div>
<div className="mt-4 flex flex-col overflow-y-auto absolute top-32 bottom-10 left-0 right-0"> <div className="mt-4 flex flex-col overflow-hidden hover:overflow-y-auto absolute top-32 bottom-10 left-0 right-0">
{collections && collections.length {collections && collections.length
? collections.map((c) => { ? collections.map((c) => {
return ( return (

View File

@ -115,7 +115,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
collectionLocation: Yup.string() collectionLocation: Yup.string()
.min(1, 'must be at least 1 character') .min(1, 'must be at least 1 character')
.max(500, 'must be 500 characters or less') .max(500, 'must be 500 characters or less')
.required('name is required') .required('Location is required')
}), }),
onSubmit: (values) => { onSubmit: (values) => {
handleSubmit(values.collectionLocation); handleSubmit(values.collectionLocation);
@ -124,7 +124,9 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
const browse = () => { const browse = () => {
dispatch(browseDirectory()) dispatch(browseDirectory())
.then((dirPath) => { .then((dirPath) => {
formik.setFieldValue('collectionLocation', dirPath); if (typeof dirPath === 'string' && dirPath.length > 0) {
formik.setFieldValue('collectionLocation', dirPath);
}
}) })
.catch((error) => { .catch((error) => {
formik.setFieldValue('collectionLocation', ''); formik.setFieldValue('collectionLocation', '');
@ -160,7 +162,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
type="text" type="text"
name="collectionLocation" name="collectionLocation"
readOnly={true} readOnly={true}
className="block textbox mt-2 w-full" className="block textbox mt-2 w-full cursor-pointer"
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"

View File

@ -109,7 +109,8 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
collectionUid: collection.uid, collectionUid: collection.uid,
itemUid: item ? item.uid : null, itemUid: item ? item.uid : null,
headers: request.headers, headers: request.headers,
body: request.body body: request.body,
auth: request.auth
}) })
) )
.then(() => onClose()) .then(() => onClose())
@ -229,6 +230,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
id="request-name" id="request-name"
type="text" type="text"
name="requestName" name="requestName"
placeholder="Request Name"
ref={inputRef} ref={inputRef}
className="block textbox mt-2 w-full" className="block textbox mt-2 w-full"
autoComplete="off" autoComplete="off"
@ -261,6 +263,7 @@ const NewRequest = ({ collection, item, isEphemeral, onClose }) => {
id="request-url" id="request-url"
type="text" type="text"
name="requestUrl" name="requestUrl"
placeholder="Request URL"
className="px-3 w-full " className="px-3 w-full "
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"

View File

@ -129,7 +129,7 @@ const Sidebar = () => {
Star Star
</GitHubButton> */} </GitHubButton> */}
</div> </div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.24.0</div> <div className="flex flex-grow items-center justify-end text-xs mr-2">v1.26.2</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -174,7 +174,7 @@ class SingleLineEditor extends Component {
render() { render() {
return ( return (
<div className="flex flex-row justify-between w-full"> <div className="flex flex-row justify-between w-full overflow-x-auto">
<StyledWrapper ref={this.editorRef} className="single-line-editor grow" /> <StyledWrapper ref={this.editorRef} className="single-line-editor grow" />
{this.secretEye(this.props.isSecret)} {this.secretEye(this.props.isSecret)}
</div> </div>

View File

@ -13,7 +13,6 @@ const Tooltip = ({ text, tooltipId }) => {
fill="currentColor" fill="currentColor"
className="inline-block ml-2 cursor-pointer" className="inline-block ml-2 cursor-pointer"
viewBox="0 0 16 16" viewBox="0 0 16 16"
style={{ marginTop: 1 }}
> >
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" /> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" /> <path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />

View File

@ -1,6 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions'; import { openCollection, importCollection } from 'providers/ReduxStore/slices/collections/actions';
import { IconBrandGithub, IconPlus, IconDownload, IconFolders, IconSpeakerphone, IconBook } from '@tabler/icons'; import { IconBrandGithub, IconPlus, IconDownload, IconFolders, IconSpeakerphone, IconBook } from '@tabler/icons';
@ -12,6 +13,7 @@ import StyledWrapper from './StyledWrapper';
const Welcome = () => { const Welcome = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { t } = useTranslation();
const [importedCollection, setImportedCollection] = useState(null); const [importedCollection, setImportedCollection] = useState(null);
const [importedTranslationLog, setImportedTranslationLog] = useState({}); const [importedTranslationLog, setImportedTranslationLog] = useState({});
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false); const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
@ -20,7 +22,7 @@ const Welcome = () => {
const handleOpenCollection = () => { const handleOpenCollection = () => {
dispatch(openCollection()).catch( dispatch(openCollection()).catch(
(err) => console.log(err) && toast.error('An error occurred while opening the collection') (err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR'))
); );
}; };
@ -38,12 +40,12 @@ const Welcome = () => {
.then(() => { .then(() => {
setImportCollectionLocationModalOpen(false); setImportCollectionLocationModalOpen(false);
setImportedCollection(null); setImportedCollection(null);
toast.success('Collection imported successfully'); toast.success(t('WELCOME.COLLECTION_IMPORT_SUCCESS'));
}) })
.catch((err) => { .catch((err) => {
setImportCollectionLocationModalOpen(false); setImportCollectionLocationModalOpen(false);
console.error(err); console.error(err);
toast.error('An error occurred while importing the collection. Check the logs for more information.'); toast.error(t('WELCOME.COLLECTION_IMPORT_ERROR'));
}); });
}; };
@ -66,46 +68,45 @@ const Welcome = () => {
<Bruno width={50} /> <Bruno width={50} />
</div> </div>
<div className="text-xl font-semibold select-none">bruno</div> <div className="text-xl font-semibold select-none">bruno</div>
<div className="mt-4">Opensource IDE for exploring and testing APIs</div> <div className="mt-4">{t('WELCOME.ABOUT_BRUNO')}</div>
<div className="uppercase font-semibold heading mt-10">Collections</div> <div className="uppercase font-semibold heading mt-10">{t('COMMON.COLLECTIONS')}</div>
<div className="mt-4 flex items-center collection-options select-none"> <div className="mt-4 flex items-center collection-options select-none">
<div className="flex items-center" onClick={() => setCreateCollectionModalOpen(true)}> <div className="flex items-center" onClick={() => setCreateCollectionModalOpen(true)}>
<IconPlus size={18} strokeWidth={2} /> <IconPlus size={18} strokeWidth={2} />
<span className="label ml-2" id="create-collection"> <span className="label ml-2" id="create-collection">
Create Collection {t('WELCOME.CREATE_COLLECTION')}
</span> </span>
</div> </div>
<div className="flex items-center ml-6" onClick={handleOpenCollection}> <div className="flex items-center ml-6" onClick={handleOpenCollection}>
<IconFolders size={18} strokeWidth={2} /> <IconFolders size={18} strokeWidth={2} />
<span className="label ml-2">Open Collection</span> <span className="label ml-2">{t('WELCOME.OPEN_COLLECTION')}</span>
</div> </div>
<div className="flex items-center ml-6" onClick={() => setImportCollectionModalOpen(true)}> <div className="flex items-center ml-6" onClick={() => setImportCollectionModalOpen(true)}>
<IconDownload size={18} strokeWidth={2} /> <IconDownload size={18} strokeWidth={2} />
<span className="label ml-2" id="import-collection"> <span className="label ml-2" id="import-collection">
Import Collection {t('WELCOME.IMPORT_COLLECTION')}
</span> </span>
</div> </div>
</div> </div>
<div className="uppercase font-semibold heading mt-10 pt-6">{t('WELCOME.LINKS')}</div>
<div className="uppercase font-semibold heading mt-10 pt-6">Links</div>
<div className="mt-4 flex flex-col collection-options select-none"> <div className="mt-4 flex flex-col collection-options select-none">
<div className="flex items-center mt-2"> <div className="flex items-center mt-2">
<a href="https://docs.usebruno.com" target="_blank" className="inline-flex items-center"> <a href="https://docs.usebruno.com" target="_blank" className="inline-flex items-center">
<IconBook size={18} strokeWidth={2} /> <IconBook size={18} strokeWidth={2} />
<span className="label ml-2">Documentation</span> <span className="label ml-2">{t('COMMON.DOCUMENTATION')}</span>
</a> </a>
</div> </div>
<div className="flex items-center mt-2"> <div className="flex items-center mt-2">
<a href="https://github.com/usebruno/bruno/issues" target="_blank" className="inline-flex items-center"> <a href="https://github.com/usebruno/bruno/issues" target="_blank" className="inline-flex items-center">
<IconSpeakerphone size={18} strokeWidth={2} /> <IconSpeakerphone size={18} strokeWidth={2} />
<span className="label ml-2">Report Issues</span> <span className="label ml-2">{t('COMMON.REPORT_ISSUES')}</span>
</a> </a>
</div> </div>
<div className="flex items-center mt-2"> <div className="flex items-center mt-2">
<a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center"> <a href="https://github.com/usebruno/bruno" target="_blank" className="flex items-center">
<IconBrandGithub size={18} strokeWidth={2} /> <IconBrandGithub size={18} strokeWidth={2} />
<span className="label ml-2">GitHub</span> <span className="label ml-2">{t('COMMON.GITHUB')}</span>
</a> </a>
</div> </div>
</div> </div>

View File

@ -100,6 +100,11 @@ const GlobalStyle = createGlobalStyle`
} }
} }
input::placeholder {
color: ${(props) => props.theme.input.placeholder.color};
opacity: ${(props) => props.theme.input.placeholder.opacity};
}
@keyframes fade-in { @keyframes fade-in {
from { from {
opacity: 0; opacity: 0;

View File

@ -0,0 +1,24 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import translationEn from './translation/en.json';
const resources = {
en: {
translation: translationEn,
},
};
i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
resources,
lng: 'en', // Use "en" as the default language. "cimode" can be used to debug / show translation placeholder
ns: 'translation', // Use translation as the default Namespace that will be loaded by default
interpolation: {
escapeValue: false // react already safes from xss
}
});
export default i18n;

View File

@ -0,0 +1,20 @@
{
"COMMON": {
"COLLECTIONS": "Collections",
"DOCUMENTATION": "Documentation",
"REPORT_ISSUES": "Report Issues",
"GITHUB": "GitHub",
"DISCORD": "Discord",
"TWITTER": "Twitter"
},
"WELCOME": {
"ABOUT_BRUNO": "Opensource IDE for exploring and testing APIs",
"LINKS": "Links",
"CREATE_COLLECTION": "Create Collection",
"OPEN_COLLECTION": "Open Collection",
"IMPORT_COLLECTION": "Import Collection",
"COLLECTION_IMPORT_SUCCESS": "Collection imported successfully",
"COLLECTION_IMPORT_ERROR": "An error occurred while importing the collection. Check the logs for more information.",
"COLLECTION_OPEN_ERROR": "An error occurred while opening the collection"
}
}

View File

@ -14,6 +14,15 @@ import 'codemirror/lib/codemirror.css';
import 'graphiql/graphiql.min.css'; import 'graphiql/graphiql.min.css';
import 'react-tooltip/dist/react-tooltip.css'; import 'react-tooltip/dist/react-tooltip.css';
import '@usebruno/graphql-docs/dist/esm/index.css'; import '@usebruno/graphql-docs/dist/esm/index.css';
import '@fontsource/inter/100.css';
import '@fontsource/inter/200.css';
import '@fontsource/inter/300.css';
import '@fontsource/inter/400.css';
import '@fontsource/inter/500.css';
import '@fontsource/inter/600.css';
import '@fontsource/inter/700.css';
import '@fontsource/inter/800.css';
import '@fontsource/inter/900.css';
function SafeHydrate({ children }) { function SafeHydrate({ children }) {
return <div suppressHydrationWarning>{typeof window === 'undefined' ? null : children}</div>; return <div suppressHydrationWarning>{typeof window === 'undefined' ? null : children}</div>;

View File

@ -30,12 +30,7 @@ export default class MyDocument extends Document {
render() { render() {
return ( return (
<Html> <Html>
<Head> <Head />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
</Head>
<body id="bruno-app-body"> <body id="bruno-app-body">
<Main /> <Main />
<NextScript /> <NextScript />

View File

@ -1,6 +1,7 @@
import Head from 'next/head'; import Head from 'next/head';
import Bruno from './Bruno'; import Bruno from './Bruno';
import GlobalStyle from '../globalStyles'; import GlobalStyle from '../globalStyles';
import '../i18n';
export default function Home() { export default function Home() {
return ( return (

View File

@ -60,7 +60,7 @@ const trackStart = () => {
event: 'start', event: 'start',
properties: { properties: {
os: platformLib.os.family, os: platformLib.os.family,
version: '1.24.0' version: '1.26.2'
} }
}); });
}; };

View File

@ -154,6 +154,31 @@ export const HotkeysProvider = (props) => {
}; };
}, [activeTabUid]); }, [activeTabUid]);
// close all tabs
useEffect(() => {
Mousetrap.bind(['command+shift+w', 'ctrl+shift+w'], (e) => {
const activeTab = find(tabs, (t) => t.uid === activeTabUid);
if (activeTab) {
const collection = findCollectionByUid(collections, activeTab.collectionUid);
if (collection) {
const tabUids = tabs.filter((tab) => tab.collectionUid === collection.uid).map((tab) => tab.uid);
dispatch(
closeTabs({
tabUids: tabUids
})
);
}
}
return false; // this stops the event bubbling
});
return () => {
Mousetrap.unbind(['command+shift+w', 'ctrl+shift+w']);
};
}, [activeTabUid, tabs, collections, dispatch]);
return ( return (
<HotkeysContext.Provider {...props} value="hotkey"> <HotkeysContext.Provider {...props} value="hotkey">
{showSaveRequestModal && ( {showSaveRequestModal && (

View File

@ -33,13 +33,14 @@ import {
requestCancelled, requestCancelled,
resetRunResults, resetRunResults,
responseReceived, responseReceived,
updateLastAction updateLastAction,
setCollectionSecurityConfig
} from './index'; } from './index';
import { each } from 'lodash'; import { each } from 'lodash';
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs'; import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform'; import { resolveRequestFilename } from 'utils/common/platform';
import { parseQueryParams, splitOnFirst } from 'utils/url/index'; import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index'; import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
import { name } from 'file-loader'; import { name } from 'file-loader';
@ -281,7 +282,7 @@ export const cancelRunnerExecution = (cancelTokenUid) => (dispatch) => {
cancelNetworkRequest(cancelTokenUid).catch((err) => console.log(err)); cancelNetworkRequest(cancelTokenUid).catch((err) => console.log(err));
}; };
export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dispatch, getState) => { export const runCollectionFolder = (collectionUid, folderUid, recursive, delay) => (dispatch, getState) => {
const state = getState(); const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid); const collection = findCollectionByUid(state.collections.collections, collectionUid);
@ -312,7 +313,8 @@ export const runCollectionFolder = (collectionUid, folderUid, recursive) => (dis
collectionCopy, collectionCopy,
environment, environment,
collectionCopy.runtimeVariables, collectionCopy.runtimeVariables,
recursive recursive,
delay
) )
.then(resolve) .then(resolve)
.catch((err) => { .catch((err) => {
@ -372,6 +374,7 @@ export const newFolder = (folderName, collectionUid, itemUid) => (dispatch, getS
}); });
}; };
// rename item
export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getState) => { export const renameItem = (newName, itemUid, collectionUid) => (dispatch, getState) => {
const state = getState(); const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid); const collection = findCollectionByUid(state.collections.collections, collectionUid);
@ -697,7 +700,7 @@ export const moveItemToRootOfCollection = (collectionUid, draggedItemUid) => (di
}; };
export const newHttpRequest = (params) => (dispatch, getState) => { export const newHttpRequest = (params) => (dispatch, getState) => {
const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body } = params; const { requestName, requestType, requestUrl, requestMethod, collectionUid, itemUid, headers, body, auth } = params;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const state = getState(); const state = getState();
@ -707,11 +710,20 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
} }
const parts = splitOnFirst(requestUrl, '?'); const parts = splitOnFirst(requestUrl, '?');
const params = parseQueryParams(parts[1]); const queryParams = parseQueryParams(parts[1]);
each(params, (urlParam) => { each(queryParams, (urlParam) => {
urlParam.enabled = true; urlParam.enabled = true;
urlParam.type = 'query';
}); });
const pathParams = parsePathParams(requestUrl);
each(pathParams, (pathParm) => {
pathParams.enabled = true;
pathParm.type = 'path';
});
const params = [...queryParams, ...pathParams];
const item = { const item = {
uid: uuid(), uid: uuid(),
type: requestType, type: requestType,
@ -729,6 +741,9 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
sparql: null, sparql: null,
multipartForm: null, multipartForm: null,
formUrlEncoded: null formUrlEncoded: null
},
auth: auth ?? {
mode: 'none'
} }
} }
}; };
@ -1038,11 +1053,13 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
}; };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
collectionSchema ipcRenderer.invoke('renderer:get-collection-security-config', pathname).then((securityConfig) => {
.validate(collection) collectionSchema
.then(() => dispatch(_createCollection(collection))) .validate(collection)
.then(resolve) .then(() => dispatch(_createCollection({ ...collection, securityConfig })))
.catch(reject); .then(resolve)
.catch(reject);
});
}); });
}; };
@ -1107,3 +1124,19 @@ export const importCollection = (collection, collectionLocation) => (dispatch, g
ipcRenderer.invoke('renderer:import-collection', collection, collectionLocation).then(resolve).catch(reject); ipcRenderer.invoke('renderer:import-collection', collection, collectionLocation).then(resolve).catch(reject);
}); });
}; };
export const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (dispatch, getState) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);
ipcRenderer
.invoke('renderer:save-collection-security-config', collection?.pathname, securityConfig)
.then(async () => {
await dispatch(setCollectionSecurityConfig({ collectionUid, securityConfig }));
resolve();
})
.catch(reject);
});
};

View File

@ -33,7 +33,6 @@ export const collectionsSlice = createSlice({
const collection = action.payload; const collection = action.payload;
collection.settingsSelectedTab = 'headers'; collection.settingsSelectedTab = 'headers';
collection.folderLevelSettingsSelectedTab = {}; collection.folderLevelSettingsSelectedTab = {};
// TODO: move this to use the nextAction approach // TODO: move this to use the nextAction approach
@ -51,6 +50,12 @@ export const collectionsSlice = createSlice({
state.collections.push(collection); state.collections.push(collection);
} }
}, },
setCollectionSecurityConfig: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
collection.securityConfig = action.payload.securityConfig;
}
},
brunoConfigUpdateEvent: (state, action) => { brunoConfigUpdateEvent: (state, action) => {
const { collectionUid, brunoConfig } = action.payload; const { collectionUid, brunoConfig } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid); const collection = findCollectionByUid(state.collections, collectionUid);
@ -1622,6 +1627,7 @@ export const collectionsSlice = createSlice({
export const { export const {
createCollection, createCollection,
setCollectionSecurityConfig,
brunoConfigUpdateEvent, brunoConfigUpdateEvent,
renameCollection, renameCollection,
removeCollection, removeCollection,

View File

@ -24,7 +24,9 @@ export const tabsSlice = createSlice({
return; return;
} }
if (['variables', 'collection-settings', 'collection-runner'].includes(action.payload.type)) { if (
['variables', 'collection-settings', 'collection-runner', 'security-settings'].includes(action.payload.type)
) {
const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type); const tab = tabTypeAlreadyExists(state.tabs, action.payload.collectionUid, action.payload.type);
if (tab) { if (tab) {
state.activeTabUid = tab.uid; state.activeTabUid = tab.uid;
@ -39,7 +41,7 @@ export const tabsSlice = createSlice({
requestPaneTab: action.payload.requestPaneTab || 'params', requestPaneTab: action.payload.requestPaneTab || 'params',
responsePaneTab: 'response', responsePaneTab: 'response',
type: action.payload.type || 'request', type: action.payload.type || 'request',
...(action.payload.folderUid ? { folderUid: action.payload.folderUid } : {}) ...(action.payload.uid ? { folderUid: action.payload.uid } : {})
}); });
state.activeTabUid = action.payload.uid; state.activeTabUid = action.payload.uid;
}, },

View File

@ -20,7 +20,11 @@ const darkTheme = {
input: { input: {
bg: 'rgb(65, 65, 65)', bg: 'rgb(65, 65, 65)',
border: 'rgb(65, 65, 65)', border: 'rgb(65, 65, 65)',
focusBorder: 'rgb(65, 65, 65)' focusBorder: 'rgb(65, 65, 65)',
placeholder: {
color: '#a2a2a2',
opacity: 0.75
}
}, },
variables: { variables: {
@ -154,7 +158,7 @@ const darkTheme = {
modal: { modal: {
title: { title: {
color: '#ccc', color: '#ccc',
bg: 'rgb(48, 48, 49)', bg: 'rgb(38, 38, 39)',
iconColor: '#ccc' iconColor: '#ccc'
}, },
body: { body: {

View File

@ -20,7 +20,11 @@ const lightTheme = {
input: { input: {
bg: 'white', bg: 'white',
border: '#ccc', border: '#ccc',
focusBorder: '#8b8b8b' focusBorder: '#8b8b8b',
placeholder: {
color: '#a2a2a2',
opacity: 0.8
}
}, },
menubar: { menubar: {

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