mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-21 15:33:11 +01:00
Merge branch 'main' into fix/json-serialization-issues
This commit is contained in:
commit
00e98451d4
13
.github/workflows/tests.yml
vendored
13
.github/workflows/tests.yml
vendored
@ -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: |
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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_)。
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 वर्कस्पेस का उपयोग करते हैं
|
||||||
|
|
||||||
## डेवलपमेंट
|
## डेवलपमेंट
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 ワークスペースを使用しています。
|
||||||
|
|
||||||
## 開発
|
## 開発
|
||||||
|
|
||||||
|
@ -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를 사용합니다.
|
||||||
|
|
||||||
## 개발
|
## 개발
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
### Приступим к коду
|
### Приступим к коду
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 в цьому проекті
|
||||||
|
|
||||||
### Починаєм писати код
|
### Починаєм писати код
|
||||||
|
|
||||||
|
@ -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_)。
|
||||||
|
|
||||||
## 開發
|
## 開發
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
| [正體中文](./readme_zhtw.md)
|
| [正體中文](./readme_zhtw.md)
|
||||||
| **العربية**
|
| **العربية**
|
||||||
| [日本語](./readme_ja.md)
|
| [日本語](./readme_ja.md)
|
||||||
|
| [ქართული](./readme_ka.md)
|
||||||
|
|
||||||
برونو هو عميل API جديد ومبتكر، يهدف إلى ثورة الحالة الحالية التي يمثلها برنامج Postman وأدوات مماثلة هناك.
|
برونو هو عميل API جديد ومبتكر، يهدف إلى ثورة الحالة الحالية التي يمثلها برنامج Postman وأدوات مماثلة هناك.
|
||||||
|
|
||||||
|
@ -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 ক্লায়েন্ট, যার লক্ষ্য পোস্টম্যান এবং অনুরূপ সরঞ্জাম দ্বারা প্রতিনিধিত্ব করা স্থিতাবস্থায় বিপ্লব ঘটানো।
|
||||||
|
|
||||||
|
@ -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 和其他类似工具。
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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
176
docs/readme/readme_ka.md
Normal 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)
|
@ -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과 유사한 툴들을 혁신하는 것을 목표로 합니다.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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 и подобными инструментами.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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
22998
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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`;
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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};
|
||||||
}
|
}
|
||||||
|
@ -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 || ''}
|
||||||
|
@ -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')}>
|
||||||
|
16
packages/bruno-app/src/components/Icons/Dot/index.js
Normal file
16
packages/bruno-app/src/components/Icons/Dot/index.js
Normal 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;
|
@ -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 {
|
||||||
|
@ -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 (
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
@ -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;
|
||||||
|
@ -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}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -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
|
||||||
|
@ -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}>
|
||||||
+ <span>Add Param</span>
|
+ <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>
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
@ -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.
|
||||||
|
@ -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} />;
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -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};
|
||||||
|
|
||||||
|
@ -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 }}>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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};
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
86
packages/bruno-app/src/components/SecuritySettings/index.js
Normal file
86
packages/bruno-app/src/components/SecuritySettings/index.js
Normal 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;
|
@ -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"
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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)));
|
||||||
|
|
||||||
|
@ -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 (
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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" />
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
24
packages/bruno-app/src/i18n/index.js
Normal file
24
packages/bruno-app/src/i18n/index.js
Normal 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;
|
20
packages/bruno-app/src/i18n/translation/en.json
Normal file
20
packages/bruno-app/src/i18n/translation/en.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
@ -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>;
|
||||||
|
@ -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 />
|
||||||
|
@ -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 (
|
||||||
|
@ -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'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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 && (
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
},
|
},
|
||||||
|
@ -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: {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user