mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-04 09:58:35 +00:00
Compare commits
248 Commits
v1.2.0
...
feature/in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b059faca53 | ||
|
|
9ad265e74f | ||
|
|
aed1b41da6 | ||
|
|
d8354dca14 | ||
|
|
e375ffbed1 | ||
|
|
7de5bbbdf6 | ||
|
|
dbb5e912eb | ||
|
|
4e34aba1ca | ||
|
|
b5fccef417 | ||
|
|
db9aeec498 | ||
|
|
3e627522b7 | ||
|
|
85f24eec77 | ||
|
|
bdfcd78f3a | ||
|
|
fce164001b | ||
|
|
814d31e638 | ||
|
|
51d4dbd69b | ||
|
|
05cab18e18 | ||
|
|
fcc3a1e944 | ||
|
|
45395754bf | ||
|
|
0db6103b69 | ||
|
|
2608ec035e | ||
|
|
d0c25d46c9 | ||
|
|
d62982d52d | ||
|
|
2810c6758d | ||
|
|
3967859c51 | ||
|
|
ef5df9e114 | ||
|
|
75e19574ac | ||
|
|
b15ad5ca71 | ||
|
|
0bd3ba493f | ||
|
|
d2d1994546 | ||
|
|
e4ae0c0357 | ||
|
|
52cec963a3 | ||
|
|
f797c5d06b | ||
|
|
65879c8994 | ||
|
|
2e544183db | ||
|
|
ceccddf7f1 | ||
|
|
c1f5da1280 | ||
|
|
63aa3ded1c | ||
|
|
26c7d4f532 | ||
|
|
b7b4453278 | ||
|
|
49a51d6028 | ||
|
|
abb24c93c5 | ||
|
|
5ba2c98e1d | ||
|
|
bc01188c98 | ||
|
|
1754ea9f59 | ||
|
|
2aa073c69a | ||
|
|
48ec87ec8c | ||
|
|
35b6f7bb0a | ||
|
|
fff0293600 | ||
|
|
41d0698a87 | ||
|
|
f7ea8c93a6 | ||
|
|
83c9629820 | ||
|
|
d6f6032c6f | ||
|
|
e49999bb56 | ||
|
|
8b92055413 | ||
|
|
5ca7f6b7ad | ||
|
|
83f5763e01 | ||
|
|
4a5196c8f5 | ||
|
|
eba065aa7e | ||
|
|
10dc1d95b0 | ||
|
|
00c7b40593 | ||
|
|
b0ee137277 | ||
|
|
887c65b0d8 | ||
|
|
84905ed80f | ||
|
|
1f4ab3b5bd | ||
|
|
da983599ab | ||
|
|
fe11e45703 | ||
|
|
a08fd7eb52 | ||
|
|
d268b4786a | ||
|
|
4b76bf85f4 | ||
|
|
ed6f91533b | ||
|
|
7899b04c40 | ||
|
|
cdddf8af76 | ||
|
|
9e44c4a95f | ||
|
|
3982f9c3c3 | ||
|
|
46dda28c3a | ||
|
|
9f6890b769 | ||
|
|
eb340d4ace | ||
|
|
66f917ecec | ||
|
|
e3865d4710 | ||
|
|
983fb2c4fd | ||
|
|
cb6513c580 | ||
|
|
b8451d01ca | ||
|
|
a15a4e4a2d | ||
|
|
cada4f201a | ||
|
|
93661bd0d2 | ||
|
|
454e0e5260 | ||
|
|
647a819051 | ||
|
|
99c2dd9030 | ||
|
|
ab37e53346 | ||
|
|
8ace37848b | ||
|
|
447f40053d | ||
|
|
de530a889c | ||
|
|
882341c35b | ||
|
|
174f99f9fb | ||
|
|
2103ab20bf | ||
|
|
f0e22cb5df | ||
|
|
ee2295aec1 | ||
|
|
2d16e07747 | ||
|
|
a839d311dc | ||
|
|
82bafd5268 | ||
|
|
fc09697404 | ||
|
|
ee2d7a187a | ||
|
|
f8ff305cf4 | ||
|
|
c2c2ef6e2b | ||
|
|
aa18f17fb9 | ||
|
|
baeeeb2bb0 | ||
|
|
fff3e6d88a | ||
|
|
7953863b9d | ||
|
|
10183319c4 | ||
|
|
89c5fc2f03 | ||
|
|
8ddec6bf0c | ||
|
|
d257db27b8 | ||
|
|
08935c64bb | ||
|
|
8e1d04f5c1 | ||
|
|
da3bd95add | ||
|
|
cc89e34b4c | ||
|
|
56a456a9b6 | ||
|
|
eaa448306b | ||
|
|
9f8dba0fb2 | ||
|
|
784f63ca5b | ||
|
|
77cdc2179d | ||
|
|
d749e4b848 | ||
|
|
bb729b5793 | ||
|
|
a60f351736 | ||
|
|
0d0c4166c1 | ||
|
|
567744c2ce | ||
|
|
cb47b7be5f | ||
|
|
3061507284 | ||
|
|
d3bec5631e | ||
|
|
98b45a2fd4 | ||
|
|
dc39538d02 | ||
|
|
6d3a518043 | ||
|
|
e24b75e7d8 | ||
|
|
318036a279 | ||
|
|
b482dd68a5 | ||
|
|
1c83f5c885 | ||
|
|
e0969d6aab | ||
|
|
07eee055d4 | ||
|
|
19efa2fd35 | ||
|
|
480f8cf877 | ||
|
|
9246bf4fcb | ||
|
|
11d0bc01d2 | ||
|
|
06d62175bf | ||
|
|
96d50ebd93 | ||
|
|
06ccbc8dc2 | ||
|
|
cf329e58e7 | ||
|
|
85d55393ef | ||
|
|
f84933553f | ||
|
|
6632ae1dcb | ||
|
|
40406b96a2 | ||
|
|
832810cacd | ||
|
|
2c618cb08b | ||
|
|
95197098af | ||
|
|
06893bf3c3 | ||
|
|
861d76f7b7 | ||
|
|
33f780fb76 | ||
|
|
bacb70ea7e | ||
|
|
2aa876e526 | ||
|
|
2240acb272 | ||
|
|
752d4ae79e | ||
|
|
32c8bf296a | ||
|
|
ba994cb5a0 | ||
|
|
f4b27afb8d | ||
|
|
d23bd85cc6 | ||
|
|
a1f2e9336d | ||
|
|
d13b4d1a6b | ||
|
|
161b5eed10 | ||
|
|
41e922544c | ||
|
|
669a9fad69 | ||
|
|
3c5a8b32be | ||
|
|
d0f858681d | ||
|
|
bb852c5f80 | ||
|
|
6c73362ff2 | ||
|
|
e307c12fc8 | ||
|
|
b7e84f3623 | ||
|
|
8e78c1b4e4 | ||
|
|
64bdda6f90 | ||
|
|
a6a59ddbd7 | ||
|
|
b715e917fe | ||
|
|
78c5392675 | ||
|
|
4e02f8ad45 | ||
|
|
ffe4f7dba8 | ||
|
|
e31b601cef | ||
|
|
7d3d543f24 | ||
|
|
8d372fcdbc | ||
|
|
d00e3479cb | ||
|
|
d7eca52473 | ||
|
|
cf767165e6 | ||
|
|
3bc774bf55 | ||
|
|
0d9b5451fe | ||
|
|
0fe657d0fc | ||
|
|
ce545724bd | ||
|
|
2b08468581 | ||
|
|
0e320535a8 | ||
|
|
746c5e825e | ||
|
|
6a55a8d6ea | ||
|
|
8a48797e00 | ||
|
|
fad71e936c | ||
|
|
61f3e64751 | ||
|
|
fc6ba4641a | ||
|
|
e7d2aa3599 | ||
|
|
aa1cef9e70 | ||
|
|
3b77cfb8d6 | ||
|
|
fb8277f03e | ||
|
|
fa7afd4237 | ||
|
|
2b19ef0c2d | ||
|
|
f0d5cdecb7 | ||
|
|
be58497ba9 | ||
|
|
db1883536e | ||
|
|
c018bfc044 | ||
|
|
379697a02d | ||
|
|
c62f96c96e | ||
|
|
e1e0696e3c | ||
|
|
98ea1aa548 | ||
|
|
553d1c062e | ||
|
|
2ec343a95b | ||
|
|
2ddf79ed3f | ||
|
|
ef98eb707c | ||
|
|
fd1e8f6aa8 | ||
|
|
738c6af797 | ||
|
|
4c83dff96c | ||
|
|
531426b0a6 | ||
|
|
4d9549d2cc | ||
|
|
7980b726f6 | ||
|
|
49be0b243b | ||
|
|
005a936a61 | ||
|
|
bad9d0a3ef | ||
|
|
2ee6c5effc | ||
|
|
1ee6b5f974 | ||
|
|
924bf1217f | ||
|
|
8130de23ff | ||
|
|
7b9a4f457e | ||
|
|
bab12d7894 | ||
|
|
25af7a211a | ||
|
|
04aa921099 | ||
|
|
a3125605f3 | ||
|
|
8183ce03c5 | ||
|
|
129d659628 | ||
|
|
db0de68987 | ||
|
|
631e436079 | ||
|
|
dc32d7246c | ||
|
|
76a26b634d | ||
|
|
820e3033ea | ||
|
|
d76253ea04 | ||
|
|
4a1d45f458 | ||
|
|
3374db1ac8 | ||
|
|
d4c0207545 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
github: helloanoop
|
||||
2
.github/workflows/unit-tests.yml
vendored
2
.github/workflows/unit-tests.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Test Package bruno-cli
|
||||
run: npm run test --workspace=packages/bruno-cli
|
||||
- name: Test Package bruno-electron
|
||||
run: npm run test --workspace=packages/bruno-electron
|
||||
run: npm run test --workspace=packages/bruno-electron --passWithNoTests
|
||||
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,6 +6,8 @@ yarn.lock
|
||||
pnpm-lock.yaml
|
||||
.pnp
|
||||
.pnp.js
|
||||
bun.lockb
|
||||
bun.lock
|
||||
|
||||
# testing
|
||||
coverage
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
**English** | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md)
|
||||
|
||||
**English** | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md)
|
||||
| [简体中文](docs/contributing/contributing_cn.md)
|
||||
## Let's make bruno better, together !!
|
||||
|
||||
We are happy that you are looking to improve bruno. Below are the guidelines to get started bringing up bruno on your computer.
|
||||
|
||||
90
docs/contributing/contributing_cn.md
Normal file
90
docs/contributing/contributing_cn.md
Normal file
@@ -0,0 +1,90 @@
|
||||
[English](/contributing.md) | [Українська](./contributing_ua.md) | [Русский](./contributing_ru.md) | [Türkçe](./contributing_tr.md) | [Deutsch](./contributing_de.md) | [Français](./contributing_fr.md) | [Português (BR)](./contributing_pt_br.md) | [বাংলা](./contributing_bn.md) | [Español](./contributing_es.md) | [Română](./contributing_ro.md) | [Polski](./contributing_pl.md) | **简体中文**
|
||||
|
||||
## 让我们一起改进 Bruno!
|
||||
|
||||
很高兴看到您考虑改进 Bruno。以下是获取 Bruno 并在您的电脑上运行它的规则和指南。
|
||||
|
||||
### 使用的技术
|
||||
|
||||
Bruno 基于 NextJs 和 React 构建。我们使用 Electron 来封装桌面版本。
|
||||
|
||||
我们使用的库包括:
|
||||
|
||||
- CSS - Tailwind
|
||||
- 代码编辑器 - Codemirror
|
||||
- 状态管理 - Redux
|
||||
- 图标 - Tabler Icons
|
||||
- 表单 - formik
|
||||
- 模式验证 - Yup
|
||||
- 请求客户端 - axios
|
||||
- 文件系统监视器 - chokidar
|
||||
|
||||
### 依赖项
|
||||
|
||||
您需要 [Node v18.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我们在这个项目中也使用 npm 工作区(_npm workspaces_)。
|
||||
|
||||
|
||||
## 开发
|
||||
|
||||
Bruno 是作为一个 _client lourd(重客户端)_ 应用程序开发的。您需要在一个终端中启动 nextjs 来加载应用程序,然后在另一个终端中启动 Electron 应用程序。
|
||||
|
||||
### 依赖项
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### 本地开发
|
||||
|
||||
```bash
|
||||
# 使用 node 版本 18
|
||||
nvm use
|
||||
|
||||
# 安装依赖项
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# 构建 graphql 文档
|
||||
npm run build:graphql-docs
|
||||
|
||||
# 构建 bruno 查询
|
||||
npm run build:bruno-query
|
||||
|
||||
# 启动 next(终端 1)
|
||||
npm run dev:web
|
||||
|
||||
# 启动重客户端(终端 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### 故障排除
|
||||
|
||||
在运行 npm install 时,您可能会遇到 Unsupported platform 错误。为了解决这个问题,请删除 node_modules 目录和 package-lock.json 文件,然后再次运行 npm install。这应该会安装运行应用程序所需的所有包。
|
||||
|
||||
```shell
|
||||
# 删除子目录中的 node_modules 目录
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# 删除子目录中的 package-lock.json 文件
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
|
||||
|
||||
### 提交 Pull Request
|
||||
|
||||
- 请保持 PR 精简并专注于单一目标
|
||||
- 请遵循分支命名格式:
|
||||
- feature/[feature name]:该分支应包含特定功能
|
||||
- 例如:feature/dark-mode
|
||||
- bugfix/[bug name]:该分支应仅包含特定 bug 的修复
|
||||
- 例如:bugfix/bug-1
|
||||
@@ -1,14 +1,14 @@
|
||||
[English](/contributing.md) | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | [Türkçe](/contributing_tr.md) | [Deutsch](/contributing_de.md) | **Français** | [বাংলা](docs/contributing/contributing_bn.md)
|
||||
[English](/contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | **Français** | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md)
|
||||
|
||||
## Ensemble, améliorons Bruno !
|
||||
|
||||
Je suis content de voir que vous envisagez améliorer Bruno. Ci-dessous, vous trouverez les règles et guides pour récupérer Bruno sur votre ordinateur.
|
||||
Je suis content de voir que vous envisagez d'améliorer Bruno. Vous trouverez ci-dessous les règles et guides pour récupérer Bruno sur votre ordinateur.
|
||||
|
||||
### Technologies utilisées
|
||||
|
||||
Bruno est construit en utilisant NextJs et React. Nous utilisons aussi Electron pour embarquer la version ordinateur (qui permet les collections locales).
|
||||
Bruno est basé sur NextJs et React. Nous utilisons aussi Electron pour embarquer la version ordinateur (ce qui permet les collections locales).
|
||||
|
||||
Les bibliothèques que nous utilisons :
|
||||
Les librairies que nous utilisons :
|
||||
|
||||
- CSS - Tailwind
|
||||
- Code Editors - Codemirror
|
||||
@@ -23,22 +23,10 @@ Les bibliothèques que nous utilisons :
|
||||
|
||||
Vous aurez besoin de [Node v18.x ou la dernière version LTS](https://nodejs.org/en/) et npm 8.x. Nous utilisons aussi les espaces de travail npm (_npm workspaces_) dans ce projet.
|
||||
|
||||
### Commençons à coder
|
||||
|
||||
Veuillez vous référez à la [documentation de développement](docs/development_fr.md) pour les instructions de démarrage de l'environnement de développement local.
|
||||
|
||||
### Ouvrir une Pull Request
|
||||
|
||||
- Merci de conserver les PR petites et focalisées sur un seul objectif
|
||||
- Merci de suivre le format de nom des branches
|
||||
- feature/[feature name]: Cette branche devrait contenir une fonctionnalité spécifique
|
||||
- Exemple: feature/dark-mode
|
||||
- bugfix/[bug name]: Cette branche devrait contenir seulement une solution pour pour une bogue spécifique
|
||||
- Exemple: bugfix/bug-1
|
||||
|
||||
## Développement
|
||||
|
||||
Bruno est développé comme une application de _lourde_. Vous devez charger l'application en démarrant nextjs dans un terminal, puis démarre l'application Electron dans un autre terminal.
|
||||
Bruno est développé comme une application _client lourd_. Vous devrez charger l'application en démarrant nextjs dans un premier terminal, puis démarre l'application Electron dans un second.
|
||||
|
||||
|
||||
### Dépendances
|
||||
|
||||
@@ -47,39 +35,40 @@ Bruno est développé comme une application de _lourde_. Vous devez charger l'ap
|
||||
### Développement local
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
# utiliser node en version 18
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
# installation des dépendances
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# build graphql docs
|
||||
# construction des docs graphql
|
||||
npm run build:graphql-docs
|
||||
|
||||
# build bruno query
|
||||
# construction de bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# run next app (terminal 1)
|
||||
# démarrage de next (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# run electron app (terminal 2)
|
||||
# démarrage du client lourd (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Dépannage
|
||||
|
||||
Vous pourriez rencontrer une error `Unsupported platform` pendant le lancement de `npm install`. Pour résoudre cela, veuillez supprimer le répertoire `node_modules`, le fichier `package-lock.json` et lancer à nouveau `npm install`. Cela devrait isntaller tous les paquets nécessaires pour lancer l'application.
|
||||
Vous pourriez rencontrer une erreur `Unsupported platform` durant le lancement de `npm install`. Pour résoudre cela, veuillez supprimer le répertoire `node_modules` ainsi que le fichier `package-lock.json` et lancez à nouveau `npm install`. Cela devrait isntaller tous les paquets nécessaires pour lancer l'application.
|
||||
|
||||
```shell
|
||||
# Delete node_modules in sub-directories
|
||||
# Efface les répertoires node_modules dans les sous-répertoires
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Delete package-lock in sub-directories
|
||||
# Efface les fichiers package-lock.json dans les sous-répertoires
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
@@ -89,3 +78,13 @@ npm test --workspace=packages/bruno-schema
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
|
||||
|
||||
### Ouvrir une Pull Request
|
||||
|
||||
- Merci de conserver les PR minimes et focalisées sur un seul objectif
|
||||
- Merci de suivre le format de nom des branches :
|
||||
- feature/[feature name]: Cette branche doit contenir une fonctionnalité spécifique
|
||||
- Exemple: feature/dark-mode
|
||||
- bugfix/[bug name]: Cette branche doit contenir seulement une solution pour un bug spécifique
|
||||
- Exemple: bugfix/bug-1
|
||||
88
docs/contributing/contributing_pl.md
Normal file
88
docs/contributing/contributing_pl.md
Normal file
@@ -0,0 +1,88 @@
|
||||
[English](/contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | **Polski**
|
||||
|
||||
## Wspólnie uczynijmy Bruno lepszym !!
|
||||
|
||||
Cieszymy się, że chcesz udoskonalić Bruno. Poniżej znajdziesz wskazówki, jak rozpocząć pracę z Bruno na Twoim komputerze.
|
||||
|
||||
### Stos Technologiczny
|
||||
|
||||
Bruno jest zbudowane przy użyciu Next.js i React. Używamy również electron do stworzenia wersji desktopowej (która obsługuje lokalne kolekcje)
|
||||
|
||||
Biblioteki, których używamy
|
||||
|
||||
- CSS - Tailwind
|
||||
- Edytory Kodu - Codemirror
|
||||
- Zarządzanie Stanem - Redux
|
||||
- Ikony - Tabler Icons
|
||||
- Formularze - formik
|
||||
- Walidacja Schematu - Yup
|
||||
- Klient Zapytań - axios
|
||||
- Obserwator Systemu Plików - chokidar
|
||||
|
||||
### 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
|
||||
|
||||
## Rozwój
|
||||
|
||||
Bruno jest rozwijane jako aplikacja desktopowa. Musisz załadować aplikację, uruchamiając aplikację Next.js w jednym terminalu, a następnie uruchomić aplikację electron w innym terminalu.
|
||||
|
||||
### Zależności
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### Lokalny Rozwój
|
||||
|
||||
```bash
|
||||
# użyj wersji nodejs 18
|
||||
nvm use
|
||||
|
||||
# zainstaluj zależności
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# zbuduj dokumentację graphql
|
||||
npm run build:graphql-docs
|
||||
|
||||
# zbuduj zapytanie bruno
|
||||
npm run build:bruno-query
|
||||
|
||||
# uruchom aplikację next (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# uruchom aplikację electron (terminal 2)
|
||||
npm run dev:electron
|
||||
|
||||
|
||||
### Rozwiązywanie Problemów
|
||||
|
||||
Możesz napotkać błąd `Unsupported platform` podczas uruchamiania `npm install`. Aby to naprawić, będziesz musiał usunąć `node_modules` i `package-lock.json`, a następnie uruchomić `npm install`. Powinno to zainstalować wszystkie niezbędne pakiety potrzebne do uruchomienia aplikacji.
|
||||
|
||||
```shell
|
||||
# Usuń node_modules w podkatalogach
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Usuń package-lock w podkatalogach
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
|
||||
```
|
||||
|
||||
### Testowanie
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
|
||||
### Tworzenie Pull Request
|
||||
|
||||
- Prosimy, aby PR były małe i skoncentrowane na jednej rzeczy
|
||||
- Prosimy przestrzegać formatu tworzenia gałęzi
|
||||
- feature/[nazwa funkcji]: Ta gałąź powinna zawierać zmiany dotyczące konkretnej funkcji
|
||||
- Przykład: feature/dark-mode
|
||||
- bugfix/[nazwa błędu]: Ta gałąź powinna zawierać tylko poprawki dla konkretnego błędu
|
||||
- Przykład bugfix/bug-1
|
||||
81
docs/contributing/contributing_ro.md
Normal file
81
docs/contributing/contributing_ro.md
Normal file
@@ -0,0 +1,81 @@
|
||||
[English](/contributing.md) | [Українська](/docs/contributing/contributing_ua.md) | [Русский](/docs/contributing/contributing_ru.md) | [Türkçe](/docs/contributing/contributing_tr.md) | [Deutsch](/docs/contributing/contributing_de.md) | [Français](/docs/contributing/contributing_fr.md) | [Português (BR)](/docs/contributing/contributing_pt_br.md) | [বাংলা](/docs/contributing/contributing_bn.md) | [Español](/docs/contributing/contributing_es.md) | [Italiano](/docs/contributing/contributing_it.md) | **Română**
|
||||
|
||||
## Haideţi să îmbunătățim Bruno, împreună!!
|
||||
|
||||
Ne bucurăm că doriți să îmbunătățiți bruno. Mai jos sunt instrucțiunile pentru ca să porniți bruno pe calculatorul dvs.
|
||||
|
||||
### Stack-ul tehnologic
|
||||
|
||||
Bruno este construit cu Next.js și React. De asemenea, folosim electron pentru a livra o versiune desktop (care poate folosi colecții locale)
|
||||
|
||||
Bibliotecile pe care le folosim
|
||||
|
||||
- CSS - Tailwind
|
||||
- Editori de cod - Codemirror
|
||||
- Management de condiție - Redux
|
||||
- Icoane - Tabler Icons
|
||||
- Formulare - formik
|
||||
- Validarea schemelor - Yup
|
||||
- Cererile client - axios
|
||||
- Observatorul sistemului de fișiere - chokidar
|
||||
|
||||
### 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
|
||||
|
||||
## Dezvoltarea
|
||||
|
||||
Bruno este dezvoltat ca o aplicație desktop. Ca să porniți aplicatia trebuie să rulați aplicația Next.js într-un terminal și apoi să rulați aplicația electron într-un alt terminal.
|
||||
|
||||
```shell
|
||||
# folosiți nodejs versiunea 18
|
||||
nvm use
|
||||
|
||||
# instalați dependențele
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# construiți documente graphql
|
||||
npm run build:graphql-docs
|
||||
|
||||
# construiți bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# rulați aplicația next (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# rulați aplicația electron (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Depanare
|
||||
|
||||
Este posibil să întâmpinați o eroare `Unsupported platform` când rulați „npm install”. Pentru a remedia acest lucru, va trebui să ștergeți `node_modules` și `package-lock.json` și să rulați `npm install`. Aceasta ar trebui să instaleze toate pachetele necesare pentru a rula aplicația.
|
||||
|
||||
```shell
|
||||
# Ștergeți node_modules din subdirectoare
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Ștergeți package-lock din subdirectoare
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Testarea
|
||||
|
||||
```shell
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
|
||||
### Crearea unui Pull Request
|
||||
|
||||
- Vă rugăm să păstrați PR-urile mici și concentrate pe un singur lucru
|
||||
- Vă rugăm să urmați formatul de creare a branchurilor
|
||||
- feature/[Numele funcției]: Acest branch ar trebui să conțină modificări pentru o funcție anumită
|
||||
- Exemplu: feature/dark-mode
|
||||
- bugfix/[Numele eroarei]: Acest branch ar trebui să conţină numai remedieri pentru o eroare anumită
|
||||
- Exemplu bugfix/bug-1
|
||||
@@ -1,8 +1,8 @@
|
||||
[English](/readme.md) | [Українська](/contributing_ua.md) | [Русский](/contributing_ru.md) | **Türkçe** | [Deutsch](/contributing_de.md) | [Français](/contributing_fr.md) | [বাংলা](docs/contributing/contributing_bn.md)
|
||||
[English](../../contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | **Türkçe** | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md)
|
||||
| [简体中文](docs/contributing/contributing_cn.md)
|
||||
## Bruno'yu birlikte daha iyi hale getirelim!!!
|
||||
|
||||
## Bruno'yu birlikte daha iyi hale getirelim !!
|
||||
|
||||
Bruno'yu geliştirmek istemenizden mutluluk duyuyorum. Aşağıda, bruno'yu bilgisayarınıza getirmeye başlamak için yönergeler bulunmaktadır.
|
||||
bruno'yu geliştirmek istemenizden mutluluk duyuyoruz. Aşağıda, bruno'yu bilgisayarınıza getirmeye başlamak için yönergeler bulunmaktadır.
|
||||
|
||||
### Kullanılan Teknolojiler
|
||||
|
||||
@@ -13,7 +13,7 @@ Kullandığımız kütüphaneler
|
||||
- CSS - Tailwind
|
||||
- Kod Düzenleyiciler - Codemirror
|
||||
- Durum Yönetimi - Redux
|
||||
- Iconlar - Tabler Simgeleri
|
||||
- Iconlar - Tabler Icons
|
||||
- Formlar - formik
|
||||
- Şema Doğrulama - Yup
|
||||
- İstek İstemcisi - axios
|
||||
@@ -23,9 +23,60 @@ Kullandığımız kütüphaneler
|
||||
|
||||
[Node v18.x veya en son LTS sürümüne](https://nodejs.org/en/) ve npm 8.x'e ihtiyacınız olacaktır. Projede npm çalışma alanlarını kullanıyoruz
|
||||
|
||||
### Kodlamaya başlayalım
|
||||
## Gelişim
|
||||
|
||||
Yerel geliştirme ortamının çalıştırılmasına ilişkin talimatlar için lütfen [development.md](docs/development.md) adresine başvurun.
|
||||
Bruno bir masaüstü uygulaması olarak geliştirilmektedir. Next.js uygulamasını bir terminalde çalıştırarak uygulamayı yüklemeniz ve ardından electron uygulamasını başka bir terminalde çalıştırmanız gerekir.
|
||||
|
||||
### Bağımlılıklar
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### Yerel Geliştirme
|
||||
|
||||
```bash
|
||||
# nodejs 18 sürümünü kullan
|
||||
nvm use
|
||||
|
||||
# deps yükleyin
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# graphql dokümanlarını oluştur
|
||||
npm run build:graphql-docs
|
||||
|
||||
# bruno sorgusu oluştur
|
||||
npm run build:bruno-query
|
||||
|
||||
# sonraki uygulamayı çalıştır (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# electron uygulamasını çalıştır (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Sorun Giderme
|
||||
|
||||
`npm install`'ı çalıştırdığınızda `Unsupported platform` hatası ile karşılaşabilirsiniz. Bunu düzeltmek için `node_modules` ve `package-lock.json` dosyalarını silmeniz ve `npm install` dosyasını çalıştırmanız gerekecektir. Bu, uygulamayı çalıştırmak için gereken tüm gerekli paketleri yüklemelidir.
|
||||
|
||||
|
||||
```shell
|
||||
# Alt dizinlerdeki node_modules öğelerini silme
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Alt dizinlerdeki paket kilidini silme
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
|
||||
### Pull Request Oluşturma
|
||||
|
||||
@@ -33,5 +84,5 @@ Yerel geliştirme ortamının çalıştırılmasına ilişkin talimatlar için l
|
||||
- Lütfen şube oluşturma formatını takip edin
|
||||
- feature/[özellik adı]: Bu dal belirli bir özellik için değişiklikler içermelidir
|
||||
- Örnek: feature/dark-mode
|
||||
- bugfix/[hata adı]: Bu dal yalnızca belirli bir hata için hata düzeltmelerini içermelidir
|
||||
- bugfix/[hata adı]: Bu dal yalnızca belirli bir hata için hata düzeltmeleri içermelidir
|
||||
- Örnek bugfix/bug-1
|
||||
|
||||
7
docs/publishing/publishing_bn.md
Normal file
7
docs/publishing/publishing_bn.md
Normal file
@@ -0,0 +1,7 @@
|
||||
[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Türkçe](/docs/publishing/publishing_tr.md) | [Polski](docs/publishing/publishing_pl.md) | **বাংলা** | [Français](docs/publishing/publishing_fr.md)
|
||||
|
||||
### ব্রুনোকে নতুন প্যাকেজ ম্যানেজারে প্রকাশ করা
|
||||
|
||||
যদিও আমাদের কোড ওপেন সোর্স এবং সবার ব্যবহারের জন্য উপলব্ধ, তবে আমরা নতুন প্যাকেজ ম্যানেজারে প্রকাশনা বিবেচনা করার আগে আমাদের সাথে যোগাযোগ করার জন্য অনুরোধ করি। ব্রুনোর স্রষ্টা হিসাবে, আমি এই প্রকল্পের জন্য `Bruno` ট্রেডমার্ক ধারণ করি এবং এর বিতরণ পরিচালনা করতে চাই। যদি আপনি একটি নতুন প্যাকেজ ম্যানেজারে ব্রুনো দেখতে চান, দয়া করে একটি GitHub ইস্যু তুলুন।
|
||||
|
||||
যদিও আমাদের বেশিরভাগ বৈশিষ্ট্য বিনামূল্যে এবং ওপেন সোর্স (যা REST এবং GraphQL API গুলিকে কভার করে), আমরা ওপেন-সোর্স নীতি এবং স্থায়িত্বের মধ্যে একটি সুসঙ্গত ভারসাম্য বজায় রাখার জন্য চেষ্টা করি - https://github.com/usebruno/bruno/discussions/269
|
||||
7
docs/publishing/publishing_fr.md
Normal file
7
docs/publishing/publishing_fr.md
Normal file
@@ -0,0 +1,7 @@
|
||||
[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Türkçe](/docs/publishing/publishing_tr.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | **Français**
|
||||
|
||||
### Publier Bruno dans un nouveau gestionnaire de paquets
|
||||
|
||||
Bien que notre code soit open source et disponible pour tout le monde, nous vous remercions de nous contacter avant de considérer sa publication sur un nouveau gestionnaire de paquets. En tant que createur de Bruno, je détiens la marque `Bruno` pour ce projet et j'aimerais gérer moi-même sa distribution. Si vous voyez Bruno sur un nouveau gestionnaire de paquets, merci de créer une _issue_ Github.
|
||||
|
||||
Bien que la majorité de nos fonctionnalités soient gratuites et open source (ce qui couvre les apis REST et GraphQL), nous nous efforçons de trouver un équilibre harmonieux entre les principes de l'open source et la pérennité - https://github.com/usebruno/bruno/discussions/269
|
||||
8
docs/publishing/publishing_pl.md
Normal file
8
docs/publishing/publishing_pl.md
Normal file
@@ -0,0 +1,8 @@
|
||||
[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Türkçe](/docs/publishing/publishing_tr.md) | **Polski** | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md)
|
||||
|
||||
### Publikowanie Bruno w nowym menedżerze pakietów
|
||||
|
||||
Chociaż nasz kod jest otwartoźródłowy i dostępny dla każdego do użytku, uprzejmie prosimy o kontakt z nami przed rozważeniem publikacji w nowych menedżerach pakietów. Jako twórca Bruno, posiadam znak towarowy `Bruno` dla tego projektu i chciałbym zarządzać jego dystrybucją. Jeśli chcesz zobaczyć Bruno w nowym menedżerze pakietów, proszę zgłoś problem na GitHubie.
|
||||
|
||||
Chociaż większość naszych funkcji jest darmowa i otwartoźródłowa (co obejmuje REST i GraphQL Apis),
|
||||
staramy się osiągnąć harmonijny balans między zasadami open-source a zrównoważonym rozwojem - https://github.com/usebruno/bruno/discussions/269
|
||||
@@ -1,3 +1,5 @@
|
||||
[English](/publishing.md) | **Português (BR)** | [Română](docs/publishing/publishing_ro.md) | [Türkçe](/docs/publishing/publishing_tr.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md)
|
||||
|
||||
### Publicando Bruno em um novo gerenciador de pacotes
|
||||
|
||||
Embora nosso código seja de código aberto e esteja disponível para todos usarem, pedimos gentilmente que entre em contato conosco antes de considerar a publicação em novos gerenciadores de pacotes. Como o criador da ferramenta, mantenho a marca registrada `Bruno` para este projeto e gostaria de gerenciar sua distribuição. Se deseja ver o Bruno em um novo gerenciador de pacotes, por favor, solicite através de uma issue no GitHub.
|
||||
|
||||
8
docs/publishing/publishing_ro.md
Normal file
8
docs/publishing/publishing_ro.md
Normal file
@@ -0,0 +1,8 @@
|
||||
[English](/publishing.md) | [Português (BR)](/docs/publishing/publishing_pt_br.md) | **Română** | [Türkçe](/docs/publishing/publishing_tr.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](/docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md)
|
||||
|
||||
### Publicarea lui Bruno la un gestionar de pachete nou
|
||||
|
||||
Deși codul nostru este cu sursă deschisă și disponibil pentru utilizare pentru toată lumea, vă rugăm să ne contactați înainte de a considera publicarea pe gestionari de pachete noi. În calitate de creator al lui Bruno, dețin marca comercială `Bruno` pentru acest proiect și aș dori să gestionez distribuția acestuia. Dacă doriți să-l vedeți pe Bruno pe un gestionar de pachete nou, vă rugăm să creați un issue pe GitHub.
|
||||
|
||||
În timp ce majoritatea funcțiilor noastre sunt gratuite și cu sursă deschisă (ceea ce acoperă API-uri REST și GraphQL),
|
||||
ne străduim să găsim un echilibru armonios între principiile de sursă deschisă și sustenabilitate - https://github.com/usebruno/bruno/discussions/269
|
||||
8
docs/publishing/publishing_tr.md
Normal file
8
docs/publishing/publishing_tr.md
Normal file
@@ -0,0 +1,8 @@
|
||||
[English](../../publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | **Türkçe** | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md)
|
||||
|
||||
### Bruno'yu yeni bir paket yöneticisine yayınlama
|
||||
|
||||
Kodumuz açık kaynak kodlu ve herkesin kullanımına açık olsa da, yeni paket yöneticilerinde yayınlamayı düşünmeden önce bize ulaşmanızı rica ediyoruz. Bruno'nun yaratıcısı olarak, bu proje için `Bruno` ticari markasına sahibim ve dağıtımını yönetmek istiyorum. Bruno'yu yeni bir paket yöneticisinde görmek istiyorsanız, lütfen bir GitHub sorunu oluşturun.
|
||||
|
||||
Özelliklerimizin çoğu ücretsiz ve açık kaynak olsa da (REST ve GraphQL Apis'i kapsar),
|
||||
açık kaynak ilkeleri ile sürdürülebilirlik arasında uyumlu bir denge kurmaya çalışıyoruz - https://github.com/usebruno/bruno/discussions/269
|
||||
@@ -10,7 +10,7 @@
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
[English](../../readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | **বাংলা**
|
||||
[English](../../readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | **বাংলা** | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md)
|
||||
|
||||
ব্রুনো হল একটি নতুন এবং উদ্ভাবনী API ক্লায়েন্ট, যার লক্ষ্য পোস্টম্যান এবং অনুরূপ সরঞ্জাম দ্বারা প্রতিনিধিত্ব করা স্থিতাবস্থায় বিপ্লব ঘটানো।
|
||||
|
||||
@@ -85,7 +85,7 @@ sudo apt install bruno
|
||||
|
||||
### নতুন প্যাকেজ পরিচালকদের কাছে প্রকাশ করা হচ্ছে
|
||||
|
||||
আরও তথ্যের জন্য অনুগ্রহ করে [এখানে](publishing.md) দেখুন।
|
||||
আরও তথ্যের জন্য অনুগ্রহ করে [এখানে](../publishing/publishing_bn.md) দেখুন।
|
||||
|
||||
### অবদান 👩💻🧑💻
|
||||
|
||||
|
||||
131
docs/readme/readme_cn.md
Normal file
131
docs/readme/readme_cn.md
Normal file
@@ -0,0 +1,131 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### Bruno - 开源IDE,用于探索和测试API。
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](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)
|
||||
|
||||
|
||||
Bruno 是一款全新且创新的 API 客户端,旨在颠覆 Postman 和其他类似工具。
|
||||
|
||||
Bruno 直接在您的电脑文件夹中存储您的 API 信息。我们使用纯文本标记语言 Bru 来保存有关 API 的信息。
|
||||
|
||||
您可以使用 Git 或您选择的任何版本控制系统来对您的API信息进行版本控制和协作。
|
||||
|
||||
Bruno 仅限离线使用。我们计划永不向 Bruno 添加云同步功能。我们重视您的数据隐私,并认为它应该留在您的设备上。阅读我们的长期愿景 [点击查看](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
|
||||
📢 观看我们在印度 FOSS 3.0 会议上的最新演讲 [点击查看](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### 安装
|
||||
|
||||
Bruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) Mac、Windows 和 Linux 的可执行文件。
|
||||
|
||||
您也可以通过包管理器如 Homebrew、Chocolatey、Scoop、Snap 和 Apt 安装 Bruno。
|
||||
|
||||
```sh
|
||||
# 在 Mac 电脑上用 Homebrew 安装
|
||||
brew install bruno
|
||||
|
||||
# 在 Windows 上用 Chocolatey 安装
|
||||
choco install bruno
|
||||
|
||||
# 在 Windows 上用 Scoop 安装
|
||||
scoop bucket add extras
|
||||
scoop install bruno
|
||||
|
||||
# 在 Linux 上用 Snap 安装
|
||||
snap install bruno
|
||||
|
||||
# 在 Linux 上用 Apt 安装
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
```
|
||||
|
||||
### 在 Mac 上通过 Homebrew 安装 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Collaborate 安装 👩💻🧑💻
|
||||
|
||||
或者任何你选择的版本控制系统
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### 重要链接 📌
|
||||
|
||||
- [我们的愿景](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).
|
||||
|
||||
### 展示 🎥
|
||||
|
||||
- [Testimonials](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### 支持 ❤️
|
||||
|
||||
如果您喜欢 Bruno 并想支持我们的开源工作,请考虑通过 [Github Sponsors](https://github.com/sponsors/helloanoop) 来赞助我们。
|
||||
|
||||
### 分享评价 📣
|
||||
|
||||
如果 Bruno 在您的工作和团队中帮助了您,请不要忘记在我们的 GitHub 讨论上分享您的 [评价](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
### 发布到新的包管理器
|
||||
|
||||
有关更多信息,请参见 [此处](../../publishing.md) 。
|
||||
|
||||
### 贡献 👩💻🧑💻
|
||||
|
||||
我很高兴您希望改进bruno。请查看 [贡献指南](../../contributing.md)。
|
||||
|
||||
即使您无法通过代码做出贡献,我们仍然欢迎您提出BUG和新的功能需求。
|
||||
|
||||
### 作者
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### 联系方式 🌐
|
||||
|
||||
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
|
||||
[Website](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[LinkedIn](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### 商标
|
||||
|
||||
**名称**
|
||||
|
||||
`Bruno` 是由 [Anoop M D](https://www.helloanoop.com/) 持有的商标。
|
||||
|
||||
**Logo**
|
||||
|
||||
Logo 源自 [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### 许可证 📄
|
||||
|
||||
[MIT](../../license.md)
|
||||
@@ -14,11 +14,11 @@
|
||||
|
||||
Bruno ist ein neuer und innovativer API-Client, der den Status Quo von Postman und ähnlichen Tools revolutionieren soll.
|
||||
|
||||
Bruno speichert Deine Sammlungen direkt in einem Ordner in Deinem Dateisystem. Wir verwenden eine einfache Textauszeichnungssprache - Bru - um Informationen über API-Anfragen zu speichern.
|
||||
Bruno speichert deine Sammlungen direkt in einem Ordner in deinem Dateisystem. Wir verwenden eine einfache Textauszeichnungssprache - Bru - um Informationen über API-Anfragen zu speichern.
|
||||
|
||||
Du kannst Git oder eine andere Versionskontrolle deiner Wahl verwenden, um an deinen API-Sammlungen gemeinsam mit anderen zu arbeiten.
|
||||
Du kannst Git oder eine andere Versionskontrolle deiner Wahl verwenden, um gemeinsam mit anderen an deinen API-Sammlungen zu arbeiten.
|
||||
|
||||
Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno eine Cloud-Synchronisation hinzuzufügen. Wir schätzen den Schutz Deiner Daten und glauben, dass sie auf Deinem Gerät bleiben sollten. Lies unsere Langzeit-Vision [hier](https://github.com/usebruno/bruno/discussions/269).
|
||||
Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Synchronisation zu erweitern. Wir schätzen den Schutz deiner Daten und glauben, dass sie auf deinem Gerät bleiben sollten. Lies unsere Langzeit-Vision [hier](https://github.com/usebruno/bruno/discussions/269).
|
||||
|
||||
 <br /><br />
|
||||
|
||||
@@ -26,9 +26,9 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno eine Cloud-Synchr
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Zusammenarbeiten mit Git 👩💻🧑💻
|
||||
### Zusammenarbeit mit Git 👩💻🧑💻
|
||||
|
||||
oder eine Versionskontrolle Deiner Wahl
|
||||
Oder einer Versionskontrolle deiner Wahl
|
||||
|
||||
 <br /><br />
|
||||
|
||||
@@ -49,21 +49,21 @@ oder eine Versionskontrolle Deiner Wahl
|
||||
|
||||
### Unterstützung ❤️
|
||||
|
||||
Wuff! Wenn Du dieses Projekt magst, klick den ⭐ Button !!
|
||||
Wuff! Wenn du dieses Projekt magst, klick den ⭐ Button !!
|
||||
|
||||
### Teile Erfahrungsberichte 📣
|
||||
|
||||
Wenn Bruno Dir bei Deiner Arbeit und in Deinen Teams geholfen hat, vergiss bitte nicht, Deine [Erfahrungsberichte auf unserer GitHub-Diskussion](https://github.com/usebruno/bruno/discussions/343) zu teilen.
|
||||
Wenn Bruno dir und in deinen Teams bei der Arbeit geholfen hat, vergiss bitte nicht, deine [Erfahrungsberichte auf unserer GitHub-Diskussion](https://github.com/usebruno/bruno/discussions/343) zu teilen.
|
||||
|
||||
### Veröffentlichung in neuen Paketmanagern
|
||||
### Bereitstellung in neuen Paket-Managern
|
||||
|
||||
Bitte [hier](/publishing.md) für mehr Informationen lesen.
|
||||
Mehr Informationen findest du [hier](/publishing.md).
|
||||
|
||||
### Mitmachen 👩💻🧑💻
|
||||
|
||||
Ich freue mich, dass Du Bruno verbessern willst. Bitte schau Dir den [Leitfaden zum Mitmachen](../contributing/contributing_de.md) an.
|
||||
Ich freue mich, dass du Bruno verbessern willst. Bitte schau dir den [Leitfaden zum Mitmachen](../contributing/contributing_de.md) an.
|
||||
|
||||
Auch wenn Du nicht in der Lage bist, einen Beitrag in Form von Code zu leisten, zögere bitte nicht, uns Fehler und Funktionswünsche mitzuteilen, die implementiert werden müssen, um Deinen Anwendungsfall zu unterstützen.
|
||||
Auch wenn du nicht in der Lage bist, einen Beitrag in Form von Code zu leisten, zögere bitte nicht, uns Fehler und Funktionswünsche mitzuteilen, die implementiert werden müssen, um deinen Anwendungsfall zu unterstützen.
|
||||
|
||||
### Autoren
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
|
||||
[English](/readme.md) | [Українська](/readme_ua.md) | [Русский](/readme_ru.md) | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md) | **Français** | [বাংলা](docs/readme/readme_bn.md)
|
||||
[English](/readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | **Français** | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md)
|
||||
|
||||
Bruno est un nouveau client API, innovant, qui a pour but de révolutionner le _status quo_ que représente Postman et les autres outils.
|
||||
|
||||
@@ -21,8 +21,42 @@ Vous pouvez utiliser git ou tout autre gestionnaire de version pour travailler d
|
||||
|
||||
Bruno ne fonctionne qu'en mode déconnecté. Il n'y a pas de d'abonnement ou de synchronisation avec le cloud Bruno, il n'y en aura jamais. Nous sommes conscients de la confidentialité de vos données et nous sommes convaincus qu'elles doivent rester sur vos appareils. Vous pouvez lire notre vision à long terme [ici (en anglais)](https://github.com/usebruno/bruno/discussions/269).
|
||||
|
||||
|
||||
📢 Regarder notre présentation récente lors de la conférence India FOSS 3.0 (en anglais) [ici](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Installation
|
||||
|
||||
Bruno est disponible au téléchargement [sur notre site web](https://www.usebruno.com/downloads), pour Mac, Windows et Linux.
|
||||
|
||||
Vous pouvez aussi installer Bruno via un gestionnaire de paquets, comme Homebrew, Chocolatey, Scoop, Snap et Apt.
|
||||
|
||||
```sh
|
||||
# Mac via Homebrew
|
||||
brew install bruno
|
||||
|
||||
# Windows via Chocolatey
|
||||
choco install bruno
|
||||
|
||||
# Windows via Scoop
|
||||
scoop bucket add extras
|
||||
scoop install bruno
|
||||
|
||||
# Linux via Snap
|
||||
snap install bruno
|
||||
|
||||
# Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
```
|
||||
|
||||
### Fonctionne sur de multiples platformes 🖥️
|
||||
|
||||
 <br /><br />
|
||||
@@ -41,6 +75,7 @@ Ou n'importe quel système de gestion de sources
|
||||
- [Site web](https://www.usebruno.com)
|
||||
- [Prix](https://www.usebruno.com/pricing)
|
||||
- [Téléchargement](https://www.usebruno.com/downloads)
|
||||
- [Sponsors Github](https://github.com/sponsors/helloanoop)
|
||||
|
||||
### Showcase 🎥
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Bruno funziona solo in modalità offline. Non ci sono piani per aggiungere la si
|
||||
|
||||
### Installazione
|
||||
|
||||
Bruno è disponisible come download binario [sul nostro sito](https://www.usebruno.com/downloads) per Mac, Windows e Linux.
|
||||
Bruno è disponibile come download binario [sul nostro sito](https://www.usebruno.com/downloads) per Mac, Windows e Linux.
|
||||
|
||||
Puoi installare Bruno anche tramite package manger come Homebrew, Chocolatey, Snap e Apt.
|
||||
|
||||
|
||||
129
docs/readme/readme_pl.md
Normal file
129
docs/readme/readme_pl.md
Normal file
@@ -0,0 +1,129 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### Bruno - Otwartoźródłowe IDE do exploracji i testów APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
[English](/readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md)) | [한국어](docs/readme/readme_kr.md) ) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | **Polski**
|
||||
|
||||
Bruno to nowy i innowacyjny klient API, którego celem jest zrewolucjonizowanie status quo reprezentowy przez Postman i podobne narzędzia.
|
||||
|
||||
Bruno przechowuje twoje kolekcje bezpośrednio w folderze na twoim systemie plików. Używamy prostego języka znaczników, Bru, do zapisywania informacji o żądaniach API.
|
||||
|
||||
Możesz użyć Git lub dowolnego systemu kontroli wersji do współpracy nad swoimi kolekcjami API.
|
||||
|
||||
Bruno działa tylko w trybie offline. Nie planujemy nigdy dodawać synchronizacji w chmurze do Bruno. Cenimy prywatność Twoich danych i wierzymy, że powinny one pozostać na Twoim urządzeniu. Przeczytaj naszą długoterminową wizję [tutaj](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
📢 Obejrzyj naszą ostatnią rozmowę na konferencji India FOSS 3.0 [tutaj](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Instalacja
|
||||
|
||||
Bruno jest dostępny jako plik binarny do pobrania [na naszej stronie internetowej](https://www.usebruno.com/downloads) dla Mac, Windows i Linux.
|
||||
|
||||
Możesz również zainstalować Bruno za pomocą menedżerów pakietów, takich jak Homebrew, Chocolatey, Scoop, Snap i Apt.
|
||||
|
||||
```sh
|
||||
# On Mac via Homebrew
|
||||
brew install bruno
|
||||
|
||||
# On Windows via Chocolatey
|
||||
choco install bruno
|
||||
|
||||
# On Windows via Scoop
|
||||
scoop bucket add extras
|
||||
scoop install bruno
|
||||
|
||||
# On Linux via Snap
|
||||
snap install bruno
|
||||
|
||||
# On Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
```
|
||||
|
||||
### Uruchom na wielu platformach 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Współpracuj przez Git 👩💻🧑💻
|
||||
|
||||
Lub dowolny inny system kontroli wersji, który wybierzesz
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Ważne Linki 📌
|
||||
|
||||
- [Nasza Długoterminowa Wizja](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [Mapa Drogi](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [Dokumentacja](https://docs.usebruno.com)
|
||||
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
|
||||
- [Strona Internetowa](https://www.usebruno.com)
|
||||
- [Cennik](https://www.usebruno.com/pricing)
|
||||
- [Pobieranie](https://www.usebruno.com/downloads)
|
||||
- [Sponsorzy Github](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### Zobacz 🎥
|
||||
|
||||
- [Opinie](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Centrum Wiedzy](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### Wsparcie ❤️
|
||||
|
||||
Jeśli podoba Ci się Bruno i chcesz wspierać naszą pracę opensource, rozważ sponsorowanie nas przez [Sponsorzy Github](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### Udostępnij Opinie 📣
|
||||
|
||||
Jeśli Bruno pomógł Tobie w pracy i Twoim zespołom, nie zapomnij podzielić się swoimi [opiniami na naszej dyskusji GitHub](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
### Publikowanie w Nowych Menedżerach Pakietów
|
||||
|
||||
Więcej informacji znajdziesz [tutaj](publishing.md).
|
||||
|
||||
### Współpraca 👩💻🧑💻
|
||||
|
||||
Cieszę się, że chcesz udoskonalić bruno. Proszę sprawdź [przewodnik współpracy](contributing.md)
|
||||
|
||||
Nawet jeśli nie jesteś w stanie przyczynić się poprzez kod, nie wahaj się zgłaszać błędów i wniosków o funkcje, które muszą zostać zaimplementowane, aby rozwiązać Twój przypadek użycia.
|
||||
|
||||
### Autorzy
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Pozostań w kontakcie 🌐
|
||||
|
||||
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
|
||||
[Strona Internetowa](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[LinkedIn](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### Znak Towarowy
|
||||
|
||||
**Nazwa**
|
||||
|
||||
`Bruno` jest znakiem towarowym należącym do [Anoop M D](https://www.helloanoop.com/)
|
||||
|
||||
**Logo**
|
||||
|
||||
Logo pochodzi z [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licencja: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### Licencja 📄
|
||||
|
||||
[MIT](license.md)
|
||||
125
docs/readme/readme_ro.md
Normal file
125
docs/readme/readme_ro.md
Normal file
@@ -0,0 +1,125 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### Bruno - Mediu integrat de dezvoltare cu sursă deschisă pentru explorarea și testarea API-urilor.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
[English](/readme.md) | [Українська](/docs/readme/readme_ua.md) | [Русский](/docs/readme/readme_ru.md) | [Türkçe](/docs/readme/readme_tr.md) | [Deutsch](/docs/readme/readme_de.md) | [Français](/docs/readme/readme_fr.md) | [Português (BR)](/docs/readme/readme_pt_br.md)) | [한국어](/docs/readme/readme_kr.md) | [বাংলা](/docs/readme/readme_bn.md) | [Español](/docs/readme/readme_es.md) | [Italiano](/docs/readme/readme_it.md) | **Română**
|
||||
|
||||
Bruno este un client API nou și inovativ, care vizează să revoluționeze status quo-ul reprezentat de Postman și alte instrumente similare.
|
||||
|
||||
Bruno salvează colecțiile voastre direct într-o mapă din sistemul dvs. de fișiere. Folosim un limbaj de marcare cu text simplu, Bru, pentru a salva informații despre cererile API.
|
||||
|
||||
Puteți folosi Git sau orice altă unealtă de control al versiunii la alegere pentru a colabora la colecțiile API voastre.
|
||||
|
||||
Bruno este numai offline. Nu va exista niciodată vreun plan pentru a adăuga sincronizarea cloud la Bruno. Noi valorăm confidențialitatea datelor voastre și credem că ar trebui să rămână pe dispozitivul vostru. Citiți viziunea noastră pe termen lung [aici](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
📢 Priviți prezentarea noastră recentă de la India FOSS 3.0 Conference [aici](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Instalarea
|
||||
|
||||
Bruno este disponibil ca descărcare binară [pe website-ul nostru](https://www.usebruno.com/downloads) pentru Mac, Windows și Linux.
|
||||
|
||||
De asemenea, puteţi instala Bruno cu un gestionar de pachete precum Homebrew, Chocolatey, Snap şi Apt.
|
||||
|
||||
```sh
|
||||
# Pe Mac cu Homebrew
|
||||
brew install bruno
|
||||
|
||||
# Pe Windows cu Chocolatey
|
||||
choco install bruno
|
||||
|
||||
# Pe Linux cu Snap
|
||||
snap install bruno
|
||||
|
||||
# Pe Linux cu Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
```
|
||||
|
||||
### Utilizați pe mai multe platforme 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Colaborați cu Git 👩💻🧑💻
|
||||
|
||||
Sau orice unealtă de control al versiunii la alegere
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Linkuri importante 📌
|
||||
|
||||
- [Viziunea noastră pe termen lung](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [Documentație](https://docs.usebruno.com)
|
||||
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
|
||||
- [Website](https://www.usebruno.com)
|
||||
- [Prețuri](https://www.usebruno.com/pricing)
|
||||
- [Descărcări](https://www.usebruno.com/downloads)
|
||||
- [Sponsori GitHub](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### Vitrina 🎥
|
||||
|
||||
- [Recenzii](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Centrul de cunoștințe](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### Sprijiniți ❤️
|
||||
|
||||
Dacă vă place Bruno și doriți să sprijiniți munca noastră de sursă deschisă, puteți considera să ne sponsorizați [pe GitHub](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### Distribuiți recenziile 📣
|
||||
|
||||
Dacă Bruno va ajutat la locul de muncă și la echipele dvs., vă rugăm să nu uitați să distribuiți [recenziile în discuția noastră GitHub](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
### Publicarea la gestionari de pachete noi
|
||||
|
||||
Vă rugăm să citiţi [aici](/docs/publishing/publishing_ro.md) pentru mai multă informaţie.
|
||||
|
||||
### Contribuiți 👩💻🧑💻
|
||||
|
||||
Mă bucur că doriți să îmbunătățiți Bruno. Vă rugăm să consultați [ghidul pentru contribuire](/docs/contributing/contributing_ro.md)
|
||||
|
||||
Chiar dacă nu puteți face contribuții prin cod, vă rugăm să nu ezitați să raportați erori și să solicitați funcții care trebuie implementate pentru a rezolva cazul dvs. de utilizare.
|
||||
|
||||
### Autori
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Păstrați legătura 🌐
|
||||
|
||||
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
|
||||
[Website](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[LinkedIn](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### Marcă comercială
|
||||
|
||||
**Nume**
|
||||
|
||||
`Bruno` este o marcă deținută de [Anoop M D](https://www.helloanoop.com/)
|
||||
|
||||
**Logo**
|
||||
|
||||
Logo-ul provine de la [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licența: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### Licența 📄
|
||||
|
||||
[MIT](license.md)
|
||||
@@ -10,23 +10,55 @@
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
[English](/readme.md) | [Українська](/readme_ua.md) | [Русский](/readme_ru.md) | **Türkçe** | [Deutsch](/readme_de.md) | [Français](/readme_fr.md) | [বাংলা](docs/readme/readme_bn.md)
|
||||
[English](../../readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | **Türkçe** | [Deutsch](docs/readme/readme_de.md) | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md) | [简体中文](docs/readme/readme_cn.md)
|
||||
|
||||
Bruno, Postman ve benzeri araçlar tarafından temsil edilen statükoda devrim yaratmayı amaçlayan yeni ve yenilikçi bir API istemcisidir.
|
||||
|
||||
Bruno koleksiyonlarınızı doğrudan dosya sisteminizdeki bir klasörde saklar. API istekleri hakkındaki bilgileri kaydetmek için düz bir metin biçimlendirme dili olan Bru kullanıyoruz.
|
||||
|
||||
API koleksiyonlarınız üzerinde işbirliği yapmak için git veya seçtiğiniz herhangi bir sürüm kontrolünü kullanabilirsiniz.
|
||||
API koleksiyonlarınız üzerinde işbirliği yapmak için Git veya seçtiğiniz herhangi bir sürüm kontrolünü kullanabilirsiniz.
|
||||
|
||||
Bruno yalnızca çevrimdışıdır. Bruno'ya bulut senkronizasyonu eklemek gibi bir planımız yok. Veri gizliliğinize değer veriyoruz ve cihazınızda kalması gerektiğine inanıyoruz. Uzun vadeli vizyonumuzu okuyun [burada](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
📢 Hindistan FOSS 3.0 Konferansındaki son konuşmamızı izleyin [burada](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Kurulum
|
||||
|
||||
Bruno Mac, Windows ve Linux için ikili indirme olarak [web sitemizde](https://www.usebruno.com/downloads) mevcuttur.
|
||||
|
||||
Bruno'yu Homebrew, Chocolatey, Scoop, Snap ve Apt gibi paket yöneticileri aracılığıyla da yükleyebilirsiniz.
|
||||
|
||||
```sh
|
||||
# Homebrew aracılığıyla Mac'te
|
||||
brew install bruno
|
||||
|
||||
# Chocolatey aracılığıyla Windows'ta
|
||||
choco install bruno
|
||||
|
||||
# Scoop aracılığıyla Windows'ta
|
||||
scoop bucket add extras
|
||||
scoop install bruno
|
||||
|
||||
# Snap aracılığıyla Linux'ta
|
||||
snap install bruno
|
||||
|
||||
# Apt aracılığıyla Linux'ta
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
```
|
||||
|
||||
### Birden fazla platformda çalıştırın 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Git üzerinden işbirliği yapın 👩💻🧑💻
|
||||
### Git üzerinden katkıda bulunun 👩💻🧑💻
|
||||
|
||||
Veya seçtiğiniz herhangi bir sürüm kontrol sistemi
|
||||
|
||||
@@ -37,8 +69,11 @@ Veya seçtiğiniz herhangi bir sürüm kontrol sistemi
|
||||
- [Uzun Vadeli Vizyonumuz](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [Yol Haritası](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [Dokümantasyon](https://docs.usebruno.com)
|
||||
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
|
||||
- [Web sitesi](https://www.usebruno.com)
|
||||
- [Fiyatlandırma](https://www.usebruno.com/pricing)
|
||||
- [İndir](https://www.usebruno.com/downloads)
|
||||
- [Github Sponsorları](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### Vitrin 🎥
|
||||
|
||||
@@ -48,15 +83,19 @@ Veya seçtiğiniz herhangi bir sürüm kontrol sistemi
|
||||
|
||||
### Destek ❤️
|
||||
|
||||
Woof! Projeyi beğendiyseniz, şu ⭐ düğmesine basın!
|
||||
Bruno'yu seviyorsanız ve açık kaynak çalışmalarımızı desteklemek istiyorsanız, [Github Sponsorları](https://github.com/sponsors/helloanoop) aracılığıyla bize sponsor olmayı düşünün.
|
||||
|
||||
### Referansları Paylaşın 📣
|
||||
|
||||
Bruno işinizde ve ekiplerinizde size yardımcı olduysa, lütfen [github tartışmamızdaki referanslarınızı](https://github.com/usebruno/bruno/discussions/343) paylaşmayı unutmayın
|
||||
Bruno işinizde ve ekiplerinizde size yardımcı olduysa, lütfen [github tartışmamızdaki referanslarınızı](https://github.com/usebruno/bruno/discussions/343) paylaşmayı unutmayın.
|
||||
|
||||
### Yeni Paket Yöneticilerine Yayınlama
|
||||
|
||||
Daha fazla bilgi için lütfen [buraya](publishing.md) bakın.
|
||||
|
||||
### Katkıda Bulunun 👩💻🧑💻
|
||||
|
||||
Bruno'yu geliştirmek istemenize sevindim. Lütfen [katkıda bulunma kılavuzu](../contributing/contributing.md)'na göz atın
|
||||
Bruno'yu geliştirmek istemenize sevindim. Lütfen [katkıda bulunma kılavuzuna](contributing.md) göz atın
|
||||
|
||||
Kod yoluyla katkıda bulunamasanız bile, lütfen kullanım durumunuzu çözmek için uygulanması gereken hataları ve özellik isteklerini bildirmekten çekinmeyin.
|
||||
|
||||
@@ -70,11 +109,21 @@ Kod yoluyla katkıda bulunamasanız bile, lütfen kullanım durumunuzu çözmek
|
||||
|
||||
### İletişimde Kalın 🌐
|
||||
|
||||
[Twitter](https://twitter.com/use_bruno) <br />
|
||||
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
|
||||
[Website](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq)
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[LinkedIn](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### Ticari Marka
|
||||
|
||||
**İsim**
|
||||
|
||||
`Bruno` [Anoop M D](https://www.helloanoop.com/) tarafından sahip olunan bir ticari markadır.
|
||||
|
||||
**Logo**
|
||||
|
||||
Logo [OpenMoji](https://openmoji.org/library/emoji-1F436/) adresinden alınmıştır. Lisans: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### Lisans 📄
|
||||
|
||||
[MIT](/license.md)
|
||||
[MIT](license.md)
|
||||
|
||||
4593
package-lock.json
generated
4593
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,12 +5,13 @@
|
||||
"packages/bruno-app",
|
||||
"packages/bruno-electron",
|
||||
"packages/bruno-cli",
|
||||
"packages/bruno-tauri",
|
||||
"packages/bruno-common",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-query",
|
||||
"packages/bruno-js",
|
||||
"packages/bruno-lang",
|
||||
"packages/bruno-testbench",
|
||||
"packages/bruno-toml",
|
||||
"packages/bruno-graphql-docs"
|
||||
],
|
||||
"homepage": "https://usebruno.com",
|
||||
@@ -18,18 +19,20 @@
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"fs-extra": "^11.1.1",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.2.0",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"ts-jest": "^29.0.5",
|
||||
"fs-extra": "^11.1.1"
|
||||
"ts-jest": "^29.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
|
||||
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
"build:electron": "node ./scripts/build-electron.js",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"target": "es2017",
|
||||
"allowSyntheticDefaultImports": false,
|
||||
"baseUrl": "./",
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
"@usebruno/schema": "0.6.0",
|
||||
"axios": "^1.5.1",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
"codemirror-graphql": "^1.2.5",
|
||||
"codemirror": "5.65.2",
|
||||
"codemirror-graphql": "1.2.5",
|
||||
"cookie": "^0.6.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"file": "^0.2.2",
|
||||
@@ -39,6 +39,7 @@
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"jsesc": "^3.0.2",
|
||||
"jsonpath-plus": "^7.2.0",
|
||||
"jshint": "^2.13.6",
|
||||
"jsonlint": "^1.6.3",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
@@ -54,6 +55,7 @@
|
||||
"qs": "^6.11.0",
|
||||
"query-string": "^7.0.1",
|
||||
"react": "18.2.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
@@ -64,6 +66,7 @@
|
||||
"react-redux": "^7.2.6",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
"strip-json-comments": "^5.0.1",
|
||||
"styled-components": "^5.3.3",
|
||||
"system": "^2.0.1",
|
||||
"tailwindcss": "^2.2.19",
|
||||
|
||||
@@ -12,6 +12,7 @@ import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import jsonlint from 'jsonlint';
|
||||
import { JSHINT } from 'jshint';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
@@ -27,10 +28,12 @@ if (!SERVER_RENDERED) {
|
||||
'res.statusText',
|
||||
'res.headers',
|
||||
'res.body',
|
||||
'res.responseTime',
|
||||
'res.getStatus()',
|
||||
'res.getHeader(name)',
|
||||
'res.getHeaders()',
|
||||
'res.getBody()',
|
||||
'res.getResponseTime()',
|
||||
'req',
|
||||
'req.url',
|
||||
'req.method',
|
||||
@@ -57,7 +60,8 @@ if (!SERVER_RENDERED) {
|
||||
'bru.getEnvVar(key)',
|
||||
'bru.setEnvVar(key,value)',
|
||||
'bru.getVar(key)',
|
||||
'bru.setVar(key,value)'
|
||||
'bru.setVar(key,value)',
|
||||
'bru.setNextRequest(requestName)'
|
||||
];
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
const cursor = editor.getCursor();
|
||||
@@ -100,6 +104,12 @@ export default class CodeEditor extends React.Component {
|
||||
// unnecessary updates during the update lifecycle.
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
|
||||
this.lintOptions = {
|
||||
esversion: 11,
|
||||
expr: true,
|
||||
asi: true
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -115,7 +125,7 @@ export default class CodeEditor extends React.Component {
|
||||
showCursorWhenSelecting: true,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
||||
lint: { esversion: 11 },
|
||||
lint: this.lintOptions,
|
||||
readOnly: this.props.readOnly,
|
||||
scrollbarStyle: 'overlay',
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
@@ -183,8 +193,30 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
}));
|
||||
CodeMirror.registerHelper('lint', 'json', function (text) {
|
||||
let found = [];
|
||||
if (!window.jsonlint) {
|
||||
if (window.console) {
|
||||
window.console.error('Error: window.jsonlint not defined, CodeMirror JSON linting cannot run.');
|
||||
}
|
||||
return found;
|
||||
}
|
||||
let jsonlint = window.jsonlint.parser || window.jsonlint;
|
||||
jsonlint.parseError = function (str, hash) {
|
||||
let loc = hash.loc;
|
||||
found.push({
|
||||
from: CodeMirror.Pos(loc.first_line - 1, loc.first_column),
|
||||
to: CodeMirror.Pos(loc.last_line - 1, loc.last_column),
|
||||
message: str
|
||||
});
|
||||
};
|
||||
try {
|
||||
jsonlint.parse(stripJsonComments(text.replace(/(?<!"[^":{]*){{[^}]*}}(?![^"},]*")/g, '1')));
|
||||
} catch (e) {}
|
||||
return found;
|
||||
});
|
||||
if (editor) {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? { esversion: 11 } : false);
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
}
|
||||
@@ -274,7 +306,7 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? { esversion: 11 } : false);
|
||||
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
this.cachedValue = this.editor.getValue();
|
||||
if (this.props.onEdit) {
|
||||
this.props.onEdit(this.cachedValue);
|
||||
|
||||
@@ -3,7 +3,7 @@ import get from 'lodash/get';
|
||||
import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Markdown from 'components/MarkDown';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
@@ -14,6 +14,7 @@ const Docs = ({ collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const docs = get(collection, 'root.docs', '');
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const toggleViewMode = () => {
|
||||
setIsEditing((prev) => !prev);
|
||||
@@ -44,6 +45,7 @@ const Docs = ({ collection }) => {
|
||||
onEdit={onEdit}
|
||||
onSave={onSave}
|
||||
mode="application/text"
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
/>
|
||||
) : (
|
||||
<Markdown onDoubleClick={toggleViewMode} content={docs} />
|
||||
|
||||
@@ -164,7 +164,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
http
|
||||
HTTP
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
@@ -175,7 +175,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
https
|
||||
HTTPS
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
@@ -186,7 +186,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
socks4
|
||||
SOCKS4
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
@@ -197,7 +197,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
socks5
|
||||
SOCKS5
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateCollectionRequestScript, updateCollectionResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -13,6 +13,7 @@ const Script = ({ collection }) => {
|
||||
const responseScript = get(collection, 'root.request.script.res', '');
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
@@ -47,6 +48,7 @@ const Script = ({ collection }) => {
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 mt-6">
|
||||
@@ -58,6 +60,7 @@ const Script = ({ collection }) => {
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateCollectionTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -12,6 +12,7 @@ const Tests = ({ collection }) => {
|
||||
const tests = get(collection, 'root.request.tests', '');
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
@@ -33,6 +34,7 @@ const Tests = ({ collection }) => {
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
|
||||
16
packages/bruno-app/src/components/Cookies/StyledWrapper.js
Normal file
16
packages/bruno-app/src/components/Cookies/StyledWrapper.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,28 +1,52 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import Modal from 'components/Modal';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { deleteCookiesForDomain } from 'providers/ReduxStore/slices/app';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CollectionProperties = ({ onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const cookies = useSelector((state) => state.app.cookies) || [];
|
||||
|
||||
const handleDeleteDomain = (domain) => {
|
||||
dispatch(deleteCookiesForDomain(domain))
|
||||
.then(() => {
|
||||
toast.success('Domain deleted successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to delete domain'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal size="md" title="Cookies" hideFooter={true} handleCancel={onClose}>
|
||||
<table className="w-full border-collapse" style={{ marginTop: '-1rem' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-2 px-2 text-left">Domain</th>
|
||||
<th className="py-2 px-2 text-left">Cookie</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cookies.map((cookie) => (
|
||||
<tr key={cookie.id}>
|
||||
<td className="py-2 px-2">{cookie.domain}</td>
|
||||
<td className="py-2 px-2 break-all">{cookie.cookieString}</td>
|
||||
<StyledWrapper>
|
||||
<table className="w-full border-collapse" style={{ marginTop: '-1rem' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-2 px-2 text-left">Domain</th>
|
||||
<th className="py-2 px-2 text-left">Cookie</th>
|
||||
<th className="py-2 px-2 text-center" style={{ width: 80 }}>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cookies.map((cookie) => (
|
||||
<tr key={cookie.domain}>
|
||||
<td className="py-2 px-2">{cookie.domain}</td>
|
||||
<td className="py-2 px-2 break-all">{cookie.cookieString}</td>
|
||||
<td className="text-center">
|
||||
<button tabIndex="-1" onClick={() => handleDeleteDomain(cookie.domain)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'github-markdown-css/github-markdown.css';
|
||||
import get from 'lodash/get';
|
||||
import { updateRequestDocs } from 'providers/ReduxStore/slices/collections';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Markdown from 'components/MarkDown';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
@@ -14,6 +14,7 @@ const Documentation = ({ item, collection }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const docs = item.draft ? get(item, 'draft.request.docs') : get(item, 'request.docs');
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const toggleViewMode = () => {
|
||||
setIsEditing((prev) => !prev);
|
||||
@@ -45,6 +46,7 @@ const Documentation = ({ item, collection }) => {
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={storedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
value={docs || ''}
|
||||
onEdit={onEdit}
|
||||
onSave={onSave}
|
||||
|
||||
@@ -10,7 +10,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { uuid } from 'utils/common';
|
||||
import { envVariableNameRegex } from 'utils/common/regex';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -25,8 +25,8 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
name: Yup.string()
|
||||
.required('Name cannot be empty')
|
||||
.matches(
|
||||
envVariableNameRegex,
|
||||
'Name contains invalid characters. Must only contain alphanumeric characters, "-" and "_"'
|
||||
variableNameRegex,
|
||||
'Name contains invalid characters. Must only contain alphanumeric characters, "-", "_", "." and cannot start with a digit.'
|
||||
)
|
||||
.trim(),
|
||||
secret: Yup.boolean(),
|
||||
@@ -52,7 +52,6 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
|
||||
const ErrorMessage = ({ name }) => {
|
||||
const meta = formik.getFieldMeta(name);
|
||||
console.log(name, meta);
|
||||
if (!meta.error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,14 @@ const StyledMarkdownBodyWrapper = styled.div`
|
||||
pre {
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
}
|
||||
|
||||
table {
|
||||
th,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
background-color: ${(props) => props.theme.bg};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as React from 'react';
|
||||
|
||||
const md = new MarkdownIt();
|
||||
|
||||
const Markdown = ({ onDoubleClick, content }) => {
|
||||
const handleOnDoubleClick = (event) => {
|
||||
switch (event.detail) {
|
||||
case 2: {
|
||||
onDoubleClick();
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
if (event?.detail === 2) {
|
||||
onDoubleClick();
|
||||
}
|
||||
};
|
||||
const htmlFromMarkdown = md.render(content || '');
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ModalHeader = ({ title, handleCancel }) => (
|
||||
@@ -62,6 +62,9 @@ const Modal = ({
|
||||
confirmDisabled,
|
||||
hideCancel,
|
||||
hideFooter,
|
||||
disableCloseOnOutsideClick,
|
||||
disableEscapeKey,
|
||||
onClick,
|
||||
closeModalFadeTimeout = 500
|
||||
}) => {
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
@@ -78,12 +81,13 @@ const Modal = ({
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (disableEscapeKey) return;
|
||||
document.addEventListener('keydown', escFunction, false);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', escFunction, false);
|
||||
};
|
||||
}, []);
|
||||
}, [disableEscapeKey, document]);
|
||||
|
||||
let classes = 'bruno-modal';
|
||||
if (isClosing) {
|
||||
@@ -93,7 +97,7 @@ const Modal = ({
|
||||
classes += ' modal-footer-none';
|
||||
}
|
||||
return (
|
||||
<StyledWrapper className={classes}>
|
||||
<StyledWrapper className={classes} onClick={onClick ? (e) => onClick(e) : null}>
|
||||
<div className={`bruno-modal-card modal-${size}`}>
|
||||
<ModalHeader title={title} handleCancel={() => closeModal({ type: 'icon' })} />
|
||||
<ModalContent>{children}</ModalContent>
|
||||
@@ -111,9 +115,13 @@ const Modal = ({
|
||||
{/* Clicking on backdrop closes the modal */}
|
||||
<div
|
||||
className="bruno-modal-backdrop"
|
||||
onClick={() => {
|
||||
closeModal({ type: 'backdrop' });
|
||||
}}
|
||||
onClick={
|
||||
disableCloseOnOutsideClick
|
||||
? null
|
||||
: () => {
|
||||
closeModal({ type: 'backdrop' });
|
||||
}
|
||||
}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useFormik } from 'formik';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
import path from 'path';
|
||||
import slash from 'utils/common/slash';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
|
||||
const General = ({ close }) => {
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const dispatch = useDispatch();
|
||||
const inputFileCaCertificateRef = useRef();
|
||||
|
||||
const preferencesSchema = Yup.object().shape({
|
||||
sslVerification: Yup.boolean(),
|
||||
customCaCertificate: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
filePath: Yup.string().nullable()
|
||||
}),
|
||||
storeCookies: Yup.boolean(),
|
||||
sendCookies: Yup.boolean(),
|
||||
timeout: Yup.mixed()
|
||||
.transform((value, originalValue) => {
|
||||
return originalValue === '' ? undefined : value;
|
||||
@@ -28,7 +39,13 @@ const General = ({ close }) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
sslVerification: preferences.request.sslVerification,
|
||||
timeout: preferences.request.timeout
|
||||
customCaCertificate: {
|
||||
enabled: get(preferences, 'request.customCaCertificate.enabled', false),
|
||||
filePath: get(preferences, 'request.customCaCertificate.filePath', null)
|
||||
},
|
||||
timeout: preferences.request.timeout,
|
||||
storeCookies: get(preferences, 'request.storeCookies', true),
|
||||
sendCookies: get(preferences, 'request.sendCookies', true)
|
||||
},
|
||||
validationSchema: preferencesSchema,
|
||||
onSubmit: async (values) => {
|
||||
@@ -47,7 +64,13 @@ const General = ({ close }) => {
|
||||
...preferences,
|
||||
request: {
|
||||
sslVerification: newPreferences.sslVerification,
|
||||
timeout: newPreferences.timeout
|
||||
customCaCertificate: {
|
||||
enabled: newPreferences.customCaCertificate.enabled,
|
||||
filePath: newPreferences.customCaCertificate.filePath
|
||||
},
|
||||
timeout: newPreferences.timeout,
|
||||
storeCookies: newPreferences.storeCookies,
|
||||
sendCookies: newPreferences.sendCookies
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -57,24 +80,112 @@ const General = ({ close }) => {
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update preferences'));
|
||||
};
|
||||
|
||||
const addCaCertificate = (e) => {
|
||||
formik.setFieldValue('customCaCertificate.filePath', e.target.files[0]?.path);
|
||||
};
|
||||
|
||||
const deleteCaCertificate = () => {
|
||||
formik.setFieldValue('customCaCertificate.filePath', null);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="flex items-center mt-2">
|
||||
<label className="block font-medium mr-2 select-none" style={{ minWidth: 200 }} htmlFor="sslVerification">
|
||||
SSL/TLS Certificate Verification
|
||||
</label>
|
||||
<input
|
||||
id="ssl-cert-verification"
|
||||
id="sslVerification"
|
||||
type="checkbox"
|
||||
name="sslVerification"
|
||||
checked={formik.values.sslVerification}
|
||||
onChange={formik.handleChange}
|
||||
className="mousetrap mr-0"
|
||||
/>
|
||||
<label className="block ml-2 select-none" htmlFor="sslVerification">
|
||||
SSL/TLS Certificate Verification
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="customCaCertificateEnabled"
|
||||
type="checkbox"
|
||||
name="customCaCertificate.enabled"
|
||||
checked={formik.values.customCaCertificate.enabled}
|
||||
onChange={formik.handleChange}
|
||||
className="mousetrap mr-0"
|
||||
/>
|
||||
<label className="block ml-2 select-none" htmlFor="customCaCertificateEnabled">
|
||||
Use custom CA Certificate
|
||||
</label>
|
||||
</div>
|
||||
{formik.values.customCaCertificate.filePath ? (
|
||||
<div
|
||||
className={`flex items-center mt-2 pl-6 ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
|
||||
>
|
||||
<span className="flex items-center border px-2 rounded-md">
|
||||
{path.basename(slash(formik.values.customCaCertificate.filePath))}
|
||||
<button
|
||||
type="button"
|
||||
tabIndex="-1"
|
||||
className="pl-1"
|
||||
disabled={formik.values.customCaCertificate.enabled ? false : true}
|
||||
onClick={deleteCaCertificate}
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={14} />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`flex items-center mt-2 pl-6 ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex="-1"
|
||||
className="flex items-center border px-2 rounded-md"
|
||||
disabled={formik.values.customCaCertificate.enabled ? false : true}
|
||||
onClick={() => inputFileCaCertificateRef.current.click()}
|
||||
>
|
||||
select file
|
||||
<input
|
||||
id="caCertFilePath"
|
||||
type="file"
|
||||
name="customCaCertificate.filePath"
|
||||
className="hidden"
|
||||
ref={inputFileCaCertificateRef}
|
||||
disabled={formik.values.customCaCertificate.enabled ? false : true}
|
||||
onChange={addCaCertificate}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="storeCookies"
|
||||
type="checkbox"
|
||||
name="storeCookies"
|
||||
checked={formik.values.storeCookies}
|
||||
onChange={formik.handleChange}
|
||||
className="mousetrap mr-0"
|
||||
/>
|
||||
<label className="block ml-2 select-none" htmlFor="storeCookies">
|
||||
Store Cookies automatically
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
id="sendCookies"
|
||||
type="checkbox"
|
||||
name="sendCookies"
|
||||
checked={formik.values.sendCookies}
|
||||
onChange={formik.handleChange}
|
||||
className="mousetrap mr-0"
|
||||
/>
|
||||
<label className="block ml-2 select-none" htmlFor="sendCookies">
|
||||
Send Cookies automatically
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex flex-col mt-6">
|
||||
<label className="block font-medium select-none" htmlFor="timeout">
|
||||
<label className="block select-none" htmlFor="timeout">
|
||||
Request Timeout (in ms)
|
||||
</label>
|
||||
<input
|
||||
|
||||
@@ -127,7 +127,7 @@ const ProxySettings = ({ close }) => {
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
http
|
||||
HTTP
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
@@ -138,7 +138,7 @@ const ProxySettings = ({ close }) => {
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
https
|
||||
HTTPS
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
@@ -149,7 +149,7 @@ const ProxySettings = ({ close }) => {
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
socks4
|
||||
SOCKS4
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
@@ -160,7 +160,7 @@ const ProxySettings = ({ close }) => {
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
socks5
|
||||
SOCKS5
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ const Theme = () => {
|
||||
theme: storedTheme
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
theme: Yup.string().oneOf(['light', 'dark']).required('theme is required')
|
||||
theme: Yup.string().oneOf(['light', 'dark', 'system']).required('theme is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
setStoredTheme(values.theme);
|
||||
@@ -55,6 +55,22 @@ const Theme = () => {
|
||||
<label htmlFor="dark-theme" className="ml-1 cursor-pointer select-none">
|
||||
Dark
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="system-theme"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="theme"
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
formik.handleSubmit();
|
||||
}}
|
||||
value="system"
|
||||
checked={formik.values.theme === 'system'}
|
||||
/>
|
||||
<label htmlFor="system-theme" className="ml-1 cursor-pointer select-none">
|
||||
System
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -9,7 +9,7 @@ import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { envVariableNameRegex } from 'utils/common/regex';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
|
||||
const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -33,14 +33,9 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
case 'name': {
|
||||
const value = e.target.value;
|
||||
|
||||
if (/^(?!\d).*$/.test(value) === false) {
|
||||
toast.error('Variable names must not start with a number!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (envVariableNameRegex.test(value) === false) {
|
||||
if (variableNameRegex.test(value) === false) {
|
||||
toast.error(
|
||||
'Variable contains invalid character! Variables must only contain alpha-numeric characters, "-" and "_".'
|
||||
'Variable contains invalid characters! Variables must only contain alpha-numeric characters, "-", "_", "."'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,46 @@
|
||||
import React from 'react';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ConfirmRequestClose = ({ onCancel, onCloseWithoutSave, onSaveAndClose }) => {
|
||||
const _handleCancel = ({ type }) => {
|
||||
if (type === 'button') {
|
||||
return onCloseWithoutSave();
|
||||
}
|
||||
|
||||
return onCancel();
|
||||
};
|
||||
|
||||
const ConfirmRequestClose = ({ item, onCancel, onCloseWithoutSave, onSaveAndClose }) => {
|
||||
return (
|
||||
<Modal
|
||||
size="sm"
|
||||
size="md"
|
||||
title="Unsaved changes"
|
||||
confirmText="Save and Close"
|
||||
cancelText="Close without saving"
|
||||
handleConfirm={onSaveAndClose}
|
||||
handleCancel={_handleCancel}
|
||||
disableEscapeKey={true}
|
||||
disableCloseOnOutsideClick={true}
|
||||
closeModalFadeTimeout={150}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
hideFooter={true}
|
||||
>
|
||||
<div className="font-normal">You have unsaved changes in you request.</div>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
You have unsaved changes in request <span className="font-semibold">{item.name}</span>.
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onCloseWithoutSave}>
|
||||
Don't Save
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={onSaveAndClose}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,15 +3,15 @@ import get from 'lodash/get';
|
||||
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import RequestTabNotFound from './RequestTabNotFound';
|
||||
import ConfirmRequestClose from './ConfirmRequestClose';
|
||||
import SpecialTab from './SpecialTab';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import darkTheme from 'themes/dark';
|
||||
import lightTheme from 'themes/light';
|
||||
import { findItemInCollection } from 'utils/collections';
|
||||
import ConfirmRequestClose from './ConfirmRequestClose';
|
||||
import RequestTabNotFound from './RequestTabNotFound';
|
||||
import SpecialTab from './SpecialTab';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const RequestTab = ({ tab, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -92,6 +92,7 @@ const RequestTab = ({ tab, collection }) => {
|
||||
<StyledWrapper className="flex items-center justify-between tab-container px-1">
|
||||
{showConfirmClose && (
|
||||
<ConfirmRequestClose
|
||||
item={item}
|
||||
onCancel={() => setShowConfirmClose(false)}
|
||||
onCloseWithoutSave={() => {
|
||||
dispatch(
|
||||
@@ -136,6 +137,8 @@ const RequestTab = ({ tab, collection }) => {
|
||||
onClick={(e) => {
|
||||
if (!item.draft) return handleCloseClick(e);
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowConfirmClose(true);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { IconFilter } from '@tabler/icons';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Tooltip as ReactTooltip } from 'react-tooltip';
|
||||
|
||||
const QueryResultFilter = ({ onChange, mode }) => {
|
||||
const tooltipText = useMemo(() => {
|
||||
if (mode.includes('json')) {
|
||||
return 'Filter with JSONPath';
|
||||
}
|
||||
|
||||
if (mode.includes('xml')) {
|
||||
return 'Filter with XPath';
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [mode]);
|
||||
|
||||
return (
|
||||
<div className={'response-filter relative'}>
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center">
|
||||
<div className="text-gray-500 sm:text-sm" id="request-filter-icon">
|
||||
<IconFilter size={16} strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tooltipText && <ReactTooltip anchorId={'request-filter-icon'} html={tooltipText} />}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
name="response-filter"
|
||||
id="response-filter"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="block w-full pl-10 py-1 sm:text-sm"
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryResultFilter;
|
||||
@@ -3,7 +3,7 @@ import styled from 'styled-components';
|
||||
const StyledWrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
grid-template-rows: 1.25rem calc(100% - 1.25rem);
|
||||
grid-template-rows: ${(props) => (props.queryFilterEnabled ? '1.25rem 1fr 2.25rem' : '1.25rem 1fr')};
|
||||
|
||||
/* This is a hack to force Codemirror to use all available space */
|
||||
> div {
|
||||
@@ -40,6 +40,22 @@ const StyledWrapper = styled.div`
|
||||
.muted {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.response-filter {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
|
||||
input {
|
||||
border: ${(props) => props.theme.sidebar.search.border};
|
||||
border-radius: 2px;
|
||||
background-color: ${(props) => props.theme.sidebar.search.bg};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { debounce } from 'lodash';
|
||||
import QueryResultFilter from './QueryResultFilter';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
|
||||
@@ -10,12 +13,20 @@ import { useMemo } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
|
||||
const formatResponse = (data, mode) => {
|
||||
if (!data) {
|
||||
const formatResponse = (data, mode, filter) => {
|
||||
if (data === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (mode.includes('json')) {
|
||||
if (filter) {
|
||||
try {
|
||||
data = JSONPath({ path: filter, json: data });
|
||||
} catch (e) {
|
||||
console.warn('Could not filter with JSONPath.', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
return safeStringifyJSON(data, true);
|
||||
}
|
||||
|
||||
@@ -28,7 +39,7 @@ const formatResponse = (data, mode) => {
|
||||
return safeStringifyJSON(parsed, true);
|
||||
}
|
||||
|
||||
if (['text', 'html'].includes(mode) || typeof data === 'string') {
|
||||
if (typeof data === 'string') {
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -37,10 +48,15 @@ const formatResponse = (data, mode) => {
|
||||
|
||||
const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEventListener, headers, error }) => {
|
||||
const contentType = getContentType(headers);
|
||||
const mode = getCodeMirrorModeBasedOnContentType(contentType);
|
||||
const formattedData = formatResponse(data, mode);
|
||||
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
|
||||
const [filter, setFilter] = useState(null);
|
||||
const formattedData = formatResponse(data, mode, filter);
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const debouncedResultFilterOnChange = debounce((e) => {
|
||||
setFilter(e.target.value);
|
||||
}, 250);
|
||||
|
||||
const allowedPreviewModes = useMemo(() => {
|
||||
// Always show raw
|
||||
const allowedPreviewModes = ['raw'];
|
||||
@@ -81,8 +97,14 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
));
|
||||
}, [allowedPreviewModes, previewTab]);
|
||||
|
||||
const queryFilterEnabled = useMemo(() => mode.includes('json'), [mode]);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full h-full" style={{ maxWidth: width }}>
|
||||
<StyledWrapper
|
||||
className="w-full h-full relative"
|
||||
style={{ maxWidth: width }}
|
||||
queryFilterEnabled={queryFilterEnabled}
|
||||
>
|
||||
<div className="flex justify-end gap-2 text-xs" role="tablist">
|
||||
{tabs}
|
||||
</div>
|
||||
@@ -98,19 +120,22 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<QueryResultPreview
|
||||
previewTab={previewTab}
|
||||
data={data}
|
||||
dataBuffer={dataBuffer}
|
||||
formattedData={formattedData}
|
||||
item={item}
|
||||
contentType={contentType}
|
||||
mode={mode}
|
||||
collection={collection}
|
||||
allowedPreviewModes={allowedPreviewModes}
|
||||
disableRunEventListener={disableRunEventListener}
|
||||
storedTheme={storedTheme}
|
||||
/>
|
||||
<>
|
||||
<QueryResultPreview
|
||||
previewTab={previewTab}
|
||||
data={data}
|
||||
dataBuffer={dataBuffer}
|
||||
formattedData={formattedData}
|
||||
item={item}
|
||||
contentType={contentType}
|
||||
mode={mode}
|
||||
collection={collection}
|
||||
allowedPreviewModes={allowedPreviewModes}
|
||||
disableRunEventListener={disableRunEventListener}
|
||||
storedTheme={storedTheme}
|
||||
/>
|
||||
{queryFilterEnabled && <QueryResultFilter onChange={debouncedResultFilterOnChange} mode={mode} />}
|
||||
</>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
color: ${(props) => props.theme.requestTabPanel.responseStatus};
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { IconEraser } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { responseCleared } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const ResponseClear = ({ collection, item }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const clearResponse = () =>
|
||||
dispatch(
|
||||
responseCleared({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
response: null
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button onClick={clearResponse} title="Clear response">
|
||||
<IconEraser size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default ResponseClear;
|
||||
@@ -10,7 +10,6 @@ const ResponseSave = ({ item }) => {
|
||||
|
||||
const saveResponseToFile = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log(item);
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-response-to-file', response, item.requestSent.url)
|
||||
.then(resolve)
|
||||
@@ -22,7 +21,7 @@ const ResponseSave = ({ item }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="ml-4 flex items-center">
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button onClick={saveResponseToFile} disabled={!response.dataBuffer} title="Save response to file">
|
||||
<IconDownload size={16} strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
@@ -15,6 +15,7 @@ import TestResults from './TestResults';
|
||||
import TestResultsLabel from './TestResultsLabel';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ResponseSave from 'src/components/ResponsePane/ResponseSave';
|
||||
import ResponseClear from 'src/components/ResponsePane/ResponseClear';
|
||||
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -104,6 +105,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
{response.headers?.length > 0 && <sup className="ml-1 font-medium">{response.headers.length}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
||||
Timeline
|
||||
@@ -113,6 +115,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
</div>
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<ResponseSave item={item} />
|
||||
<StatusCode status={response.status} />
|
||||
<ResponseTime duration={response.duration} />
|
||||
|
||||
@@ -15,9 +15,9 @@ import StyledWrapper from './StyledWrapper';
|
||||
const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const [selectedTab, setSelectedTab] = useState('response');
|
||||
|
||||
const { requestSent, responseReceived, testResults } = item;
|
||||
const { requestSent, responseReceived, testResults, assertionResults } = item;
|
||||
|
||||
const headers = get(item, 'responseReceived.headers', {});
|
||||
const headers = get(item, 'responseReceived.headers', []);
|
||||
const status = get(item, 'responseReceived.status', 0);
|
||||
const size = get(item, 'responseReceived.size', 0);
|
||||
const duration = get(item, 'responseReceived.duration', 0);
|
||||
@@ -47,7 +47,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
return <Timeline request={requestSent} response={responseReceived} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <TestResults results={testResults} />;
|
||||
return <TestResults results={testResults} assertionResults={assertionResults} />;
|
||||
}
|
||||
|
||||
default: {
|
||||
@@ -70,12 +70,13 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
{headers?.length > 0 && <sup className="ml-1 font-medium">{headers.length}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('timeline')} role="tab" onClick={() => selectTab('timeline')}>
|
||||
Timeline
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
<TestResultsLabel results={testResults} />
|
||||
<TestResultsLabel results={testResults} assertionResults={assertionResults} />
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<StatusCode status={status} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import path from 'path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { get, each, cloneDeep } from 'lodash';
|
||||
import { get, cloneDeep } from 'lodash';
|
||||
import { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { resetCollectionRunner } from 'providers/ReduxStore/slices/collections';
|
||||
import { findItemInCollection, getTotalRequestCountInCollection } from 'utils/collections';
|
||||
@@ -31,35 +31,39 @@ export default function RunnerResults({ collection }) {
|
||||
}, [collection, setSelectedItem]);
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const items = cloneDeep(get(collection, 'runnerResult.items', []));
|
||||
const runnerInfo = get(collection, 'runnerResult.info', {});
|
||||
each(items, (item) => {
|
||||
const info = findItemInCollection(collectionCopy, item.uid);
|
||||
|
||||
item.name = info.name;
|
||||
item.type = info.type;
|
||||
item.filename = info.filename;
|
||||
item.pathname = info.pathname;
|
||||
item.relativePath = getRelativePath(collection.pathname, info.pathname);
|
||||
|
||||
if (item.status !== 'error') {
|
||||
if (item.testResults) {
|
||||
const failed = item.testResults.filter((result) => result.status === 'fail');
|
||||
|
||||
item.testStatus = failed.length ? 'fail' : 'pass';
|
||||
} else {
|
||||
item.testStatus = 'pass';
|
||||
const items = cloneDeep(get(collection, 'runnerResult.items', []))
|
||||
.map((item) => {
|
||||
const info = findItemInCollection(collectionCopy, item.uid);
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
const newItem = {
|
||||
...item,
|
||||
name: info.name,
|
||||
type: info.type,
|
||||
filename: info.filename,
|
||||
pathname: info.pathname,
|
||||
relativePath: getRelativePath(collection.pathname, info.pathname)
|
||||
};
|
||||
if (newItem.status !== 'error') {
|
||||
if (newItem.testResults) {
|
||||
const failed = newItem.testResults.filter((result) => result.status === 'fail');
|
||||
newItem.testStatus = failed.length ? 'fail' : 'pass';
|
||||
} else {
|
||||
newItem.testStatus = 'pass';
|
||||
}
|
||||
|
||||
if (item.assertionResults) {
|
||||
const failed = item.assertionResults.filter((result) => result.status === 'fail');
|
||||
|
||||
item.assertionStatus = failed.length ? 'fail' : 'pass';
|
||||
} else {
|
||||
item.assertionStatus = 'pass';
|
||||
if (newItem.assertionResults) {
|
||||
const failed = newItem.assertionResults.filter((result) => result.status === 'fail');
|
||||
newItem.assertionStatus = failed.length ? 'fail' : 'pass';
|
||||
} else {
|
||||
newItem.assertionStatus = 'pass';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return newItem;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const runCollection = () => {
|
||||
dispatch(runCollectionFolder(collection.uid, null, true));
|
||||
@@ -109,12 +113,12 @@ export default function RunnerResults({ collection }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4">
|
||||
<StyledWrapper className="px-4 pb-4 flex flex-grow flex-col relative">
|
||||
<div className="font-medium mt-6 mb-4 title flex items-center">
|
||||
Runner
|
||||
<IconRun size={20} strokeWidth={1.5} className="ml-2" />
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="flex flex-1">
|
||||
<div className="flex flex-col flex-1">
|
||||
<div className="py-2 font-medium test-summary">
|
||||
Total Requests: {items.length}, Passed: {passedRequests.length}, Failed: {failedRequests.length}
|
||||
@@ -168,26 +172,24 @@ export default function RunnerResults({ collection }) {
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.assertionResults
|
||||
? item.assertionResults.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
{item.assertionResults?.map((result) => (
|
||||
<li key={result.uid}>
|
||||
{result.status === 'pass' ? (
|
||||
<span className="test-success flex items-center">
|
||||
<IconCheck size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span className="test-failure flex items-center">
|
||||
<IconX size={18} strokeWidth={2} className="mr-2" />
|
||||
{result.lhsExpr}: {result.rhsExpr}
|
||||
</span>
|
||||
<span className="error-message pl-8 text-xs">{result.error}</span>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,155 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { cloneCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const CloneCollection = ({ onClose, collection }) => {
|
||||
const inputRef = useRef();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
collectionName: '',
|
||||
collectionFolderName: '',
|
||||
collectionLocation: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
collectionName: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('collection name is required'),
|
||||
collectionFolderName: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.matches(/^[\w\-. ]+$/, 'Folder name contains invalid characters')
|
||||
.required('folder name is required'),
|
||||
collectionLocation: Yup.string().min(1, 'location is required').required('location is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(
|
||||
cloneCollection(
|
||||
values.collectionName,
|
||||
values.collectionFolderName,
|
||||
values.collectionLocation,
|
||||
collection.pathname
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('Collection created');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while creating the collection'));
|
||||
}
|
||||
});
|
||||
|
||||
const browse = () => {
|
||||
dispatch(browseDirectory())
|
||||
.then((dirPath) => {
|
||||
// When the user closes the diolog without selecting anything dirPath will be false
|
||||
if (typeof dirPath === 'string') {
|
||||
formik.setFieldValue('collectionLocation', dirPath);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
formik.setFieldValue('collectionLocation', '');
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => formik.handleSubmit();
|
||||
|
||||
return (
|
||||
<Modal size="sm" title="Clone Collection" confirmText="Create" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="collection-name" className="flex items-center font-semibold">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
id="collection-name"
|
||||
type="text"
|
||||
name="collectionName"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
if (formik.values.collectionName === formik.values.collectionFolderName) {
|
||||
formik.setFieldValue('collectionFolderName', e.target.value);
|
||||
}
|
||||
}}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionName || ''}
|
||||
/>
|
||||
{formik.touched.collectionName && formik.errors.collectionName ? (
|
||||
<div className="text-red-500">{formik.errors.collectionName}</div>
|
||||
) : null}
|
||||
|
||||
<label htmlFor="collection-location" className="block font-semibold mt-3">
|
||||
Location
|
||||
</label>
|
||||
<input
|
||||
id="collection-location"
|
||||
type="text"
|
||||
name="collectionLocation"
|
||||
readOnly={true}
|
||||
className="block textbox mt-2 w-full cursor-pointer"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionLocation || ''}
|
||||
onClick={browse}
|
||||
/>
|
||||
{formik.touched.collectionLocation && formik.errors.collectionLocation ? (
|
||||
<div className="text-red-500">{formik.errors.collectionLocation}</div>
|
||||
) : null}
|
||||
<div className="mt-1">
|
||||
<span className="text-link cursor-pointer hover:underline" onClick={browse}>
|
||||
Browse
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label htmlFor="collection-folder-name" className="flex items-center mt-3">
|
||||
<span className="font-semibold">Folder Name</span>
|
||||
<Tooltip
|
||||
text="This folder will be created under the selected location"
|
||||
tooltipId="collection-folder-name-tooltip"
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
id="collection-folder-name"
|
||||
type="text"
|
||||
name="collectionFolderName"
|
||||
className="block textbox mt-2 w-full"
|
||||
onChange={formik.handleChange}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.collectionFolderName || ''}
|
||||
/>
|
||||
{formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (
|
||||
<div className="text-red-500">{formik.errors.collectionFolderName}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloneCollection;
|
||||
@@ -0,0 +1,20 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
|
||||
.copy-to-clipboard {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -2,14 +2,26 @@ import CodeEditor from 'components/CodeEditor/index';
|
||||
import get from 'lodash/get';
|
||||
import { HTTPSnippet } from 'httpsnippet';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { buildHarRequest } from 'utils/codegenerator/har';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconCopy } from '@tabler/icons';
|
||||
import { findCollectionByItemUid } from '../../../../../../../utils/collections/index';
|
||||
|
||||
const CodeView = ({ language, item }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const { target, client, language: lang } = language;
|
||||
const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
|
||||
const requestHeaders = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');
|
||||
const collection = findCollectionByItemUid(
|
||||
useSelector((state) => state.collections.collections),
|
||||
item.uid
|
||||
);
|
||||
|
||||
const headers = [...(collection?.root?.request?.headers || []), ...(requestHeaders || [])];
|
||||
|
||||
let snippet = '';
|
||||
|
||||
try {
|
||||
@@ -20,13 +32,24 @@ const CodeView = ({ language, item }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
readOnly
|
||||
value={snippet}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
theme={storedTheme}
|
||||
mode={lang}
|
||||
/>
|
||||
<>
|
||||
<StyledWrapper>
|
||||
<CopyToClipboard
|
||||
className="copy-to-clipboard"
|
||||
text={snippet}
|
||||
onCopy={() => toast.success('Copied to clipboard!')}
|
||||
>
|
||||
<IconCopy size={25} strokeWidth={1.5} />
|
||||
</CopyToClipboard>
|
||||
<CodeEditor
|
||||
readOnly
|
||||
value={snippet}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
theme={storedTheme}
|
||||
mode={lang}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useSelector, useDispatch } from 'react-redux';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { collectionFolderClicked } from 'providers/ReduxStore/slices/collections';
|
||||
import { moveItem } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import NewRequest from 'components/Sidebar/NewRequest';
|
||||
import NewFolder from 'components/Sidebar/NewFolder';
|
||||
@@ -23,6 +24,7 @@ import { getDefaultRequestPaneTab } from 'utils/collections';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import NetworkError from 'components/ResponsePane/NetworkError/index';
|
||||
|
||||
const CollectionItem = ({ item, collection, searchText }) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
@@ -95,6 +97,14 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendRequest(item, collection.uid)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleClick = (event) => {
|
||||
//scroll to the active tab
|
||||
setTimeout(scrollToTheActiveTab, 50);
|
||||
@@ -296,15 +306,25 @@ const CollectionItem = ({ item, collection, searchText }) => {
|
||||
>
|
||||
Rename
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setCloneItemModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Clone
|
||||
</div>
|
||||
{!isFolder && (
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setCloneItemModalOpen(true);
|
||||
handleClick(null);
|
||||
handleRun();
|
||||
}}
|
||||
>
|
||||
Clone
|
||||
Run
|
||||
</div>
|
||||
)}
|
||||
{!isFolder && item.type === 'http-request' && (
|
||||
|
||||
@@ -20,11 +20,13 @@ import exportCollection from 'utils/collections/export';
|
||||
|
||||
import RenameCollection from './RenameCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import CloneCollection from './CloneCollection/index';
|
||||
|
||||
const Collection = ({ collection, searchText }) => {
|
||||
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
|
||||
const [showNewRequestModal, setShowNewRequestModal] = useState(false);
|
||||
const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);
|
||||
const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false);
|
||||
const [showExportCollectionModal, setShowExportCollectionModal] = useState(false);
|
||||
const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);
|
||||
const [collectionIsCollapsed, setCollectionIsCollapsed] = useState(collection.collapsed);
|
||||
@@ -133,6 +135,9 @@ const Collection = ({ collection, searchText }) => {
|
||||
{showExportCollectionModal && (
|
||||
<ExportCollection collection={collection} onClose={() => setShowExportCollectionModal(false)} />
|
||||
)}
|
||||
{showCloneCollectionModalOpen && (
|
||||
<CloneCollection collection={collection} onClose={() => setShowCloneCollectionModalOpen(false)} />
|
||||
)}
|
||||
<div className="flex py-1 collection-name items-center" ref={drop}>
|
||||
<div
|
||||
className="flex flex-grow items-center overflow-hidden"
|
||||
@@ -169,6 +174,15 @@ const Collection = ({ collection, searchText }) => {
|
||||
>
|
||||
New Folder
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
menuDropdownTippyRef.current.hide();
|
||||
setShowCloneCollectionModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Clone
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.text};
|
||||
.collection-options {
|
||||
svg {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.label {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
185
packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js
Normal file
185
packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js
Normal file
@@ -0,0 +1,185 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { PostHog } from 'posthog-node';
|
||||
import { uuid } from 'utils/common';
|
||||
import { IconHeart, IconUser, IconUsers } from '@tabler/icons';
|
||||
import platformLib from 'platform';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
|
||||
let posthogClient = null;
|
||||
const posthogApiKey = 'phc_7gtqSrrdZRohiozPMLIacjzgHbUlhalW1Bu16uYijMR';
|
||||
const getPosthogClient = () => {
|
||||
if (posthogClient) {
|
||||
return posthogClient;
|
||||
}
|
||||
|
||||
posthogClient = new PostHog(posthogApiKey);
|
||||
return posthogClient;
|
||||
};
|
||||
const getAnonymousTrackingId = () => {
|
||||
let id = localStorage.getItem('bruno.anonymousTrackingId');
|
||||
|
||||
if (!id || !id.length || id.length !== 21) {
|
||||
id = uuid();
|
||||
localStorage.setItem('bruno.anonymousTrackingId', id);
|
||||
}
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
const HeartIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
className="flex-shrink-0 w-5 h-4 text-yellow-600"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path fillRule="evenodd" d="M8 1.314C12.438-3.248 23.534 4.735 8 15-7.534 4.736 3.562-3.248 8 1.314z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const CheckIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
className="flex-shrink-0 w-5 h-5 text-green-500"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const GoldenEdition = ({ onClose }) => {
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const anonymousId = getAnonymousTrackingId();
|
||||
const client = getPosthogClient();
|
||||
client.capture({
|
||||
distinctId: anonymousId,
|
||||
event: 'golden-edition-modal-opened',
|
||||
properties: {
|
||||
os: platformLib.os.family
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const goldenEditionBuyClick = () => {
|
||||
const anonymousId = getAnonymousTrackingId();
|
||||
const client = getPosthogClient();
|
||||
client.capture({
|
||||
distinctId: anonymousId,
|
||||
event: 'golden-edition-buy-clicked',
|
||||
properties: {
|
||||
os: platformLib.os.family
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const goldenEditon = [
|
||||
'Inbuilt Bru File Explorer',
|
||||
'Visual Git (Like Gitlens for Vscode)',
|
||||
'GRPC, Websocket, SocketIO, MQTT',
|
||||
'Intergration with Secret Managers',
|
||||
'Load Data from File for Collection Run',
|
||||
'Developer Tools',
|
||||
'OpenAPI Designer',
|
||||
'Performance/Load Testing',
|
||||
'Inbuilt Terminal',
|
||||
'Custom Themes'
|
||||
];
|
||||
|
||||
const [pricingOption, setPricingOption] = useState('individuals');
|
||||
|
||||
const handlePricingOptionChange = (option) => {
|
||||
setPricingOption(option);
|
||||
};
|
||||
|
||||
const themeBasedContainerClassNames = storedTheme === 'light' ? 'text-gray-900' : 'text-white';
|
||||
const themeBasedTabContainerClassNames = storedTheme === 'light' ? 'bg-gray-200' : 'bg-gray-800';
|
||||
const themeBasedActiveTabClassNames =
|
||||
storedTheme === 'light' ? 'bg-white text-gray-900 font-medium' : 'bg-gray-700 text-white font-medium';
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={'Golden Edition'} handleCancel={onClose} hideFooter={true}>
|
||||
<div className={`flex flex-col w-full ${themeBasedContainerClassNames}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">Golden Edition</h3>
|
||||
<a
|
||||
onClick={() => {
|
||||
goldenEditionBuyClick();
|
||||
window.open('https://www.usebruno.com/pricing', '_blank');
|
||||
}}
|
||||
target="_blank"
|
||||
className="flex text-white bg-yellow-600 hover:bg-yellow-700 font-medium rounded-lg text-sm px-4 py-2 text-center cursor-pointer"
|
||||
>
|
||||
<IconHeart size={18} strokeWidth={1.5} />{' '}
|
||||
<span className="ml-2">{pricingOption === 'individuals' ? 'Buy' : 'Subscribe'}</span>
|
||||
</a>
|
||||
</div>
|
||||
{pricingOption === 'individuals' ? (
|
||||
<div>
|
||||
<div className="my-4">
|
||||
<span className="text-3xl font-extrabold">$19</span>
|
||||
</div>
|
||||
<p className="bg-yellow-200 text-black rounded-md px-2 py-1 mb-2 inline-flex text-sm">One Time Payment</p>
|
||||
<p className="text-sm">perpetual license for 2 devices, with 2 years of updates</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="my-4">
|
||||
<span className="text-3xl font-extrabold">$2</span>
|
||||
</div>
|
||||
<p>/user/month</p>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`flex items-center justify-between my-8 w-40 rounded-full p-1 ${themeBasedTabContainerClassNames}`}
|
||||
style={{ width: '24rem' }}
|
||||
>
|
||||
<div
|
||||
className={`cursor-pointer w-1/2 h-8 flex items-center justify-center rounded-full ${
|
||||
pricingOption === 'individuals' ? themeBasedActiveTabClassNames : 'text-gray-500'
|
||||
}`}
|
||||
onClick={() => handlePricingOptionChange('individuals')}
|
||||
>
|
||||
<IconUser className="text-gray-500 mr-2 icon" size={16} strokeWidth={1.5} /> Individuals
|
||||
</div>
|
||||
<div
|
||||
className={`cursor-pointer w-1/2 h-8 flex items-center justify-center rounded-full ${
|
||||
pricingOption === 'organizations' ? themeBasedActiveTabClassNames : 'text-gray-500'
|
||||
}`}
|
||||
onClick={() => handlePricingOptionChange('organizations')}
|
||||
>
|
||||
<IconUsers className="text-gray-500 mr-2 icon" size={16} strokeWidth={1.5} /> Organizations
|
||||
</div>
|
||||
</div>
|
||||
<ul role="list" className="space-y-3 text-left">
|
||||
<li className="flex items-center space-x-3">
|
||||
<HeartIcon />
|
||||
<span>Support Bruno's Development</span>
|
||||
</li>
|
||||
{goldenEditon.map((item, index) => (
|
||||
<li className="flex items-center space-x-3" key={index}>
|
||||
<CheckIcon />
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoldenEdition;
|
||||
@@ -21,7 +21,6 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) =>
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
console.log('here');
|
||||
handleSubmit(values.collectionLocation);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,19 +4,21 @@ import StyledWrapper from './StyledWrapper';
|
||||
import GitHubButton from 'react-github-btn';
|
||||
import Preferences from 'components/Preferences';
|
||||
import Cookies from 'components/Cookies';
|
||||
import GoldenEdition from './GoldenEdition';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconSettings, IconCookie } from '@tabler/icons';
|
||||
import { IconSettings, IconCookie, IconHeart } from '@tabler/icons';
|
||||
import { updateLeftSidebarWidth, updateIsDragging, showPreferences } from 'providers/ReduxStore/slices/app';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const MIN_LEFT_SIDEBAR_WIDTH = 222;
|
||||
const MIN_LEFT_SIDEBAR_WIDTH = 221;
|
||||
const MAX_LEFT_SIDEBAR_WIDTH = 600;
|
||||
|
||||
const Sidebar = () => {
|
||||
const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);
|
||||
const preferencesOpen = useSelector((state) => state.app.showPreferences);
|
||||
const [goldenEditonOpen, setGoldenEditonOpen] = useState(false);
|
||||
|
||||
const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);
|
||||
const [cookiesOpen, setCookiesOpen] = useState(false);
|
||||
@@ -79,6 +81,7 @@ const Sidebar = () => {
|
||||
return (
|
||||
<StyledWrapper className="flex relative h-screen">
|
||||
<aside>
|
||||
{goldenEditonOpen && <GoldenEdition onClose={() => setGoldenEditonOpen(false)} />}
|
||||
<div className="flex flex-row h-screen w-full">
|
||||
{preferencesOpen && <Preferences onClose={() => dispatch(showPreferences(false))} />}
|
||||
{cookiesOpen && <Cookies onClose={() => setCookiesOpen(false)} />}
|
||||
@@ -103,6 +106,12 @@ const Sidebar = () => {
|
||||
className="mr-2 hover:text-gray-700"
|
||||
onClick={() => setCookiesOpen(true)}
|
||||
/>
|
||||
<IconHeart
|
||||
size={18}
|
||||
strokeWidth={1.5}
|
||||
className="mr-2 hover:text-gray-700"
|
||||
onClick={() => setGoldenEditonOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="pl-1" style={{ position: 'relative', top: '3px' }}>
|
||||
{/* This will get moved to home page */}
|
||||
@@ -115,7 +124,7 @@ const Sidebar = () => {
|
||||
Star
|
||||
</GitHubButton> */}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.2.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.6.1</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -122,7 +122,7 @@ class SingleLineEditor extends Component {
|
||||
}
|
||||
});
|
||||
}
|
||||
this.editor.setValue(this.props.value || '');
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
this.editor.on('change', this._onEdit);
|
||||
this.addOverlay();
|
||||
}
|
||||
@@ -151,8 +151,8 @@ class SingleLineEditor extends Component {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value || '');
|
||||
this.cachedValue = String(this.props.value);
|
||||
this.editor.setValue(String(this.props.value) || '');
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,18 @@ const GlobalStyle = createGlobalStyle`
|
||||
}
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
color: ${(props) => props.theme.button.danger.color};
|
||||
background: ${(props) => props.theme.button.danger.bg};
|
||||
border: solid 1px ${(props) => props.theme.button.danger.border};
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
color: ${(props) => props.theme.button.secondary.color};
|
||||
background: ${(props) => props.theme.button.secondary.bg};
|
||||
|
||||
@@ -25,7 +25,6 @@ if (!SERVER_RENDERED) {
|
||||
require('codemirror/addon/hint/javascript-hint');
|
||||
require('codemirror/addon/hint/show-hint');
|
||||
require('codemirror/addon/lint/lint');
|
||||
require('codemirror/addon/lint/javascript-lint');
|
||||
require('codemirror/addon/lint/json-lint');
|
||||
require('codemirror/addon/mode/overlay');
|
||||
require('codemirror/addon/scroll/simplescrollbars');
|
||||
@@ -41,6 +40,7 @@ if (!SERVER_RENDERED) {
|
||||
require('codemirror-graphql/mode');
|
||||
|
||||
require('utils/codemirror/brunoVarInfo');
|
||||
require('utils/codemirror/javascript-lint');
|
||||
}
|
||||
|
||||
export default function Main() {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import each from 'lodash/each';
|
||||
import filter from 'lodash/filter';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { findCollectionByUid, flattenItems, isItemARequest } from 'utils/collections';
|
||||
import { pluralizeWord } from 'utils/common';
|
||||
import { completeQuitFlow } from 'providers/ReduxStore/slices/app';
|
||||
import { saveMultipleRequests } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const SaveRequestsModal = ({ onClose }) => {
|
||||
const MAX_UNSAVED_REQUESTS_TO_SHOW = 5;
|
||||
const currentDrafts = [];
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const tabsByCollection = groupBy(tabs, (t) => t.collectionUid);
|
||||
Object.keys(tabsByCollection).forEach((collectionUid) => {
|
||||
const collection = findCollectionByUid(collections, collectionUid);
|
||||
if (collection) {
|
||||
const items = flattenItems(collection.items);
|
||||
const drafts = filter(items, (item) => isItemARequest(item) && item.draft);
|
||||
each(drafts, (draft) => {
|
||||
currentDrafts.push({
|
||||
...draft,
|
||||
collectionUid: collectionUid
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDrafts.length === 0) {
|
||||
return dispatch(completeQuitFlow());
|
||||
}
|
||||
}, [currentDrafts, dispatch]);
|
||||
|
||||
const closeWithoutSave = () => {
|
||||
dispatch(completeQuitFlow());
|
||||
onClose();
|
||||
};
|
||||
|
||||
const closeWithSave = () => {
|
||||
dispatch(saveMultipleRequests(currentDrafts))
|
||||
.then(() => dispatch(completeQuitFlow()))
|
||||
.then(() => onClose());
|
||||
};
|
||||
|
||||
if (!currentDrafts.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="md"
|
||||
title="Unsaved changes"
|
||||
confirmText="Save and Close"
|
||||
cancelText="Close without saving"
|
||||
handleCancel={onClose}
|
||||
disableEscapeKey={true}
|
||||
disableCloseOnOutsideClick={true}
|
||||
closeModalFadeTimeout={150}
|
||||
hideFooter={true}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
|
||||
</div>
|
||||
<p className="mt-4">
|
||||
Do you want to save the changes you made to the following{' '}
|
||||
<span className="font-medium">{currentDrafts.length}</span> {pluralizeWord('request', currentDrafts.length)}?
|
||||
</p>
|
||||
|
||||
<ul className="mt-4">
|
||||
{currentDrafts.slice(0, MAX_UNSAVED_REQUESTS_TO_SHOW).map((item) => {
|
||||
return (
|
||||
<li key={item.uid} className="mt-1 text-xs">
|
||||
{item.filename}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
{currentDrafts.length > MAX_UNSAVED_REQUESTS_TO_SHOW && (
|
||||
<p className="mt-1 text-xs">
|
||||
...{currentDrafts.length - MAX_UNSAVED_REQUESTS_TO_SHOW} additional{' '}
|
||||
{pluralizeWord('request', currentDrafts.length - MAX_UNSAVED_REQUESTS_TO_SHOW)} not shown
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={closeWithoutSave}>
|
||||
Don't Save
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={closeWithSave}>
|
||||
{currentDrafts.length > 1 ? 'Save All' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveRequestsModal;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SaveRequestsModal from './SaveRequestsModal';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
|
||||
const ConfirmAppClose = () => {
|
||||
const { ipcRenderer } = window;
|
||||
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectron()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clearListener = ipcRenderer.on('main:start-quit-flow', () => {
|
||||
setShowConfirmClose(true);
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearListener();
|
||||
};
|
||||
}, [isElectron, ipcRenderer, dispatch, setShowConfirmClose]);
|
||||
|
||||
if (!showConfirmClose) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SaveRequestsModal onClose={() => setShowConfirmClose(false)} />;
|
||||
};
|
||||
|
||||
export default ConfirmAppClose;
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import useTelemetry from './useTelemetry';
|
||||
import useIpcEvents from './useIpcEvents';
|
||||
import useCollectionNextAction from './useCollectionNextAction';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { refreshScreenWidth } from 'providers/ReduxStore/slices/app';
|
||||
import ConfirmAppClose from './ConfirmAppClose';
|
||||
import useIpcEvents from './useIpcEvents';
|
||||
import useTelemetry from './useTelemetry';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
export const AppContext = React.createContext();
|
||||
@@ -11,7 +11,6 @@ export const AppContext = React.createContext();
|
||||
export const AppProvider = (props) => {
|
||||
useTelemetry();
|
||||
useIpcEvents();
|
||||
useCollectionNextAction();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -31,7 +30,10 @@ export const AppProvider = (props) => {
|
||||
|
||||
return (
|
||||
<AppContext.Provider {...props} value="appProvider">
|
||||
<StyledWrapper>{props.children}</StyledWrapper>
|
||||
<StyledWrapper>
|
||||
<ConfirmAppClose />
|
||||
{props.children}
|
||||
</StyledWrapper>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import each from 'lodash/each';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { getDefaultRequestPaneTab, findItemInCollectionByPathname } from 'utils/collections/index';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import { updateNextAction } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
const useCollectionNextAction = () => {
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
each(collections, (collection) => {
|
||||
if (collection.nextAction && collection.nextAction.type === 'OPEN_REQUEST') {
|
||||
const item = findItemInCollectionByPathname(collection, get(collection, 'nextAction.payload.pathname'));
|
||||
|
||||
if (item) {
|
||||
dispatch(updateNextAction(collection.uid, null));
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item.type)
|
||||
})
|
||||
);
|
||||
dispatch(hideHomePage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [collections, each, dispatch, updateNextAction, hideHomePage, addTab]);
|
||||
};
|
||||
|
||||
export default useCollectionNextAction;
|
||||
@@ -1,22 +1,22 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { showPreferences, updateCookies, updatePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import {
|
||||
brunoConfigUpdateEvent,
|
||||
collectionAddDirectoryEvent,
|
||||
collectionAddFileEvent,
|
||||
collectionChangeFileEvent,
|
||||
collectionUnlinkFileEvent,
|
||||
collectionRenamedEvent,
|
||||
collectionUnlinkDirectoryEvent,
|
||||
collectionUnlinkEnvFileEvent,
|
||||
scriptEnvironmentUpdateEvent,
|
||||
collectionUnlinkFileEvent,
|
||||
processEnvUpdateEvent,
|
||||
collectionRenamedEvent,
|
||||
runRequestEvent,
|
||||
runFolderEvent,
|
||||
brunoConfigUpdateEvent
|
||||
runRequestEvent,
|
||||
scriptEnvironmentUpdateEvent
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { showPreferences, updatePreferences, updateCookies } from 'providers/ReduxStore/slices/app';
|
||||
import { collectionAddEnvFileEvent, openCollectionEvent } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import { openCollectionEvent, collectionAddEnvFileEvent } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
|
||||
const useIpcEvents = () => {
|
||||
@@ -80,6 +80,7 @@ const useIpcEvents = () => {
|
||||
};
|
||||
|
||||
ipcRenderer.invoke('renderer:ready');
|
||||
|
||||
const removeCollectionTreeUpdateListener = ipcRenderer.on('main:collection-tree-updated', _collectionTreeUpdated);
|
||||
|
||||
const removeOpenCollectionListener = ipcRenderer.on('main:collection-opened', (pathname, uid, brunoConfig) => {
|
||||
@@ -127,7 +128,7 @@ const useIpcEvents = () => {
|
||||
dispatch(brunoConfigUpdateEvent(val))
|
||||
);
|
||||
|
||||
const showPreferencesListener = ipcRenderer.on('main:open-preferences', () => {
|
||||
const removeShowPreferencesListener = ipcRenderer.on('main:open-preferences', () => {
|
||||
dispatch(showPreferences(true));
|
||||
});
|
||||
|
||||
@@ -151,7 +152,7 @@ const useIpcEvents = () => {
|
||||
removeProcessEnvUpdatesListener();
|
||||
removeConsoleLogListener();
|
||||
removeConfigUpdatesListener();
|
||||
showPreferencesListener();
|
||||
removeShowPreferencesListener();
|
||||
removePreferencesUpdatesListener();
|
||||
removeCookieUpdateListener();
|
||||
};
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
/**
|
||||
* Telemetry in bruno is just an anonymous visit counter (triggered once per day).
|
||||
* The only details shared are:
|
||||
* - OS (ex: mac, windows, linux)
|
||||
* - Bruno Version (ex: 1.3.0)
|
||||
* We don't track usage analytics / micro-interactions / crash logs / anything else.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import getConfig from 'next/config';
|
||||
import { PostHog } from 'posthog-node';
|
||||
import platformLib from 'platform';
|
||||
import { uuid } from 'utils/common';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const posthogApiKey = 'phc_7gtqSrrdZRohiozPMLIacjzgHbUlhalW1Bu16uYijMR';
|
||||
@@ -17,11 +24,6 @@ const isDevEnv = () => {
|
||||
return publicRuntimeConfig.ENV === 'dev';
|
||||
};
|
||||
|
||||
// Todo support chrome and firefox extension
|
||||
const getPlatform = () => {
|
||||
return isElectron() ? 'electron' : 'web';
|
||||
};
|
||||
|
||||
const getPosthogClient = () => {
|
||||
if (posthogClient) {
|
||||
return posthogClient;
|
||||
@@ -52,14 +54,13 @@ const trackStart = () => {
|
||||
}
|
||||
|
||||
const trackingId = getAnonymousTrackingId();
|
||||
const platform = getPlatform();
|
||||
const client = getPosthogClient();
|
||||
client.capture({
|
||||
distinctId: trackingId,
|
||||
event: 'start',
|
||||
properties: {
|
||||
platform: platform,
|
||||
os: platformLib.os.family
|
||||
os: platformLib.os.family,
|
||||
version: '1.6.1'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import getConfig from 'next/config';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import tasksMiddleware from './middlewares/tasks/middleware';
|
||||
import debugMiddleware from './middlewares/debug/middleware';
|
||||
import appReducer from './slices/app';
|
||||
import collectionsReducer from './slices/collections';
|
||||
import tabsReducer from './slices/tabs';
|
||||
|
||||
const { publicRuntimeConfig } = getConfig();
|
||||
const isDevEnv = () => {
|
||||
return publicRuntimeConfig.ENV === 'dev';
|
||||
};
|
||||
|
||||
let middleware = [tasksMiddleware.middleware];
|
||||
if (isDevEnv()) {
|
||||
middleware = [...middleware, debugMiddleware.middleware];
|
||||
}
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
app: appReducer,
|
||||
collections: collectionsReducer,
|
||||
tabs: tabsReducer
|
||||
}
|
||||
},
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware)
|
||||
});
|
||||
|
||||
export default store;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createListenerMiddleware } from '@reduxjs/toolkit';
|
||||
|
||||
const debugMiddleware = createListenerMiddleware();
|
||||
|
||||
debugMiddleware.startListening({
|
||||
predicate: () => true, // it'll track every change
|
||||
effect: (action, listenerApi) => {
|
||||
console.debug('---redux action---');
|
||||
console.debug('action', action.type); // which action did it
|
||||
console.debug('action.payload', action.payload);
|
||||
console.debug(listenerApi.getState()); // the updated store
|
||||
}
|
||||
});
|
||||
|
||||
export default debugMiddleware;
|
||||
@@ -0,0 +1,51 @@
|
||||
import get from 'lodash/get';
|
||||
import each from 'lodash/each';
|
||||
import filter from 'lodash/filter';
|
||||
import { createListenerMiddleware } from '@reduxjs/toolkit';
|
||||
import { removeTaskFromQueue, hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { collectionAddFileEvent } from 'providers/ReduxStore/slices/collections';
|
||||
import { findCollectionByUid, findItemInCollectionByPathname, getDefaultRequestPaneTab } from 'utils/collections/index';
|
||||
import { taskTypes } from './utils';
|
||||
|
||||
const taskMiddleware = createListenerMiddleware();
|
||||
|
||||
/*
|
||||
* When a new request is created in the app, a task to open the request is added to the queue.
|
||||
* We wait for the File IO to complete, after which the "collectionAddFileEvent" gets dispatched.
|
||||
* This middleware listens for the event and checks if there is a task in the queue that matches
|
||||
* the collectionUid and itemPathname. If there is a match, we open the request and remove the task
|
||||
* from the queue.
|
||||
*/
|
||||
taskMiddleware.startListening({
|
||||
actionCreator: collectionAddFileEvent,
|
||||
effect: (action, listenerApi) => {
|
||||
const state = listenerApi.getState();
|
||||
const collectionUid = get(action, 'payload.file.meta.collectionUid');
|
||||
|
||||
const openRequestTasks = filter(state.app.taskQueue, { type: taskTypes.OPEN_REQUEST });
|
||||
each(openRequestTasks, (task) => {
|
||||
if (collectionUid === task.collectionUid) {
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
const item = findItemInCollectionByPathname(collection, task.itemPathname);
|
||||
if (item) {
|
||||
listenerApi.dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||
})
|
||||
);
|
||||
listenerApi.dispatch(hideHomePage());
|
||||
listenerApi.dispatch(
|
||||
removeTaskFromQueue({
|
||||
taskUid: task.uid
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default taskMiddleware;
|
||||
@@ -0,0 +1,3 @@
|
||||
export const taskTypes = {
|
||||
OPEN_REQUEST: 'OPEN_REQUEST'
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import filter from 'lodash/filter';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const initialState = {
|
||||
@@ -11,13 +12,18 @@ const initialState = {
|
||||
preferences: {
|
||||
request: {
|
||||
sslVerification: true,
|
||||
customCaCertificate: {
|
||||
enabled: false,
|
||||
filePath: null
|
||||
},
|
||||
timeout: 0
|
||||
},
|
||||
font: {
|
||||
codeFont: 'default'
|
||||
}
|
||||
},
|
||||
cookies: []
|
||||
cookies: [],
|
||||
taskQueue: []
|
||||
};
|
||||
|
||||
export const appSlice = createSlice({
|
||||
@@ -50,7 +56,15 @@ export const appSlice = createSlice({
|
||||
},
|
||||
updateCookies: (state, action) => {
|
||||
state.cookies = action.payload;
|
||||
console.log(state.cookies);
|
||||
},
|
||||
insertTaskIntoQueue: (state, action) => {
|
||||
state.taskQueue.push(action.payload);
|
||||
},
|
||||
removeTaskFromQueue: (state, action) => {
|
||||
state.taskQueue = filter(state.taskQueue, (task) => task.uid !== action.payload.taskUid);
|
||||
},
|
||||
removeAllTasksFromQueue: (state) => {
|
||||
state.taskQueue = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -64,7 +78,10 @@ export const {
|
||||
hideHomePage,
|
||||
showPreferences,
|
||||
updatePreferences,
|
||||
updateCookies
|
||||
updateCookies,
|
||||
insertTaskIntoQueue,
|
||||
removeTaskFromQueue,
|
||||
removeAllTasksFromQueue
|
||||
} = appSlice.actions;
|
||||
|
||||
export const savePreferences = (preferences) => (dispatch, getState) => {
|
||||
@@ -84,4 +101,17 @@ export const savePreferences = (preferences) => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteCookiesForDomain = (domain) => (dispatch, getState) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('renderer:delete-cookies-for-domain', domain).then(resolve).catch(reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const completeQuitFlow = () => (dispatch, getState) => {
|
||||
const { ipcRenderer } = window;
|
||||
return ipcRenderer.invoke('main:complete-quit-flow');
|
||||
};
|
||||
|
||||
export default appSlice.reducer;
|
||||
|
||||
@@ -1,51 +1,45 @@
|
||||
import path from 'path';
|
||||
import toast from 'react-hot-toast';
|
||||
import trim from 'lodash/trim';
|
||||
import { collectionSchema, environmentSchema, itemSchema } from '@usebruno/schema';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
import get from 'lodash/get';
|
||||
import filter from 'lodash/filter';
|
||||
import { uuid } from 'utils/common';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import trim from 'lodash/trim';
|
||||
import path from 'path';
|
||||
import { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';
|
||||
import toast from 'react-hot-toast';
|
||||
import {
|
||||
findItemInCollection,
|
||||
moveCollectionItem,
|
||||
getItemsToResequence,
|
||||
moveCollectionItemToRootOfCollection,
|
||||
findCollectionByUid,
|
||||
transformRequestToSaveToFilesystem,
|
||||
findParentItemInCollection,
|
||||
findEnvironmentInCollection,
|
||||
isItemARequest,
|
||||
findItemInCollection,
|
||||
findParentItemInCollection,
|
||||
getItemsToResequence,
|
||||
isItemAFolder,
|
||||
refreshUidsInItem
|
||||
isItemARequest,
|
||||
moveCollectionItem,
|
||||
moveCollectionItemToRootOfCollection,
|
||||
refreshUidsInItem,
|
||||
transformRequestToSaveToFilesystem
|
||||
} from 'utils/collections';
|
||||
import { collectionSchema, itemSchema, environmentSchema, environmentsSchema } from '@usebruno/schema';
|
||||
import { waitForNextTick } from 'utils/common';
|
||||
import { getDirectoryName, isWindowsOS, PATH_SEPARATOR } from 'utils/common/platform';
|
||||
import { sendNetworkRequest, cancelNetworkRequest } from 'utils/network';
|
||||
import { uuid, waitForNextTick } from 'utils/common';
|
||||
import { PATH_SEPARATOR, getDirectoryName } from 'utils/common/platform';
|
||||
import { cancelNetworkRequest, sendNetworkRequest } from 'utils/network';
|
||||
|
||||
import {
|
||||
updateLastAction,
|
||||
updateNextAction,
|
||||
resetRunResults,
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
newItem as _newItem,
|
||||
cloneItem as _cloneItem,
|
||||
deleteItem as _deleteItem,
|
||||
saveRequest as _saveRequest,
|
||||
selectEnvironment as _selectEnvironment,
|
||||
collectionAddEnvFileEvent as _collectionAddEnvFileEvent,
|
||||
createCollection as _createCollection,
|
||||
renameCollection as _renameCollection,
|
||||
removeCollection as _removeCollection,
|
||||
selectEnvironment as _selectEnvironment,
|
||||
sortCollections as _sortCollections,
|
||||
collectionAddEnvFileEvent as _collectionAddEnvFileEvent
|
||||
requestCancelled,
|
||||
resetRunResults,
|
||||
responseReceived,
|
||||
updateLastAction
|
||||
} from './index';
|
||||
|
||||
import { each } from 'lodash';
|
||||
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
|
||||
import { resolveRequestFilename } from 'utils/common/platform';
|
||||
import { parseQueryParams, splitOnFirst } from 'utils/url/index';
|
||||
import { each } from 'lodash';
|
||||
|
||||
export const renameCollection = (newName, collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
@@ -90,10 +84,41 @@ export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const saveMultipleRequests = (items) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const { collections } = state.collections;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const itemsToSave = [];
|
||||
each(items, (item) => {
|
||||
const collection = findCollectionByUid(collections, item.collectionUid);
|
||||
if (collection) {
|
||||
const itemToSave = transformRequestToSaveToFilesystem(item);
|
||||
const itemIsValid = itemSchema.validateSync(itemToSave);
|
||||
if (itemIsValid) {
|
||||
itemsToSave.push({
|
||||
item: itemToSave,
|
||||
pathname: item.pathname
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-multiple-requests', itemsToSave)
|
||||
.then(resolve)
|
||||
.catch((err) => {
|
||||
toast.error('Failed to save requests!');
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const collection = findCollectionByUid(state.collections.collections, collectionUid);
|
||||
console.log(collection.root);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!collection) {
|
||||
@@ -319,7 +344,20 @@ export const cloneItem = (newName, itemUid, collectionUid) => (dispatch, getStat
|
||||
}
|
||||
|
||||
if (isItemAFolder(item)) {
|
||||
throw new Error('Cloning folders is not supported yet');
|
||||
const parentFolder = findParentItemInCollection(collection, item.uid) || collection;
|
||||
|
||||
const folderWithSameNameExists = find(
|
||||
parentFolder.items,
|
||||
(i) => i.type === 'folder' && trim(i.name) === trim(newName)
|
||||
);
|
||||
|
||||
if (folderWithSameNameExists) {
|
||||
return reject(new Error('Duplicate folder names under same parent folder are not allowed'));
|
||||
}
|
||||
|
||||
const collectionPath = `${parentFolder.pathname}${PATH_SEPARATOR}${newName}`;
|
||||
ipcRenderer.invoke('renderer:clone-folder', item, collectionPath).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
const parentItem = findParentItemInCollection(collectionCopy, itemUid);
|
||||
@@ -583,7 +621,6 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
urlParam.enabled = true;
|
||||
});
|
||||
|
||||
const collectionCopy = cloneDeep(collection);
|
||||
const item = {
|
||||
uid: uuid(),
|
||||
type: requestType,
|
||||
@@ -620,17 +657,13 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
|
||||
// the useCollectionNextAction() will track this and open the new request in a new tab
|
||||
// once the request is created
|
||||
// task middleware will track this and open the new request in a new tab once request is created
|
||||
dispatch(
|
||||
updateNextAction({
|
||||
nextAction: {
|
||||
type: 'OPEN_REQUEST',
|
||||
payload: {
|
||||
pathname: fullName
|
||||
}
|
||||
},
|
||||
collectionUid
|
||||
insertTaskIntoQueue({
|
||||
uid: uuid(),
|
||||
type: 'OPEN_REQUEST',
|
||||
collectionUid,
|
||||
itemPathname: fullName
|
||||
})
|
||||
);
|
||||
} else {
|
||||
@@ -650,18 +683,13 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
ipcRenderer.invoke('renderer:new-request', fullName, item).then(resolve).catch(reject);
|
||||
|
||||
// the useCollectionNextAction() will track this and open the new request in a new tab
|
||||
// once the request is created
|
||||
// task middleware will track this and open the new request in a new tab once request is created
|
||||
dispatch(
|
||||
updateNextAction({
|
||||
nextAction: {
|
||||
type: 'OPEN_REQUEST',
|
||||
payload: {
|
||||
pathname: fullName
|
||||
}
|
||||
},
|
||||
collectionUid
|
||||
insertTaskIntoQueue({
|
||||
uid: uuid(),
|
||||
type: 'OPEN_REQUEST',
|
||||
collectionUid,
|
||||
itemPathname: fullName
|
||||
})
|
||||
);
|
||||
} else {
|
||||
@@ -927,7 +955,17 @@ export const createCollection = (collectionName, collectionFolderName, collectio
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
||||
export const cloneCollection = (collectionName, collectionFolderName, collectionLocation, perviousPath) => () => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
return ipcRenderer.invoke(
|
||||
'renderer:clone-collection',
|
||||
collectionName,
|
||||
collectionFolderName,
|
||||
collectionLocation,
|
||||
perviousPath
|
||||
);
|
||||
};
|
||||
export const openCollection = () => () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
import { uuid } from 'utils/common';
|
||||
import find from 'lodash/find';
|
||||
import map from 'lodash/map';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import concat from 'lodash/concat';
|
||||
import filter from 'lodash/filter';
|
||||
import each from 'lodash/each';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { splitOnFirst } from 'utils/url';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import concat from 'lodash/concat';
|
||||
import each from 'lodash/each';
|
||||
import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import get from 'lodash/get';
|
||||
import map from 'lodash/map';
|
||||
import set from 'lodash/set';
|
||||
import {
|
||||
findCollectionByUid,
|
||||
findCollectionByPathname,
|
||||
findItemInCollection,
|
||||
findEnvironmentInCollection,
|
||||
findItemInCollectionByPathname,
|
||||
addDepth,
|
||||
areItemsTheSameExceptSeqUpdate,
|
||||
collapseCollection,
|
||||
deleteItemInCollection,
|
||||
deleteItemInCollectionByPathname,
|
||||
isItemARequest,
|
||||
areItemsTheSameExceptSeqUpdate
|
||||
findCollectionByPathname,
|
||||
findCollectionByUid,
|
||||
findEnvironmentInCollection,
|
||||
findItemInCollection,
|
||||
findItemInCollectionByPathname,
|
||||
isItemARequest
|
||||
} from 'utils/collections';
|
||||
import { parseQueryParams, stringifyQueryParams } from 'utils/url';
|
||||
import { getSubdirectoriesFromRoot, getDirectoryName, PATH_SEPARATOR } from 'utils/common/platform';
|
||||
import { uuid } from 'utils/common';
|
||||
import { PATH_SEPARATOR, getDirectoryName, getSubdirectoriesFromRoot } from 'utils/common/platform';
|
||||
import { parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url';
|
||||
|
||||
const initialState = {
|
||||
collections: [],
|
||||
@@ -50,10 +49,6 @@ export const collectionsSlice = createSlice({
|
||||
collection.importedAt = new Date().getTime();
|
||||
collection.lastAction = null;
|
||||
|
||||
// an improvement over the above approach.
|
||||
// this defines an action that need to be performed next and is executed vy the useCollectionNextAction()
|
||||
collection.nextAction = null;
|
||||
|
||||
collapseCollection(collection);
|
||||
addDepth(collection.items);
|
||||
if (!collectionUids.includes(collection.uid)) {
|
||||
@@ -100,14 +95,6 @@ export const collectionsSlice = createSlice({
|
||||
collection.lastAction = lastAction;
|
||||
}
|
||||
},
|
||||
updateNextAction: (state, action) => {
|
||||
const { collectionUid, nextAction } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
collection.nextAction = nextAction;
|
||||
}
|
||||
},
|
||||
updateSettingsSelectedTab: (state, action) => {
|
||||
const { collectionUid, tab } = action.payload;
|
||||
|
||||
@@ -217,6 +204,19 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
if (variable) {
|
||||
variable.value = value;
|
||||
} else {
|
||||
// __name__ is a private variable used to store the name of the environment
|
||||
// this is not a user defined variable and hence should not be updated
|
||||
if (key !== '__name__') {
|
||||
activeEnvironment.variables.push({
|
||||
name: key,
|
||||
value,
|
||||
secret: false,
|
||||
enabled: true,
|
||||
type: 'text',
|
||||
uid: uuid()
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -256,6 +256,16 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
responseCleared: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
if (item) {
|
||||
item.response = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
saveRequest: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@@ -972,7 +982,6 @@ export const collectionsSlice = createSlice({
|
||||
switch (action.payload.mode) {
|
||||
case 'awsv4':
|
||||
set(collection, 'root.request.auth.awsv4', action.payload.content);
|
||||
console.log('set auth awsv4', action.payload.content);
|
||||
break;
|
||||
case 'bearer':
|
||||
set(collection, 'root.request.auth.bearer', action.payload.content);
|
||||
@@ -1212,6 +1221,7 @@ export const collectionsSlice = createSlice({
|
||||
existingEnv.variables = environment.variables;
|
||||
} else {
|
||||
collection.environments.push(environment);
|
||||
collection.environments.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const lastAction = collection.lastAction;
|
||||
if (lastAction && lastAction.type === 'ADD_ENVIRONMENT') {
|
||||
@@ -1371,7 +1381,6 @@ export const {
|
||||
removeCollection,
|
||||
sortCollections,
|
||||
updateLastAction,
|
||||
updateNextAction,
|
||||
updateSettingsSelectedTab,
|
||||
collectionUnlinkEnvFileEvent,
|
||||
saveEnvironment,
|
||||
@@ -1384,6 +1393,7 @@ export const {
|
||||
processEnvUpdateEvent,
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
responseCleared,
|
||||
saveRequest,
|
||||
deleteRequestDraft,
|
||||
newEphemeralHttpRequest,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import find from 'lodash/find';
|
||||
import filter from 'lodash/filter';
|
||||
import last from 'lodash/last';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
import last from 'lodash/last';
|
||||
|
||||
// todo: errors should be tracked in each slice and displayed as toasts
|
||||
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import themes from 'themes/index';
|
||||
import useLocalStorage from 'hooks/useLocalStorage/index';
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { ThemeProvider as SCThemeProvider } from 'styled-components';
|
||||
|
||||
export const ThemeContext = createContext();
|
||||
export const ThemeProvider = (props) => {
|
||||
const isBrowserThemeLight = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
const [storedTheme, setStoredTheme] = useLocalStorage('bruno.theme', isBrowserThemeLight ? 'light' : 'dark');
|
||||
const [displayedTheme, setDisplayedTheme] = useState(isBrowserThemeLight ? 'light' : 'dark');
|
||||
const [storedTheme, setStoredTheme] = useLocalStorage('bruno.theme', 'system');
|
||||
|
||||
const theme = themes[storedTheme];
|
||||
useEffect(() => {
|
||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
|
||||
if (storedTheme !== 'system') return;
|
||||
setDisplayedTheme(e.matches ? 'light' : 'dark');
|
||||
});
|
||||
}, []);
|
||||
|
||||
const theme = storedTheme === 'system' ? themes[displayedTheme] : themes[storedTheme];
|
||||
const themeOptions = Object.keys(themes);
|
||||
const value = {
|
||||
theme,
|
||||
|
||||
@@ -173,6 +173,11 @@ const darkTheme = {
|
||||
color: '#a5a5a5',
|
||||
bg: '#626262',
|
||||
border: '#626262'
|
||||
},
|
||||
danger: {
|
||||
color: '#fff',
|
||||
bg: '#dc3545',
|
||||
border: '#dc3545'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -177,6 +177,11 @@ const lightTheme = {
|
||||
color: '#9f9f9f',
|
||||
bg: '#efefef',
|
||||
border: 'rgb(234, 234, 234)'
|
||||
},
|
||||
danger: {
|
||||
color: '#fff',
|
||||
bg: '#dc3545',
|
||||
border: '#dc3545'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ const createHeaders = (headers) => {
|
||||
};
|
||||
|
||||
const createQuery = (queryParams = []) => {
|
||||
return queryParams.map((param) => {
|
||||
return {
|
||||
return queryParams
|
||||
.filter((param) => param.enabled)
|
||||
.map((param) => ({
|
||||
name: param.name,
|
||||
value: param.value
|
||||
};
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
const createPostData = (body) => {
|
||||
|
||||
92
packages/bruno-app/src/utils/codemirror/javascript-lint.js
Normal file
92
packages/bruno-app/src/utils/codemirror/javascript-lint.js
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* MIT License
|
||||
* https://github.com/codemirror/codemirror5/blob/master/LICENSE
|
||||
*
|
||||
* Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others
|
||||
*/
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
const { filter } = require('lodash');
|
||||
|
||||
function validator(text, options) {
|
||||
if (!window.JSHINT) {
|
||||
if (window.console) {
|
||||
window.console.error('Error: window.JSHINT not defined, CodeMirror JavaScript linting cannot run.');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
if (!options.indent)
|
||||
// JSHint error.character actually is a column index, this fixes underlining on lines using tabs for indentation
|
||||
options.indent = 1; // JSHint default value is 4
|
||||
JSHINT(text, options, options.globals);
|
||||
var errors = JSHINT.data().errors,
|
||||
result = [];
|
||||
|
||||
/*
|
||||
* Filter out errors due to top level awaits
|
||||
* See https://github.com/usebruno/bruno/issues/1214
|
||||
*
|
||||
* Once JSHINT top level await support is added, this file can be removed
|
||||
* and we can use the default javascript-lint addon from codemirror
|
||||
*/
|
||||
errors = filter(errors, (error) => {
|
||||
if (error.code === 'E058') {
|
||||
if (
|
||||
error.evidence &&
|
||||
error.evidence.includes('await') &&
|
||||
error.reason === 'Missing semicolon.' &&
|
||||
error.scope === '(main)'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (errors) parseErrors(errors, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
CodeMirror.registerHelper('lint', 'javascript', validator);
|
||||
|
||||
function parseErrors(errors, output) {
|
||||
for (var i = 0; i < errors.length; i++) {
|
||||
var error = errors[i];
|
||||
if (error) {
|
||||
if (error.line <= 0) {
|
||||
if (window.console) {
|
||||
window.console.warn('Cannot display JSHint error (invalid line ' + error.line + ')', error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var start = error.character - 1,
|
||||
end = start + 1;
|
||||
if (error.evidence) {
|
||||
var index = error.evidence.substring(start).search(/.\b/);
|
||||
if (index > -1) {
|
||||
end += index;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to format expected by validation service
|
||||
var hint = {
|
||||
message: error.reason,
|
||||
severity: error.code ? (error.code.startsWith('W') ? 'warning' : 'error') : 'error',
|
||||
from: CodeMirror.Pos(error.line - 1, start),
|
||||
to: CodeMirror.Pos(error.line - 1, end)
|
||||
};
|
||||
|
||||
output.push(hint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,12 @@ export const findCollectionByPathname = (collections, pathname) => {
|
||||
return find(collections, (c) => c.pathname === pathname);
|
||||
};
|
||||
|
||||
export const findCollectionByItemUid = (collections, itemUid) => {
|
||||
return find(collections, (c) => {
|
||||
return findItemInCollection(c, itemUid);
|
||||
});
|
||||
};
|
||||
|
||||
export const findItemByPathname = (items = [], pathname) => {
|
||||
return find(items, (i) => i.pathname === pathname);
|
||||
};
|
||||
|
||||
@@ -42,7 +42,10 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getCodeMirrorModeBasedOnContentType = (contentType) => {
|
||||
export const getCodeMirrorModeBasedOnContentType = (contentType, body) => {
|
||||
if (typeof body === 'object') {
|
||||
return 'application/ld+json';
|
||||
}
|
||||
if (!contentType || typeof contentType !== 'string') {
|
||||
return 'application/text';
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export const safeParseJSON = (str) => {
|
||||
};
|
||||
|
||||
export const safeStringifyJSON = (obj, indent = false) => {
|
||||
if (!obj) {
|
||||
if (obj === undefined) {
|
||||
return obj;
|
||||
}
|
||||
try {
|
||||
@@ -106,3 +106,7 @@ export const startsWith = (str, search) => {
|
||||
|
||||
return str.substr(0, search.length) === search;
|
||||
};
|
||||
|
||||
export const pluralizeWord = (word, count) => {
|
||||
return count === 1 ? word : `${word}s`;
|
||||
};
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const envVariableNameRegex = /^(?!\d)[\w-]*$/;
|
||||
export const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
@@ -43,8 +43,6 @@ export const getRequestFromCurlCommand = (curlCommand) => {
|
||||
body.xml = parsedBody;
|
||||
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||
body.mode = 'formUrlEncoded';
|
||||
console.log(parsedBody);
|
||||
console.log(parseFormData(parsedBody));
|
||||
body.formUrlEncoded = parseFormData(parsedBody);
|
||||
} else if (contentType.includes('multipart/form-data')) {
|
||||
body.mode = 'multipartForm';
|
||||
|
||||
@@ -41,7 +41,7 @@ const parseGraphQL = (text) => {
|
||||
} catch (e) {
|
||||
return {
|
||||
query: '',
|
||||
variables: {}
|
||||
variables: ''
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,7 +54,7 @@ const buildEmptyJsonBody = (bodySchema) => {
|
||||
const transformOpenapiRequestItem = (request) => {
|
||||
let _operationObject = request.operationObject;
|
||||
|
||||
let operationName = _operationObject.operationId || _operationObject.summary || _operationObject.description;
|
||||
let operationName = _operationObject.summary || _operationObject.operationId || _operationObject.description;
|
||||
if (!operationName) {
|
||||
operationName = `${request.method} ${request.path}`;
|
||||
}
|
||||
|
||||
@@ -55,16 +55,27 @@ const convertV21Auth = (array) => {
|
||||
|
||||
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
|
||||
brunoParent.items = brunoParent.items || [];
|
||||
const folderMap = {};
|
||||
|
||||
each(item, (i) => {
|
||||
if (isItemAFolder(i)) {
|
||||
const baseFolderName = i.name;
|
||||
let folderName = baseFolderName;
|
||||
let count = 1;
|
||||
|
||||
while (folderMap[folderName]) {
|
||||
folderName = `${baseFolderName}_${count}`;
|
||||
count++;
|
||||
}
|
||||
|
||||
const brunoFolderItem = {
|
||||
uid: uuid(),
|
||||
name: i.name,
|
||||
name: folderName,
|
||||
type: 'folder',
|
||||
items: []
|
||||
};
|
||||
brunoParent.items.push(brunoFolderItem);
|
||||
folderMap[folderName] = brunoFolderItem;
|
||||
if (i.item && i.item.length) {
|
||||
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## 1.3.0
|
||||
|
||||
- Junit report generation
|
||||
|
||||
## 1.2.1
|
||||
|
||||
- Fixed bug related to `bru.setNextRequest()`
|
||||
|
||||
## 1.2.0
|
||||
|
||||
- Support for `bru.setNextRequest()`
|
||||
|
||||
## 1.1.0
|
||||
|
||||
- Upgraded axios to 1.5.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@usebruno/cli",
|
||||
"version": "1.1.1",
|
||||
"version": "1.3.0",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
@@ -24,7 +24,7 @@
|
||||
"package.json"
|
||||
],
|
||||
"dependencies": {
|
||||
"@usebruno/js": "0.9.2",
|
||||
"@usebruno/js": "0.9.4",
|
||||
"@usebruno/lang": "0.9.0",
|
||||
"axios": "^1.5.1",
|
||||
"chai": "^4.3.7",
|
||||
@@ -40,6 +40,7 @@
|
||||
"mustache": "^4.2.0",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"yargs": "^17.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const { forOwn } = require('lodash');
|
||||
const { exists, isFile, isDirectory } = require('../utils/filesystem');
|
||||
const { runSingleRequest } = require('../runner/run-single-request');
|
||||
const { bruToEnvJson, getEnvVars } = require('../utils/bru');
|
||||
const makeJUnitOutput = require('../reporters/junit');
|
||||
const { rpad } = require('../utils/common');
|
||||
const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru');
|
||||
const { dotenvToJson } = require('@usebruno/lang');
|
||||
@@ -186,7 +187,13 @@ const builder = async (yargs) => {
|
||||
})
|
||||
.option('output', {
|
||||
alias: 'o',
|
||||
describe: 'Path to write JSON results to',
|
||||
describe: 'Path to write file results to',
|
||||
type: 'string'
|
||||
})
|
||||
.option('format', {
|
||||
alias: 'f',
|
||||
describe: 'Format of the file results; available formats are "json" (default) or "junit"',
|
||||
default: 'json',
|
||||
type: 'string'
|
||||
})
|
||||
.option('insecure', {
|
||||
@@ -204,12 +211,16 @@ const builder = async (yargs) => {
|
||||
.example(
|
||||
'$0 run request.bru --output results.json',
|
||||
'Run a request and write the results to results.json in the current directory'
|
||||
)
|
||||
.example(
|
||||
'$0 run request.bru --output results.xml --format junit',
|
||||
'Run a request and write the results to results.xml in junit format in the current directory'
|
||||
);
|
||||
};
|
||||
|
||||
const handler = async function (argv) {
|
||||
try {
|
||||
let { filename, cacert, env, envVar, insecure, r: recursive, output: outputPath } = argv;
|
||||
let { filename, cacert, env, envVar, insecure, r: recursive, output: outputPath, format } = argv;
|
||||
const collectionPath = process.cwd();
|
||||
|
||||
// todo
|
||||
@@ -297,6 +308,11 @@ const handler = async function (argv) {
|
||||
}
|
||||
}
|
||||
|
||||
if (['json', 'junit'].indexOf(format) === -1) {
|
||||
console.error(chalk.red(`Format must be one of "json" or "junit"`));
|
||||
return;
|
||||
}
|
||||
|
||||
// load .env file at root of collection if it exists
|
||||
const dotEnvPath = path.join(collectionPath, '.env');
|
||||
const dotEnvExists = await exists(dotEnvPath);
|
||||
@@ -355,8 +371,13 @@ const handler = async function (argv) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const iter of bruJsons) {
|
||||
let currentRequestIndex = 0;
|
||||
let nJumps = 0; // count the number of jumps to avoid infinite loops
|
||||
while (currentRequestIndex < bruJsons.length) {
|
||||
const iter = bruJsons[currentRequestIndex];
|
||||
const { bruFilepath, bruJson } = iter;
|
||||
|
||||
const start = process.hrtime();
|
||||
const result = await runSingleRequest(
|
||||
bruFilepath,
|
||||
bruJson,
|
||||
@@ -368,7 +389,33 @@ const handler = async function (argv) {
|
||||
collectionRoot
|
||||
);
|
||||
|
||||
results.push(result);
|
||||
results.push({
|
||||
...result,
|
||||
runtime: process.hrtime(start)[0] + process.hrtime(start)[1] / 1e9,
|
||||
suitename: bruFilepath.replace('.bru', '')
|
||||
});
|
||||
|
||||
// determine next request
|
||||
const nextRequestName = result?.nextRequestName;
|
||||
if (nextRequestName !== undefined) {
|
||||
nJumps++;
|
||||
if (nJumps > 10000) {
|
||||
console.error(chalk.red(`Too many jumps, possible infinite loop`));
|
||||
process.exit(1);
|
||||
}
|
||||
if (nextRequestName === null) {
|
||||
break;
|
||||
}
|
||||
const nextRequestIdx = bruJsons.findIndex((iter) => iter.bruJson.name === nextRequestName);
|
||||
if (nextRequestIdx >= 0) {
|
||||
currentRequestIndex = nextRequestIdx;
|
||||
} else {
|
||||
console.error("Could not find request with name '" + nextRequestName + "'");
|
||||
currentRequestIndex++;
|
||||
}
|
||||
} else {
|
||||
currentRequestIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
const summary = printRunSummary(results);
|
||||
@@ -388,7 +435,12 @@ const handler = async function (argv) {
|
||||
results
|
||||
};
|
||||
|
||||
fs.writeFileSync(outputPath, JSON.stringify(outputJson, null, 2));
|
||||
if (format === 'json') {
|
||||
fs.writeFileSync(outputPath, JSON.stringify(outputJson, null, 2));
|
||||
} else if (format === 'junit') {
|
||||
makeJUnitOutput(results, outputPath);
|
||||
}
|
||||
|
||||
console.log(chalk.dim(chalk.grey(`Wrote results to ${outputPath}`)));
|
||||
}
|
||||
|
||||
|
||||
85
packages/bruno-cli/src/reporters/junit.js
Normal file
85
packages/bruno-cli/src/reporters/junit.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const xmlbuilder = require('xmlbuilder');
|
||||
|
||||
const makeJUnitOutput = async (results, outputPath) => {
|
||||
const output = {
|
||||
testsuites: {
|
||||
testsuite: []
|
||||
}
|
||||
};
|
||||
|
||||
results.forEach((result) => {
|
||||
const assertionTestCount = result.assertionResults ? result.assertionResults.length : 0;
|
||||
const testCount = result.testResults ? result.testResults.length : 0;
|
||||
const totalTests = assertionTestCount + testCount;
|
||||
|
||||
const suite = {
|
||||
'@name': result.suitename,
|
||||
'@errors': 0,
|
||||
'@failures': 0,
|
||||
'@skipped': 0,
|
||||
'@tests': totalTests,
|
||||
'@timestamp': new Date().toISOString().split('Z')[0],
|
||||
'@hostname': os.hostname(),
|
||||
'@time': result.runtime.toFixed(3),
|
||||
testcase: []
|
||||
};
|
||||
|
||||
result.assertionResults &&
|
||||
result.assertionResults.forEach((assertion) => {
|
||||
const testcase = {
|
||||
'@name': `${assertion.lhsExpr} ${assertion.rhsExpr}`,
|
||||
'@status': assertion.status,
|
||||
'@classname': result.request.url,
|
||||
'@time': (result.runtime / totalTests).toFixed(3)
|
||||
};
|
||||
|
||||
if (assertion.status === 'fail') {
|
||||
suite['@failures']++;
|
||||
|
||||
testcase.failure = [{ '@type': 'failure', '@message': assertion.error }];
|
||||
}
|
||||
|
||||
suite.testcase.push(testcase);
|
||||
});
|
||||
|
||||
result.testResults &&
|
||||
result.testResults.forEach((test) => {
|
||||
const testcase = {
|
||||
'@name': test.description,
|
||||
'@status': test.status,
|
||||
'@classname': result.request.url,
|
||||
'@time': (result.runtime / totalTests).toFixed(3)
|
||||
};
|
||||
|
||||
if (test.status === 'fail') {
|
||||
suite['@failures']++;
|
||||
|
||||
testcase.failure = [{ '@type': 'failure', '@message': test.error }];
|
||||
}
|
||||
|
||||
suite.testcase.push(testcase);
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
suite['@errors'] = 1;
|
||||
suite['@tests'] = 1;
|
||||
suite.testcase = [
|
||||
{
|
||||
'@name': 'Test suite has no errors',
|
||||
'@status': 'fail',
|
||||
'@classname': result.request.url,
|
||||
'@time': result.runtime.toFixed(3),
|
||||
error: [{ '@type': 'error', '@message': result.error }]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
output.testsuites.testsuite.push(suite);
|
||||
});
|
||||
|
||||
fs.writeFileSync(outputPath, xmlbuilder.create(output).end({ pretty: true }));
|
||||
};
|
||||
|
||||
module.exports = makeJUnitOutput;
|
||||
@@ -28,6 +28,8 @@ const interpolateEnvVars = (str, processEnvVars) => {
|
||||
});
|
||||
};
|
||||
|
||||
const varsRegex = /(?<!\\)\{\{(?!process\.env\.\w+)(.*\..*)\}\}/g;
|
||||
|
||||
const interpolateVars = (request, envVars = {}, collectionVariables = {}, processEnvVars = {}) => {
|
||||
// we clone envVars because we don't want to modify the original object
|
||||
envVars = cloneDeep(envVars);
|
||||
@@ -43,6 +45,10 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
|
||||
return str;
|
||||
}
|
||||
|
||||
if (varsRegex.test(str)) {
|
||||
// Handlebars doesn't allow dots as identifiers, so we need to use literal segments
|
||||
str = str.replaceAll(varsRegex, '{{[$1]}}');
|
||||
}
|
||||
const template = Handlebars.compile(str, { noEscape: true });
|
||||
|
||||
// collectionVariables take precedence over envVars
|
||||
|
||||
@@ -17,6 +17,8 @@ const { SocksProxyAgent } = require('socks-proxy-agent');
|
||||
const { makeAxiosInstance } = require('../utils/axios-instance');
|
||||
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
|
||||
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
|
||||
const runSingleRequest = async function (
|
||||
filename,
|
||||
bruJson,
|
||||
@@ -29,6 +31,7 @@ const runSingleRequest = async function (
|
||||
) {
|
||||
try {
|
||||
let request;
|
||||
let nextRequestName;
|
||||
|
||||
request = prepareRequest(bruJson.request, collectionRoot);
|
||||
|
||||
@@ -66,7 +69,7 @@ const runSingleRequest = async function (
|
||||
]).join(os.EOL);
|
||||
if (requestScriptFile?.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
await scriptRuntime.runRequestScript(
|
||||
const result = await scriptRuntime.runRequestScript(
|
||||
decomment(requestScriptFile),
|
||||
request,
|
||||
envVariables,
|
||||
@@ -76,11 +79,18 @@ const runSingleRequest = async function (
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
}
|
||||
}
|
||||
|
||||
// interpolate variables inside request
|
||||
interpolateVars(request, envVariables, collectionVariables, processEnvVars);
|
||||
|
||||
if (!protocolRegex.test(request.url)) {
|
||||
request.url = `http://${request.url}`;
|
||||
}
|
||||
|
||||
const options = getOptions();
|
||||
const insecure = get(options, 'insecure', false);
|
||||
const httpsAgentRequestFields = {};
|
||||
@@ -205,11 +215,14 @@ const runSingleRequest = async function (
|
||||
},
|
||||
error: err.message,
|
||||
assertionResults: [],
|
||||
testResults: []
|
||||
testResults: [],
|
||||
nextRequestName: nextRequestName
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
response.responseTime = responseTime;
|
||||
|
||||
console.log(
|
||||
chalk.green(stripExtension(filename)) +
|
||||
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
|
||||
@@ -237,7 +250,7 @@ const runSingleRequest = async function (
|
||||
]).join(os.EOL);
|
||||
if (responseScriptFile?.length) {
|
||||
const scriptRuntime = new ScriptRuntime();
|
||||
await scriptRuntime.runResponseScript(
|
||||
const result = await scriptRuntime.runResponseScript(
|
||||
decomment(responseScriptFile),
|
||||
request,
|
||||
response,
|
||||
@@ -248,6 +261,9 @@ const runSingleRequest = async function (
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
);
|
||||
if (result?.nextRequestName !== undefined) {
|
||||
nextRequestName = result.nextRequestName;
|
||||
}
|
||||
}
|
||||
|
||||
// run assertions
|
||||
@@ -319,7 +335,8 @@ const runSingleRequest = async function (
|
||||
},
|
||||
error: null,
|
||||
assertionResults,
|
||||
testResults
|
||||
testResults,
|
||||
nextRequestName: nextRequestName
|
||||
};
|
||||
} catch (err) {
|
||||
console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
|
||||
|
||||
135
packages/bruno-cli/tests/reporters/junit.spec.js
Normal file
135
packages/bruno-cli/tests/reporters/junit.spec.js
Normal file
@@ -0,0 +1,135 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
const xmlbuilder = require('xmlbuilder');
|
||||
const fs = require('fs');
|
||||
|
||||
const makeJUnitOutput = require('../../src/reporters/junit');
|
||||
|
||||
describe('makeJUnitOutput', () => {
|
||||
let createStub = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(xmlbuilder, 'create').mockImplementation(() => {
|
||||
return { end: createStub };
|
||||
});
|
||||
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should produce a junit spec object for serialization', () => {
|
||||
const results = [
|
||||
{
|
||||
description: 'description provided',
|
||||
suitename: 'Tests/Suite A',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://ima.test'
|
||||
},
|
||||
assertionResults: [
|
||||
{
|
||||
lhsExpr: 'res.status',
|
||||
rhsExpr: 'eq 200',
|
||||
status: 'pass'
|
||||
},
|
||||
{
|
||||
lhsExpr: 'res.status',
|
||||
rhsExpr: 'neq 200',
|
||||
status: 'fail',
|
||||
error: 'expected 200 to not equal 200'
|
||||
}
|
||||
],
|
||||
runtime: 1.2345678
|
||||
},
|
||||
{
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://imanother.test'
|
||||
},
|
||||
suitename: 'Tests/Suite B',
|
||||
testResults: [
|
||||
{
|
||||
lhsExpr: 'res.status',
|
||||
rhsExpr: 'eq 200',
|
||||
description: 'A test that passes',
|
||||
status: 'pass'
|
||||
},
|
||||
{
|
||||
description: 'A test that fails',
|
||||
status: 'fail',
|
||||
error: 'expected 200 to not equal 200',
|
||||
status: 'fail'
|
||||
}
|
||||
],
|
||||
runtime: 2.3456789
|
||||
}
|
||||
];
|
||||
|
||||
makeJUnitOutput(results, '/tmp/testfile.xml');
|
||||
expect(createStub).toBeCalled;
|
||||
|
||||
const junit = xmlbuilder.create.mock.calls[0][0];
|
||||
|
||||
expect(junit.testsuites).toBeDefined;
|
||||
expect(junit.testsuites.testsuite.length).toBe(2);
|
||||
expect(junit.testsuites.testsuite[0].testcase.length).toBe(2);
|
||||
expect(junit.testsuites.testsuite[1].testcase.length).toBe(2);
|
||||
|
||||
expect(junit.testsuites.testsuite[0]['@name']).toBe('Tests/Suite A');
|
||||
expect(junit.testsuites.testsuite[1]['@name']).toBe('Tests/Suite B');
|
||||
|
||||
expect(junit.testsuites.testsuite[0]['@tests']).toBe(2);
|
||||
expect(junit.testsuites.testsuite[1]['@tests']).toBe(2);
|
||||
|
||||
const testcase = junit.testsuites.testsuite[0].testcase[0];
|
||||
|
||||
expect(testcase['@name']).toBe('res.status eq 200');
|
||||
expect(testcase['@status']).toBe('pass');
|
||||
|
||||
const failcase = junit.testsuites.testsuite[0].testcase[1];
|
||||
|
||||
expect(failcase['@name']).toBe('res.status neq 200');
|
||||
expect(failcase.failure).toBeDefined;
|
||||
expect(failcase.failure[0]['@type']).toBe('failure');
|
||||
});
|
||||
|
||||
it('should handle request errors', () => {
|
||||
const results = [
|
||||
{
|
||||
description: 'description provided',
|
||||
suitename: 'Tests/Suite A',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://ima.test'
|
||||
},
|
||||
assertionResults: [
|
||||
{
|
||||
lhsExpr: 'res.status',
|
||||
rhsExpr: 'eq 200',
|
||||
status: 'fail'
|
||||
}
|
||||
],
|
||||
runtime: 1.2345678,
|
||||
error: 'timeout of 2000ms exceeded'
|
||||
}
|
||||
];
|
||||
|
||||
makeJUnitOutput(results, '/tmp/testfile.xml');
|
||||
|
||||
const junit = xmlbuilder.create.mock.calls[0][0];
|
||||
|
||||
expect(createStub).toBeCalled;
|
||||
|
||||
expect(junit.testsuites).toBeDefined;
|
||||
expect(junit.testsuites.testsuite.length).toBe(1);
|
||||
expect(junit.testsuites.testsuite[0].testcase.length).toBe(1);
|
||||
|
||||
const failcase = junit.testsuites.testsuite[0].testcase[0];
|
||||
|
||||
expect(failcase['@name']).toBe('Test suite has no errors');
|
||||
expect(failcase.error).toBeDefined;
|
||||
expect(failcase.error[0]['@type']).toBe('error');
|
||||
expect(failcase.error[0]['@message']).toBe('timeout of 2000ms exceeded');
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user