Compare commits

..

145 Commits

Author SHA1 Message Date
Anoop M D
b059faca53 chore: deleted file commited accidentally 2024-01-27 14:12:11 +05:30
Anoop M D
9ad265e74f feat(#1460): string interpolation 2024-01-27 14:07:49 +05:30
Tathagata Chakraborty
aed1b41da6 fixed misspelling (global, collection) (#1299)
Co-authored-by: Tathagata Chakraborty <tathagata.chakraborty97@gmail.com>
2024-01-26 22:25:39 +05:30
Anoop M D
d8354dca14 chore(#1441): updated codemirrot hint: bru.setNextRequest(requestName) 2024-01-25 23:40:40 +05:30
Felipe Vidal
e375ffbed1 Added bru.setNextRequest into the hint words list (#1441) 2024-01-25 23:38:04 +05:30
Nelu Platonov
7de5bbbdf6 fix(#1143): Fix PR #971 - Use literal segments only for env/collection variables + Add to CLI (#1154)
* fix(#1143): Fix PR #971 - Add literal-segment notation in string only to variables that are not process env vars
* fix(#1143): Fix PR #971 - Add to CLI as well
* fix(#1143): Fix PR #971 - Use improved Regex after CR + add test case for escaped vars
2024-01-25 19:36:32 +05:30
Timon
dbb5e912eb fix(#1000): Convert too long numbers into String for collection vars (#1405) 2024-01-18 03:38:57 +05:30
Timon
4e34aba1ca fix(#263): Replace vm2 with fork of vm2 to fix security issues (#1400) 2024-01-17 04:01:00 +05:30
Rinku Chaudhari
b5fccef417 style: fix dark mode colors of golden edition modal (#1358) 2024-01-10 15:46:05 +05:30
Anoop M D
db9aeec498 feat: improved handling logic while closing unsaved tabs/requests 2024-01-09 22:12:40 +05:30
Anoop M D
3e627522b7 feat(#1162): warn if there are unsaved requests when quitting 2024-01-09 14:01:04 +05:30
Ricardo Silverio
85f24eec77 [Feature] Prompt user to save requests before exiting app (#1317)
* Starting quit flow and focusing in draft
* Finishing app if there is no draft to save
* Automatically opening request after creation through event queue
* Fix remove events from queue using pathname to find item
* Removing updateNextAction
* Listening via predicate
* Confirm close dialog toggle moved to store
* Draft operations as tab actions
* Complete quit flow
* Fixing close app/window hooks
* Breaking the chain when dismissing dialog
* Displaying request name in ConfirmRequestClose modal
* Added disableEscapeKey and disableCloseOnOutsideClick props to Modal (passed in ConfirmRequestClose)
* Removing logs
* Refactor
* listenerMiddleware module
* ipc events listeners names
* Update next action
* Helpful comments
* Eventually handle events to close request even if is no draft
* Request name in bold
2024-01-08 17:21:55 +05:30
Anoop M D
bdfcd78f3a chore: reorganized readme sections 2024-01-07 15:39:59 +05:30
Anoop M D
fce164001b chore: added golden edition signup form 2024-01-07 15:24:32 +05:30
Anoop M D
814d31e638 chore: bumped version to 1.6.1 2024-01-05 16:17:59 +05:30
Anoop M D
51d4dbd69b fix(#1214): fixed code mirror lint issues 2024-01-05 16:11:33 +05:30
Anoop M D
05cab18e18 fix(#1330): fixed graphql syntax highlighting issue 2024-01-05 14:52:29 +05:30
Anoop M D
fcc3a1e944 fix(#1329): fix code generation issue 2024-01-05 14:28:02 +05:30
Anoop M D
45395754bf chore: bumped version 2024-01-05 03:24:15 +05:30
Anoop M D
0db6103b69 feat: golden edition modal 2024-01-05 03:17:55 +05:30
Anoop M D
2608ec035e Merge pull request #1155 from BrandonGillis/feat/custom-ca-certificate
feat: add custom CA Certificate preference
2024-01-04 22:54:29 +05:30
Gustavo Fior
d0c25d46c9 fix #1208: add collection headers to code generated (#1316)
* fix collection headers in code generator
* remove logs
2024-01-04 13:55:13 +05:30
Doğukan Ürker
d62982d52d Wrong URL syntax and translation fixed for Turkish docs. (#1312)
* Wrong translation fixed.
* Wrong URL syntax fixed.
2024-01-03 21:37:41 +05:30
Anoop M D
2810c6758d Merge pull request #1304 from DogukanUrker/main
Turkish docs updated.
2024-01-03 15:34:53 +05:30
Anoop M D
3967859c51 chore: deleted unused vscode package as it is being tracked in a seperate repo 2024-01-03 00:17:14 +05:30
Doğukan Ürker
ef5df9e114 Update publishing_bn.md 2024-01-01 19:19:51 +03:00
Doğukan Ürker
75e19574ac Update publishing_fr.md 2024-01-01 19:19:47 +03:00
Doğukan Ürker
b15ad5ca71 Update publishing_pl.md 2024-01-01 19:19:44 +03:00
Doğukan Ürker
0bd3ba493f Update publishing_pt_br.md 2024-01-01 19:19:41 +03:00
Doğukan Ürker
d2d1994546 Update publishing_ro.md 2024-01-01 19:19:14 +03:00
Doğukan Ürker
e4ae0c0357 added other languages 2024-01-01 19:17:55 +03:00
Doğukan Ürker
52cec963a3 Update publishing_ro.md 2024-01-01 19:16:28 +03:00
Doğukan Ürker
f797c5d06b Update publishing_pl.md 2024-01-01 19:16:25 +03:00
Doğukan Ürker
65879c8994 Update publishing_fr.md 2024-01-01 19:16:23 +03:00
Doğukan Ürker
2e544183db Update publishing_bn.md 2024-01-01 19:16:21 +03:00
Doğukan Ürker
ceccddf7f1 Create publishing_tr.md 2024-01-01 19:14:40 +03:00
Doğukan Ürker
c1f5da1280 Update readme_tr.md 2024-01-01 19:11:44 +03:00
Doğukan Ürker
63aa3ded1c Update readme_tr.md 2024-01-01 19:11:06 +03:00
Doğukan Ürker
26c7d4f532 Update contributing_tr.md 2024-01-01 19:10:36 +03:00
Doğukan Ürker
b7b4453278 Update readme_tr.md 2024-01-01 19:00:01 +03:00
Doğukan Ürker
49a51d6028 Update readme_tr.md 2024-01-01 18:59:24 +03:00
Anoop M D
abb24c93c5 feat(#1303): toml parser for scripts and tests 2023-12-31 02:52:41 +05:30
Anoop M D
5ba2c98e1d feat(#1303): toml handle reserved header names 2023-12-30 21:53:37 +05:30
Anoop M D
bc01188c98 feat(#1303): toml handle duplicate headers 2023-12-30 21:27:15 +05:30
Anoop M D
1754ea9f59 feat(#1296): toml tests: different kinds of headers 2023-12-30 20:29:31 +05:30
Anoop M D
2aa073c69a feat(#1296): toml tests: simple header 2023-12-30 19:09:17 +05:30
Anoop M D
48ec87ec8c Merge branch 'main' of github.com:usebruno/bruno 2023-12-30 17:14:00 +05:30
Anoop M D
35b6f7bb0a feat(#1296): restructured toml json test setup 2023-12-30 17:13:15 +05:30
Anoop M D
fff0293600 Merge pull request #1282 from csbde/main
docs: add Simplified Chinese language docs translation
2023-12-29 23:48:33 +05:30
Anoop M D
41d0698a87 feat(#1296): toml stringify simple get request 2023-12-28 22:52:30 +05:30
Anoop M D
f7ea8c93a6 chore: updated .gitignore to ignore bun lock files 2023-12-28 21:15:00 +05:30
Anoop M D
83c9629820 chore: deleted bun lock file 2023-12-28 21:14:05 +05:30
Anoop M D
d6f6032c6f Merge pull request #1285 from nguyenbavinh-decathlon/bugfix/914_crash_app_when_set_env
fix(#914): Issue crash app when update environment variable
2023-12-27 21:26:28 +05:30
nguyenbavinh-decathlon
e49999bb56 Fix issue crash app when edit environment 2023-12-27 15:11:41 +07:00
Simon CHEN
8b92055413 docs: add Simplified Chinese language docs translation 2023-12-27 01:15:28 +08:00
Anoop M D
5ca7f6b7ad Merge branch 'main' of github.com:usebruno/bruno 2023-12-25 01:57:59 +05:30
Anoop M D
83f5763e01 Merge pull request #1263 from tobiasbrandstaedter/feature/font-preferences-collection-level
feat(#1224): use font preferences in collections
2023-12-25 01:57:28 +05:30
Anoop M D
4a5196c8f5 chore: updated package deps 2023-12-25 01:55:56 +05:30
Anoop M D
eba065aa7e Merge pull request #1272 from rsilvr/fix/runner-response-pane-height
[Fix] Runner response pane height
2023-12-25 01:51:12 +05:30
Anoop M D
10dc1d95b0 Merge pull request #1270 from quentinlegay/fix-typo-readme_fr
fix typo in readme fr
2023-12-24 16:31:04 +05:30
Ricardo Silverio
00c7b40593 Fix relative position 2023-12-23 22:04:30 -03:00
Quentin LEGAY
b0ee137277 fix typo in readme fr 2023-12-23 20:30:45 +01:00
Quentin LEGAY
887c65b0d8 fix typo in readme fr 2023-12-23 20:29:13 +01:00
Anoop M D
84905ed80f Merge pull request #1269 from DeJayDev/bug/gh-1268
bugfix: bump codemirror dep
2023-12-23 23:28:16 +05:30
Dj Isaac
1f4ab3b5bd bugfix: bump codemirror dep 2023-12-22 15:49:31 -06:00
Anoop M D
da983599ab Merge pull request #1266 from rsilvr/fix/confirm-close-typo
Fixed typo in modal
2023-12-22 12:41:36 +05:30
Ricardo Silverio
fe11e45703 Fixed typo in modal 2023-12-21 22:55:15 -03:00
tobiasbrandstaedter
a08fd7eb52 feat(#1224): use font preferences in collections 2023-12-21 20:29:30 +01:00
Anoop M D
d268b4786a Merge pull request #1243 from chrisnagel/feat/bruno-cli/commands/description
Update bruno-cli/options-description
2023-12-20 04:24:46 +05:30
Anoop M D
4b76bf85f4 Merge pull request #1213 from bpoulaindev/bugfix/delete_running_api
bugfix(#1210): prevent collection run mapping on deleted items
2023-12-20 00:28:44 +05:30
Anoop M D
ed6f91533b Merge pull request #1176 from bpoulaindev/bugfix/docs_links
bugfix(docs_links): open external URL in browser window, remove depre…
2023-12-20 00:23:02 +05:30
Chris Nagel
7899b04c40 Update bruno-cli/options-description
- Update of the options description due to the new available formats (json/junit)
2023-12-19 08:04:56 +01:00
Anoop M D
cdddf8af76 Merge branch 'main' of github.com:usebruno/bruno 2023-12-18 22:45:19 +05:30
Anoop M D
9e44c4a95f Merge pull request #952 from amstiel/feature/system-theme
feat(#731): support theme sync with system
2023-12-18 22:34:27 +05:30
Anoop M D
3982f9c3c3 chore: bumped version to v1.5.1 2023-12-18 17:38:19 +05:30
Anoop M D
46dda28c3a Merge pull request #1233 from gyunseo/bugfix/saving-and-opening-wsl2-fs
set the polling mode based on path type to resolve wsl2 file system issue.
2023-12-18 17:34:59 +05:30
Gyunseo Lee
9f6890b769 feat: set the polling mode based on path type 2023-12-18 17:56:12 +09:00
Anoop M D
eb340d4ace chore: updated bru cli changelog 2023-12-18 04:56:25 +05:30
Anoop M D
66f917ecec chore: bumped version to v1.5.0 2023-12-18 04:22:10 +05:30
Anoop M D
e3865d4710 Merge pull request #1187 from awinder/feature/junit-reporter
Feature/junit reporter
2023-12-18 04:05:37 +05:30
Anoop M D
983fb2c4fd fix(#1215): fixed issue where runner was not displaying test results 2023-12-18 03:58:40 +05:30
Anoop M D
cb6513c580 Merge pull request #1202 from akshat-khosya/feature/clone-collection
clone functionality in collection
2023-12-18 01:26:26 +05:30
Akshat Khosya
b8451d01ca removed promise from clone collection action 2023-12-16 23:19:17 +05:30
Akshat Khosya
a15a4e4a2d corrected typo error in comments 2023-12-16 13:18:52 +05:30
Akshat Khosya
cada4f201a removed try catch from clone collection 2023-12-16 13:16:03 +05:30
Akshat Khosya
93661bd0d2 handle dir of files, comments in code 2023-12-16 13:07:51 +05:30
Anoop M D
454e0e5260 Merge pull request #1227 from TheZalRevolt/main
doc: fixed typo on italian documentation
2023-12-16 00:59:39 +05:30
Andrew Winder
647a819051 remove extraneous import 2023-12-15 12:06:36 -05:00
Andrew Winder
99c2dd9030 test updates 2023-12-15 12:04:52 -05:00
Andrew Winder
ab37e53346 refactor for reporters directory 2023-12-15 12:04:41 -05:00
Riccardo Solazzi
8ace37848b Merge pull request #1 from TheZalRevolt/bugfix/it-readme-fix-typo
bugfix: fixed typo on italian readme
2023-12-15 16:28:33 +01:00
thezal
447f40053d bugfix: fixed typo on italian readme 2023-12-15 16:25:37 +01:00
Andrew
de530a889c adding request-level error reporting 2023-12-15 10:09:20 -05:00
Andrew
882341c35b remove duplicated test suite 2023-12-15 10:09:20 -05:00
Andrew
174f99f9fb test coverage for junit reporter 2023-12-15 10:09:20 -05:00
Andrew
2103ab20bf implements a reporter flag w/ junit reporter type 2023-12-15 10:09:20 -05:00
Andrew
f0e22cb5df adds xmlwriter dependency for writing junit files 2023-12-15 10:09:20 -05:00
Anoop M D
ee2295aec1 pr #1184: addressed review comments 2023-12-15 00:55:21 +05:30
Anoop M D
2d16e07747 Merge pull request #1184 from smebberson/feature/clear-response
You can now clear a response.
2023-12-15 00:47:02 +05:30
Anoop M D
a839d311dc Merge pull request #1206 from arnaduga/main
doc: updated French doc
2023-12-15 00:30:27 +05:30
Anoop M D
82bafd5268 chore: adjusted copy icon position in code generator 2023-12-15 00:29:01 +05:30
Anoop M D
fc09697404 Merge branch 'main' of github.com:usebruno/bruno 2023-12-15 00:22:45 +05:30
Anoop M D
ee2d7a187a Merge pull request #1200 from survivant/feature/-feature/1198-copy-to-clipboard-codegen-main
Add copy to clipboard icon
2023-12-15 00:20:13 +05:30
Sebastien Dionne
f8ff305cf4 And adding style to GenCode 2023-12-12 19:54:27 -05:00
Sebastien Dionne
c2c2ef6e2b Use tabler icon 2023-12-12 17:53:37 -05:00
Baptiste POULAIN
aa18f17fb9 bugfix(#1210): prevent mapping on deleted items 2023-12-12 15:37:34 +01:00
Arnaud
baeeeb2bb0 doc: updated French doc 2023-12-11 20:09:20 +01:00
Akshat Khosya
fff3e6d88a clone functionality in collection 2023-12-11 15:57:28 +05:30
Sebastien Dionne
7953863b9d forgot to commit package-lock.json 2023-12-10 15:20:47 -05:00
Sebastien Dionne
10183319c4 forgot to commit package-lock.json 2023-12-10 15:18:01 -05:00
Sebastien Dionne
89c5fc2f03 Add copy to clipboard icon 2023-12-10 15:12:04 -05:00
Anoop M D
8ddec6bf0c Merge pull request #1172 from Zomzog/1117_sort_env
fix(#1117): Sort env alphabetically
2023-12-09 19:13:04 +05:30
Baptiste Poulain
d257db27b8 Update packages/bruno-app/src/components/MarkDown/index.jsx
Co-authored-by: Timon <39559178+Its-treason@users.noreply.github.com>
2023-12-08 16:12:22 +01:00
Scott Mebberson
08935c64bb You can now clear a response. 2023-12-08 08:35:35 +10:30
Baptiste POULAIN
8e1d04f5c1 bugfix(docs_links): reverse tsx to jsx 2023-12-07 18:09:30 +01:00
Anoop M D
da3bd95add Merge pull request #1179 from CRAZy-Monk3Y/docs-fix
docs: Add Bengali publishing.md , refactor and clean Readme.md
2023-12-07 22:07:32 +05:30
Tathagata Chakraborty
cc89e34b4c fix for #1177 2023-12-07 20:29:29 +05:30
Baptiste POULAIN
56a456a9b6 bugfix(docs_links): open external URL in browser window, remove deprecated method in electron, refactor to index.tsx 2023-12-07 14:33:42 +01:00
Zomzog
eaa448306b fix(#1117): Sort env alphabetically 2023-12-07 12:00:37 +01:00
Anoop M D
9f8dba0fb2 Merge pull request #838 from Joschasa/feature/always-indent-json-in-queryresult
Feature/always indent json in queryresult
2023-12-07 01:01:11 +05:30
Anoop M D
784f63ca5b chore: updated deps 2023-12-07 00:57:45 +05:30
André Glüpker
77cdc2179d Merge branch 'main' into feature/always-indent-json-in-queryresult 2023-12-06 17:41:17 +01:00
Anoop M D
d749e4b848 Merge pull request #1159 from fgreinacher/docs/german-readme-improvements
docs: improve german readme
2023-12-06 15:49:33 +05:30
Florian Greinacher
bb729b5793 docs: improve german readme 2023-12-06 09:20:58 +01:00
Brandon Gillis
a60f351736 feat: add custom CA Certificate preference 2023-12-06 01:04:35 +01:00
Anoop M D
0d0c4166c1 chore: release bruno cli v1.2.0 2023-12-06 02:24:13 +05:30
Anoop M D
567744c2ce chore: bumped version to 1.4.0 2023-12-06 01:49:35 +05:30
Anoop M D
cb47b7be5f Merge pull request #1135 from jaktestowac/main
fix(#1134): images paths in polish readme
2023-12-06 01:43:48 +05:30
Anoop M D
3061507284 Merge pull request #619 from mj-h/feature/add-bru-setNextRequest
feat: bru.setNextRequest()
2023-12-06 01:37:44 +05:30
Anoop M D
d3bec5631e Merge pull request #1153 from grahamwhiteuk/match-protocolregex-with-axios
refactor: protocol regex matches axios
2023-12-05 23:32:54 +05:30
Graham White
98b45a2fd4 refactor: protocol regex matches axios
Since axios is used for requests, it makes sense to match the protocol parsing
with their code at
https://github.com/axios/axios/blob/main/lib/helpers/parseProtocol.js

Closes: #1152

Signed-off-by: Graham White <graham_alton@hotmail.com>
2023-12-05 17:22:56 +00:00
jaktestowac.pl
1c83f5c885 fix: images path in readme 2023-12-04 08:54:37 +01:00
Alexey Kunitsky
1ee6b5f974 fix: wrap watcher into useEffect 2023-11-14 11:00:44 +01:00
Alexey Kunitsky
8130de23ff feat: support auto theme change according to system 2023-11-11 20:44:44 +01:00
Martin Hoecker
8183ce03c5 feat: bru.setNextRequest(null) breaks execution without error 2023-11-01 23:53:15 +01:00
Martin Hoecker
129d659628 Count the number of jumps to break out of infinite loops.
This is especially useful for the bru cli, to make sure that test-runners
that are accidentally stuck in an infinite loop still terminate in a
reasonable amount of time and don't hog up resources.
2023-11-01 23:15:54 +01:00
Martin Hoecker
db0de68987 Merge branch 'main' into feature/add-bru-setNextRequest 2023-11-01 22:59:39 +01:00
André Glüpker
631e436079 Revert "JSON in QueryResult should always be indented"
This reverts commit 76a26b634d.
2023-11-01 17:32:57 +01:00
André Glüpker
dc32d7246c Use the body to check for a json content type 2023-11-01 16:32:20 +01:00
André Glüpker
76a26b634d JSON in QueryResult should always be indented
Some APIs return the wrong Content-Type 'application/html', but valid
JSON as payload. In this cases the data is still typeof object and a
indented JSON makes it easier to work with.

However JSON folding and highlighting will still be off in this case.
2023-10-31 11:58:09 +01:00
André Glüpker
820e3033ea Remove unused conditions in QueryResult
The mode is returned by getCodeMirrorModeBasedOnContentType(),
which always prefixes the returned values with 'application/'.

However returning data on those application/html or application/text
break Bruno.
2023-10-31 11:55:45 +01:00
Brian Dentino
d76253ea04 Fixes for getNextRequest in UI 2023-10-26 22:46:35 +02:00
Martin Hoecker
4a1d45f458 Merge branch 'main' into feature/add-bru-setNextRequest 2023-10-26 22:34:05 +02:00
Martin Hoecker
3374db1ac8 Merge branch 'main' into feature/add-bru-setNextRequest 2023-10-24 22:38:44 +02:00
Martin Hoecker
d4c0207545 feat: bru.setNextRequest() 2023-10-16 17:29:26 +02:00
146 changed files with 6224 additions and 2503 deletions

2
.gitignore vendored
View File

@@ -6,6 +6,8 @@ yarn.lock
pnpm-lock.yaml
.pnp
.pnp.js
bun.lockb
bun.lock
# testing
coverage

View File

@@ -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) | [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.

View 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

View File

@@ -1,4 +1,4 @@
[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 !
@@ -23,23 +23,11 @@ Les librairies 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érer à 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 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
## Développement
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
- NodeJS v18
@@ -80,6 +68,7 @@ done
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

View File

@@ -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

View 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

View 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

View File

@@ -1,4 +1,4 @@
[English](/publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | **Polski**
[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

View File

@@ -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.

View File

@@ -1,4 +1,4 @@
[English](/publishing.md) | [Português (BR)](/docs/publishing/publishing_pt_br.md) | **Română**
[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

View 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),
ık kaynak ilkeleri ile sürdürülebilirlik arasında uyumlu bir denge kurmaya çalışıyoruz - https://github.com/usebruno/bruno/discussions/269

View File

@@ -10,7 +10,7 @@
[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](../../readme.md) | [Українська](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
View File

@@ -0,0 +1,131 @@
<br />
<img src="../../assets/images/logo-transparent.png" width="80"/>
### Bruno - 开源IDE用于探索和测试API。
[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%bruno)
[![CI](https://github.com/usebruno/bruno/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)
[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)
[![网站](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![下载](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](../../readme.md) | [Українська](./readme_ua.md) | [Русский](./readme_ru.md) | [Türkçe](./readme_tr.md) | [Deutsch](./readme_de.md) | [Français](./readme_fr.md) | [Português (BR)](./readme_pt_br.md) | [한국어](./readme_kr.md) | [বাংলা](./readme_bn.md) | [Español](./readme_es.md) | [Italiano](./readme_it.md) | [Română](./readme_ro.md) | [Polski](./readme_pl.md) | [简体中文](./readme_cn.md)
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)
![bruno](../../assets/images/landing-2.png) <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 安装 🖥️
![bruno](../../assets/images/run-anywhere.png) <br /><br />
### Collaborate 安装 👩‍💻🧑‍💻
或者任何你选择的版本控制系统
![bruno](../../assets/images/version-control.png) <br /><br />
### 重要链接 📌
- [我们的愿景](https://github.com/usebruno/bruno/discussions/269)
- [路线图](https://github.com/usebruno/bruno/discussions/384)
- [文档](https://docs.usebruno.com)
- [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)

View File

@@ -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).
![bruno](/assets/images/landing-2.png) <br /><br />
@@ -26,9 +26,9 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno eine Cloud-Synchr
![bruno](/assets/images/run-anywhere.png) <br /><br />
### Zusammenarbeiten mit Git 👩‍💻🧑‍💻
### Zusammenarbeit mit Git 👩‍💻🧑‍💻
oder eine Versionskontrolle Deiner Wahl
Oder einer Versionskontrolle deiner Wahl
![bruno](/assets/images/version-control.png) <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

View File

@@ -11,7 +11,7 @@
[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | [Українська](/readme_ua.md) | [Русский](/readme_ru.md) | [Türkçe](/readme_tr.md) | [Deutsch](/readme_de.md) | **Français** | [বাংলা](docs/readme/readme_bn.md)
[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)
![bruno](/assets/images/landing-2.png) <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 🖥️
![bruno](/assets/images/run-anywhere.png) <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 🎥

View File

@@ -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.

View File

@@ -22,7 +22,7 @@ Bruno działa tylko w trybie offline. Nie planujemy nigdy dodawać synchronizacj
📢 Obejrzyj naszą ostatnią rozmowę na konferencji India FOSS 3.0 [tutaj](https://www.youtube.com/watch?v=7bSMFpbcPiY)
![bruno](assets/images/landing-2.png) <br /><br />
![bruno](/assets/images/landing-2.png) <br /><br />
### Instalacja
@@ -56,13 +56,13 @@ sudo apt install bruno
### Uruchom na wielu platformach 🖥️
![bruno](assets/images/run-anywhere.png) <br /><br />
![bruno](/assets/images/run-anywhere.png) <br /><br />
### Współpracuj przez Git 👩‍💻🧑‍💻
Lub dowolny inny system kontroli wersji, który wybierzesz
![bruno](assets/images/version-control.png) <br /><br />
![bruno](/assets/images/version-control.png) <br /><br />
### Ważne Linki 📌

View File

@@ -10,23 +10,55 @@
[![Web Sitesi](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)
[![İndir](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)
[English](/readme.md) | [Українська](/readme_ua.md) | [Русский](/readme_ru.md) | **Türkçe** | [Deutsch](/readme_de.md) | [Français](/readme_fr.md) | [বাংলা](docs/readme/readme_bn.md)
[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)
![bruno](/assets/images/landing-2.png) <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 🖥️
![bruno](/assets/images/run-anywhere.png) <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)

4477
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,11 +5,13 @@
"packages/bruno-app",
"packages/bruno-electron",
"packages/bruno-cli",
"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",
@@ -17,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",

View File

@@ -1,5 +1,6 @@
{
"compilerOptions": {
"jsx": "react",
"target": "es2017",
"allowSyntheticDefaultImports": false,
"baseUrl": "./",

View File

@@ -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",
@@ -55,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",

View File

@@ -60,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();
@@ -103,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() {
@@ -118,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',
@@ -209,7 +216,7 @@ export default class CodeEditor extends React.Component {
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();
}
@@ -299,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);

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -1,7 +1,7 @@
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, useSelector } from 'react-redux';
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';

View File

@@ -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 || '');

View File

@@ -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>
);

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useRef } from 'react';
import get from 'lodash/get';
import { useFormik } from 'formik';
import { useSelector, useDispatch } from 'react-redux';
@@ -6,13 +6,21 @@ 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()
@@ -31,6 +39,10 @@ const General = ({ close }) => {
const formik = useFormik({
initialValues: {
sslVerification: preferences.request.sslVerification,
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)
@@ -52,6 +64,10 @@ const General = ({ close }) => {
...preferences,
request: {
sslVerification: newPreferences.sslVerification,
customCaCertificate: {
enabled: newPreferences.customCaCertificate.enabled,
filePath: newPreferences.customCaCertificate.filePath
},
timeout: newPreferences.timeout,
storeCookies: newPreferences.storeCookies,
sendCookies: newPreferences.sendCookies
@@ -64,6 +80,14 @@ 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}>
@@ -80,6 +104,60 @@ const General = ({ close }) => {
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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
);
};

View File

@@ -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);
}}
>

View File

@@ -39,7 +39,7 @@ const formatResponse = (data, mode, filter) => {
return safeStringifyJSON(parsed, true);
}
if (['text', 'html'].includes(mode) || typeof data === 'string') {
if (typeof data === 'string') {
return data;
}
@@ -48,7 +48,7 @@ const formatResponse = (data, mode, filter) => {
const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEventListener, headers, error }) => {
const contentType = getContentType(headers);
const mode = getCodeMirrorModeBasedOnContentType(contentType);
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
const [filter, setFilter] = useState(null);
const formattedData = formatResponse(data, mode, filter);
const { storedTheme } = useTheme();

View File

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

View File

@@ -0,0 +1,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;

View File

@@ -21,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>

View File

@@ -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();
@@ -114,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} />

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
</>
);
};

View File

@@ -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) => {

View File

@@ -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;

View 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;

View File

@@ -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.3.2</div>
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.6.1</div>
</div>
</div>
</div>

View File

@@ -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;
}

View File

@@ -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};

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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({ collectionUid: collection.uid, nextAction: null }));
dispatch(
addTab({
uid: item.uid,
collectionUid: collection.uid,
requestPaneTab: getDefaultRequestPaneTab(item.type)
})
);
dispatch(hideHomePage());
}
}
});
}, [collections, each, dispatch, updateNextAction, hideHomePage, addTab]);
};
export default useCollectionNextAction;

View File

@@ -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();
};

View File

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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
export const taskTypes = {
OPEN_REQUEST: 'OPEN_REQUEST'
};

View File

@@ -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,6 +56,15 @@ export const appSlice = createSlice({
},
updateCookies: (state, action) => {
state.cookies = action.payload;
},
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 = [];
}
}
});
@@ -63,7 +78,10 @@ export const {
hideHomePage,
showPreferences,
updatePreferences,
updateCookies
updateCookies,
insertTaskIntoQueue,
removeTaskFromQueue,
removeAllTasksFromQueue
} = appSlice.actions;
export const savePreferences = (preferences) => (dispatch, getState) => {
@@ -91,4 +109,9 @@ export const deleteCookiesForDomain = (domain) => (dispatch, getState) => {
});
};
export const completeQuitFlow = () => (dispatch, getState) => {
const { ipcRenderer } = window;
return ipcRenderer.invoke('main:complete-quit-flow');
};
export default appSlice.reducer;

View File

@@ -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,6 +84,38 @@ 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);
@@ -595,7 +621,6 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
urlParam.enabled = true;
});
const collectionCopy = cloneDeep(collection);
const item = {
uid: uuid(),
type: requestType,
@@ -632,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 {
@@ -662,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 {
@@ -939,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;

View File

@@ -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;
@@ -269,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);
@@ -1224,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') {
@@ -1383,7 +1381,6 @@ export const {
removeCollection,
sortCollections,
updateLastAction,
updateNextAction,
updateSettingsSelectedTab,
collectionUnlinkEnvFileEvent,
saveEnvironment,
@@ -1396,6 +1393,7 @@ export const {
processEnvUpdateEvent,
requestCancelled,
responseReceived,
responseCleared,
saveRequest,
deleteRequestDraft,
newEphemeralHttpRequest,

View File

@@ -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

View File

@@ -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,

View File

@@ -173,6 +173,11 @@ const darkTheme = {
color: '#a5a5a5',
bg: '#626262',
border: '#626262'
},
danger: {
color: '#fff',
bg: '#dc3545',
border: '#dc3545'
}
},

View File

@@ -177,6 +177,11 @@ const lightTheme = {
color: '#9f9f9f',
bg: '#efefef',
border: 'rgb(234, 234, 234)'
},
danger: {
color: '#fff',
bg: '#dc3545',
border: '#dc3545'
}
},

View 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);
}
}
}
}

View File

@@ -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);
};

View File

@@ -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';
}

View File

@@ -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`;
};

View File

@@ -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

View File

@@ -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.3",
"@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"
}
}

View File

@@ -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}`)));
}

View 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;

View File

@@ -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

View File

@@ -17,7 +17,7 @@ const { SocksProxyAgent } = require('socks-proxy-agent');
const { makeAxiosInstance } = require('../utils/axios-instance');
const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');
const protocolRegex = /([a-zA-Z]{2,20}:\/\/)(.*)/;
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
const runSingleRequest = async function (
filename,
@@ -31,6 +31,7 @@ const runSingleRequest = async function (
) {
try {
let request;
let nextRequestName;
request = prepareRequest(bruJson.request, collectionRoot);
@@ -68,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,
@@ -78,6 +79,9 @@ const runSingleRequest = async function (
processEnvVars,
scriptingConfig
);
if (result?.nextRequestName !== undefined) {
nextRequestName = result.nextRequestName;
}
}
// interpolate variables inside request
@@ -211,7 +215,8 @@ const runSingleRequest = async function (
},
error: err.message,
assertionResults: [],
testResults: []
testResults: [],
nextRequestName: nextRequestName
};
}
}
@@ -245,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,
@@ -256,6 +261,9 @@ const runSingleRequest = async function (
processEnvVars,
scriptingConfig
);
if (result?.nextRequestName !== undefined) {
nextRequestName = result.nextRequestName;
}
}
// run assertions
@@ -327,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})`));

View 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');
});
});

22
packages/bruno-common/.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
# dependencies
node_modules
yarn.lock
pnpm-lock.yaml
package-lock.json
.pnp
.pnp.js
# testing
coverage
# production
dist
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node'
};

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Anoop M D, Anusree P S and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,33 @@
{
"name": "@usebruno/common",
"version": "0.1.0",
"license": "MIT",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"src",
"package.json"
],
"scripts": {
"clean": "rimraf dist",
"test": "jest",
"prebuild": "npm run clean",
"build": "rollup -c",
"prepack": "npm run test && npm run build"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
"rollup": "3.2.5",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.8.4"
},
"overrides": {
"rollup": "3.2.5"
}
}

View File

@@ -0,0 +1,9 @@
# bruno-common
A collection of common utilities used across Bruno App, Electron and CLI packages.
### Publish to Npm Registry
```bash
npm publish --access=public
```

View File

@@ -0,0 +1,40 @@
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const commonjs = require('@rollup/plugin-commonjs');
const typescript = require('@rollup/plugin-typescript');
const dts = require('rollup-plugin-dts');
const { terser } = require('rollup-plugin-terser');
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
const packageJson = require('./package.json');
module.exports = [
{
input: 'src/index.ts',
output: [
{
file: packageJson.main,
format: 'cjs',
sourcemap: true
},
{
file: packageJson.module,
format: 'esm',
sourcemap: true
}
],
plugins: [
peerDepsExternal(),
nodeResolve({
extensions: ['.css']
}),
commonjs(),
typescript({ tsconfig: './tsconfig.json' }),
terser()
]
},
{
input: 'dist/esm/index.d.ts',
output: [{ file: 'dist/index.d.ts', format: 'esm' }],
plugins: [dts.default()]
}
];

View File

@@ -0,0 +1,5 @@
import interpolate from './interpolate';
export default {
interpolate
};

View File

@@ -0,0 +1,84 @@
import interpolate from './index';
describe('interpolate', () => {
it('should replace placeholders with values from the object', () => {
const inputString = 'Hello, my name is {{user.name}} and I am {{user.age}} years old';
const inputObject = {
'user.name': 'Bruno',
user: {
age: 4
}
};
const result = interpolate(inputString, inputObject);
expect(result).toBe('Hello, my name is Bruno and I am 4 years old');
});
it('should handle missing values by leaving the placeholders unchanged using {{}} as delimiters', () => {
const inputString = 'Hello, my name is {{user.name}} and I am {{user.age}} years old';
const inputObject = {
user: {
name: 'Bruno'
}
};
const result = interpolate(inputString, inputObject);
expect(result).toBe('Hello, my name is Bruno and I am {{user.age}} years old');
});
it('should handle all valid keys', () => {
const inputObject = {
user: {
full_name: 'Bruno',
age: 4,
'fav-food': ['egg', 'meat'],
'want.attention': true
}
};
const inputStr = `
Hi, I am {{user.full_name}},
I am {{user.age}} years old.
My favorite food is {{user.fav-food[0]}} and {{user.fav-food[1]}}.
I like attention: {{user.want.attention}}
`;
const expectedStr = `
Hi, I am Bruno,
I am 4 years old.
My favorite food is egg and meat.
I like attention: true
`;
const result = interpolate(inputStr, inputObject);
expect(result).toBe(expectedStr);
});
it('should strictly match the keys (whitespace matters)', () => {
const inputString = 'Hello, my name is {{ user.name }} and I am {{user.age}} years old';
const inputObject = {
'user.name': 'Bruno',
user: {
age: 4
}
};
const result = interpolate(inputString, inputObject);
expect(result).toBe('Hello, my name is {{ user.name }} and I am 4 years old');
});
it('should give precedence to the last key in case of duplicates', () => {
const inputString = 'Hello, my name is {{user.name}} and I am {{user.age}} years old';
const inputObject = {
'user.name': 'Bruno',
user: {
name: 'Not Bruno',
age: 4
}
};
const result = interpolate(inputString, inputObject);
expect(result).toBe('Hello, my name is Not Bruno and I am 4 years old');
});
});

View File

@@ -0,0 +1,31 @@
/**
* The interpolation function expects a string with placeholders and an object with the values to replace the placeholders.
* The keys passed can have dot notation too.
*
* Ex: interpolate('Hello, my name is ${user.name} and I am ${user.age} years old', {
* "user.name": "Bruno",
* "user": {
* "age": 4
* }
* });
* Output: Hello, my name is Bruno and I am 4 years old
*/
import { flattenObject } from '../utils';
const interpolate = (str: string, obj: Record<string, any>): string => {
if (!str || typeof str !== 'string' || !obj || typeof obj !== 'object') {
return str;
}
const patternRegex = /\{\{([^}]+)\}\}/g;
const flattenedObj = flattenObject(obj);
const result = str.replace(patternRegex, (match, placeholder) => {
const replacement = flattenedObj[placeholder];
return replacement !== undefined ? replacement : match;
});
return result;
};
export default interpolate;

View File

@@ -0,0 +1,39 @@
import { flattenObject } from './index';
describe('flattenObject', () => {
it('should flatten a simple object', () => {
const input = { a: 1, b: { c: 2, d: { e: 3 } } };
const output = flattenObject(input);
expect(output).toEqual({ a: 1, 'b.c': 2, 'b.d.e': 3 });
});
it('should flatten an object with arrays', () => {
const input = { a: 1, b: { c: [2, 3, 4], d: { e: 5 } } };
const output = flattenObject(input);
expect(output).toEqual({ a: 1, 'b.c[0]': 2, 'b.c[1]': 3, 'b.c[2]': 4, 'b.d.e': 5 });
});
it('should flatten an object with arrays having objects', () => {
const input = { a: 1, b: { c: [{ d: 2 }, { e: 3 }], f: { g: 4 } } };
const output = flattenObject(input);
expect(output).toEqual({ a: 1, 'b.c[0].d': 2, 'b.c[1].e': 3, 'b.f.g': 4 });
});
it('should handle null values', () => {
const input = { a: 1, b: { c: null, d: { e: 3 } } };
const output = flattenObject(input);
expect(output).toEqual({ a: 1, 'b.c': null, 'b.d.e': 3 });
});
it('should handle an empty object', () => {
const input = {};
const output = flattenObject(input);
expect(output).toEqual({});
});
it('should handle an object with nested empty objects', () => {
const input = { a: { b: {}, c: { d: {} } } };
const output = flattenObject(input);
expect(output).toEqual({});
});
});

View File

@@ -0,0 +1,11 @@
export const flattenObject = (obj: Record<string, any>, parentKey: string = ''): Record<string, any> => {
return Object.entries(obj).reduce((acc: Record<string, any>, [key, value]: [string, any]) => {
const newKey = parentKey ? (Array.isArray(obj) ? `${parentKey}[${key}]` : `${parentKey}.${key}`) : key;
if (typeof value === 'object' && value !== null) {
Object.assign(acc, flattenObject(value, newKey));
} else {
acc[newKey] = value;
}
return acc;
}, {});
};

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES6",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"jsx": "react",
"module": "ESNext",
"declaration": true,
"declarationDir": "types",
"sourceMap": true,
"outDir": "dist",
"moduleResolution": "node",
"emitDeclarationOnly": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
},
"exclude": ["dist", "node_modules", "tests"]
}

View File

@@ -1,5 +1,5 @@
{
"version": "v1.3.2",
"version": "v1.6.1",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",
@@ -20,7 +20,7 @@
},
"dependencies": {
"@aws-sdk/credential-providers": "^3.425.0",
"@usebruno/js": "0.9.3",
"@usebruno/js": "0.9.4",
"@usebruno/lang": "0.9.0",
"@usebruno/schema": "0.6.0",
"about-window": "^1.15.2",
@@ -52,7 +52,7 @@
"socks-proxy-agent": "^8.0.2",
"tough-cookie": "^4.1.3",
"uuid": "^9.0.0",
"vm2": "^3.9.13",
"@n8n/vm2": "^3.9.23",
"yup": "^0.32.11"
},
"optionalDependencies": {

View File

@@ -412,7 +412,7 @@ class Watcher {
setTimeout(() => {
const watcher = chokidar.watch(watchPath, {
ignoreInitial: false,
usePolling: false,
usePolling: watchPath.startsWith("\\\\") ? true : false,
ignored: (path) => ['node_modules', '.git'].some((s) => path.includes(s)),
persistent: true,
ignorePermissionErrors: true,

View File

@@ -1,7 +1,7 @@
const path = require('path');
const isDev = require('electron-is-dev');
const { format } = require('url');
const { BrowserWindow, app, Menu } = require('electron');
const { BrowserWindow, app, Menu, ipcMain } = require('electron');
const { setContentSecurityPolicy } = require('electron-util');
const menuTemplate = require('./app/menu-template');
@@ -18,7 +18,7 @@ const lastOpenedCollections = new LastOpenedCollections();
const contentSecurityPolicy = [
"default-src 'self'",
"script-src * 'unsafe-inline' 'unsafe-eval'",
"connect-src 'self' api.github.com app.posthog.com",
"connect-src * 'unsafe-inline'",
"font-src 'self' https:",
"form-action 'none'",
"img-src 'self' blob: data: https:",
@@ -97,10 +97,21 @@ app.on('ready', async () => {
mainWindow.on('maximize', () => saveMaximized(true));
mainWindow.on('unmaximize', () => saveMaximized(false));
mainWindow.webContents.on('new-window', function (e, url) {
mainWindow.on('close', (e) => {
e.preventDefault();
require('electron').shell.openExternal(url);
ipcMain.emit('main:start-quit-flow');
});
mainWindow.webContents.on('will-redirect', (event, url) => {
event.preventDefault();
if (/^(http:\/\/|https:\/\/)/.test(url)) {
require('electron').shell.openExternal(url);
}
});
mainWindow.webContents.setWindowOpenHandler((details) => {
require('electron').shell.openExternal(details.url);
return { action: 'deny' };
});
// register all ipc handlers

View File

@@ -1,7 +1,7 @@
const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const { ipcMain, shell, dialog } = require('electron');
const { ipcMain, shell, dialog, app } = require('electron');
const { envJsonToBru, bruToJson, jsonToBru, jsonToCollectionBru } = require('../bru');
const {
@@ -70,7 +70,52 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
}
);
// clone collection
ipcMain.handle(
'renderer:clone-collection',
async (event, collectionName, collectionFolderName, collectionLocation, previousPath) => {
const dirPath = path.join(collectionLocation, collectionFolderName);
if (fs.existsSync(dirPath)) {
throw new Error(`collection: ${dirPath} already exists`);
}
if (!isValidPathname(dirPath)) {
throw new Error(`collection: invalid pathname - ${dir}`);
}
// create dir
await createDirectory(dirPath);
const uid = generateUidBasedOnHash(dirPath);
// open the bruno.json of previousPath
const brunoJsonFilePath = path.join(previousPath, 'bruno.json');
const content = fs.readFileSync(brunoJsonFilePath, 'utf8');
//Change new name of collection
let json = JSON.parse(content);
json.name = collectionName;
const cont = await stringifyJson(json);
// write the bruno.json to new dir
await writeFile(path.join(dirPath, 'bruno.json'), cont);
// Now copy all the files with extension name .bru along with there dir
const files = searchForBruFiles(previousPath);
for (const sourceFilePath of files) {
const relativePath = path.relative(previousPath, sourceFilePath);
const newFilePath = path.join(dirPath, relativePath);
// handle dir of files
fs.mkdirSync(path.dirname(newFilePath), { recursive: true });
// copy each files
fs.copyFileSync(sourceFilePath, newFilePath);
}
mainWindow.webContents.send('main:collection-opened', dirPath, uid, json);
ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);
}
);
// rename collection
ipcMain.handle('renderer:rename-collection', async (event, newName, collectionPathname) => {
try {
@@ -133,6 +178,25 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
// save multiple requests
ipcMain.handle('renderer:save-multiple-requests', async (event, requestsToSave) => {
try {
for (let r of requestsToSave) {
const request = r.item;
const pathname = r.pathname;
if (!fs.existsSync(pathname)) {
throw new Error(`path: ${pathname} does not exist`);
}
const content = jsonToBru(request);
await writeFile(pathname, content);
}
} catch (error) {
return Promise.reject(error);
}
});
// create environment
ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name, variables) => {
try {
@@ -540,6 +604,15 @@ const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) =
watcher.addWatcher(win, pathname, uid);
lastOpenedCollections.add(pathname);
});
// The app listen for this event and allows the user to save unsaved requests before closing the app
ipcMain.on('main:start-quit-flow', () => {
mainWindow.webContents.send('main:start-quit-flow');
});
ipcMain.handle('main:complete-quit-flow', () => {
mainWindow.destroy();
});
};
const registerCollectionsIpc = (mainWindow, watcher, lastOpenedCollections) => {

View File

@@ -72,7 +72,7 @@ const getEnvVars = (environment = {}) => {
};
};
const protocolRegex = /([a-zA-Z]{2,20}:\/\/)(.*)/;
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
const configureRequest = async (
collectionUid,
@@ -86,11 +86,22 @@ const configureRequest = async (
request.url = `http://${request.url}`;
}
const httpsAgentRequestFields = {};
/**
* @see https://github.com/usebruno/bruno/issues/211 set keepAlive to true, this should fix socket hang up errors
* @see https://github.com/nodejs/node/pull/43522 keepAlive was changed to true globally on Node v19+
*/
const httpsAgentRequestFields = { keepAlive: true };
if (!preferencesUtil.shouldVerifyTls()) {
httpsAgentRequestFields['rejectUnauthorized'] = false;
}
if (preferencesUtil.shouldUseCustomCaCertificate()) {
const caCertFilePath = preferencesUtil.getCustomCaCertificateFilePath();
if (caCertFilePath) {
httpsAgentRequestFields['ca'] = fs.readFileSync(caCertFilePath);
}
}
const brunoConfig = getBrunoConfig(collectionUid);
const interpolationOptions = {
envVars,
@@ -258,10 +269,11 @@ const registerNetworkIpc = (mainWindow) => {
}
// run pre-request script
let scriptResult;
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(os.EOL);
if (requestScript?.length) {
const scriptRuntime = new ScriptRuntime();
const result = await scriptRuntime.runRequestScript(
scriptResult = await scriptRuntime.runRequestScript(
decomment(requestScript),
request,
envVars,
@@ -273,8 +285,8 @@ const registerNetworkIpc = (mainWindow) => {
);
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
envVariables: scriptResult.envVariables,
collectionVariables: scriptResult.collectionVariables,
requestUid,
collectionUid
});
@@ -293,6 +305,8 @@ const registerNetworkIpc = (mainWindow) => {
if (request.headers['content-type'] === 'application/x-www-form-urlencoded') {
request.data = qs.stringify(request.data);
}
return scriptResult;
};
const runPostResponse = async (
@@ -332,12 +346,13 @@ const registerNetworkIpc = (mainWindow) => {
}
// run post-response script
let scriptResult;
const responseScript = compact([get(collectionRoot, 'request.script.res'), get(request, 'script.res')]).join(
os.EOL
);
if (responseScript?.length) {
const scriptRuntime = new ScriptRuntime();
const result = await scriptRuntime.runResponseScript(
scriptResult = await scriptRuntime.runResponseScript(
decomment(responseScript),
request,
response,
@@ -350,12 +365,13 @@ const registerNetworkIpc = (mainWindow) => {
);
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
envVariables: scriptResult.envVariables,
collectionVariables: scriptResult.collectionVariables,
requestUid,
collectionUid
});
}
return scriptResult;
};
// handler for sending http request
@@ -696,7 +712,11 @@ const registerNetworkIpc = (mainWindow) => {
});
}
for (let item of folderRequests) {
let currentRequestIndex = 0;
let nJumps = 0; // count the number of jumps to avoid infinite loops
while (currentRequestIndex < folderRequests.length) {
const item = folderRequests[currentRequestIndex];
let nextRequestName;
const itemUid = item.uid;
const eventData = {
collectionUid,
@@ -718,7 +738,7 @@ const registerNetworkIpc = (mainWindow) => {
const processEnvVars = getProcessEnvVars(collectionUid);
try {
await runPreRequest(
const preRequestScriptResult = await runPreRequest(
request,
requestUid,
envVars,
@@ -730,6 +750,10 @@ const registerNetworkIpc = (mainWindow) => {
scriptingConfig
);
if (preRequestScriptResult?.nextRequestName !== undefined) {
nextRequestName = preRequestScriptResult.nextRequestName;
}
// todo:
// i have no clue why electron can't send the request object
// without safeParseJSON(safeStringifyJSON(request.data))
@@ -805,7 +829,7 @@ const registerNetworkIpc = (mainWindow) => {
}
}
await runPostResponse(
const postRequestScriptResult = await runPostResponse(
request,
response,
requestUid,
@@ -818,6 +842,10 @@ const registerNetworkIpc = (mainWindow) => {
scriptingConfig
);
if (postRequestScriptResult?.nextRequestName !== undefined) {
nextRequestName = postRequestScriptResult.nextRequestName;
}
// run assertions
const assertions = get(item, 'request.assertions');
if (assertions) {
@@ -878,6 +906,24 @@ const registerNetworkIpc = (mainWindow) => {
...eventData
});
}
if (nextRequestName !== undefined) {
nJumps++;
if (nJumps > 10000) {
throw new Error('Too many jumps, possible infinite loop');
}
if (nextRequestName === null) {
break;
}
const nextRequestIdx = folderRequests.findIndex((request) => request.name === nextRequestName);
if (nextRequestIdx >= 0) {
currentRequestIndex = nextRequestIdx;
} else {
console.error("Could not find request with name '" + nextRequestName + "'");
currentRequestIndex++;
}
} else {
currentRequestIndex++;
}
}
mainWindow.webContents.send('main:run-folder-event', {

View File

@@ -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,9 +45,11 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
return str;
}
// Handlebars doesn't allow dots as identifiers, so we need to use literal segments
const strLiteralSegment = str.replace('{{', '{{[').replace('}}', ']}}');
const template = Handlebars.compile(strLiteralSegment, { noEscape: true });
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
const combinedVars = {

View File

@@ -11,6 +11,10 @@ const { get } = require('lodash');
const defaultPreferences = {
request: {
sslVerification: true,
customCaCertificate: {
enabled: false,
filePath: null
},
storeCookies: true,
sendCookies: true,
timeout: 0
@@ -35,6 +39,10 @@ const defaultPreferences = {
const preferencesSchema = Yup.object().shape({
request: Yup.object().shape({
sslVerification: Yup.boolean(),
customCaCertificate: Yup.object({
enabled: Yup.boolean(),
filePath: Yup.string().nullable()
}),
storeCookies: Yup.boolean(),
sendCookies: Yup.boolean(),
timeout: Yup.number()
@@ -100,6 +108,12 @@ const preferencesUtil = {
shouldVerifyTls: () => {
return get(getPreferences(), 'request.sslVerification', true);
},
shouldUseCustomCaCertificate: () => {
return get(getPreferences(), 'request.customCaCertificate.enabled', false);
},
getCustomCaCertificateFilePath: () => {
return get(getPreferences(), 'request.customCaCertificate.filePath', null);
},
getRequestTimeout: () => {
return get(getPreferences(), 'request.timeout', 0);
},

View File

@@ -0,0 +1,130 @@
const interpolateVars = require('../../src/ipc/network/interpolate-vars');
describe('interpolate-vars: interpolateVars', () => {
describe('Interpolates string', () => {
describe('With environment variables', () => {
it("If there's a var with only alphanumeric characters in its name", async () => {
const request = { method: 'GET', url: '{{testUrl1}}' };
const result = interpolateVars(request, { testUrl1: 'test.com' }, null, null);
expect(result.url).toEqual('test.com');
});
it("If there's a var with a '.' in its name", async () => {
const request = { method: 'GET', url: '{{test.url}}' };
const result = interpolateVars(request, { 'test.url': 'test.com' }, null, null);
expect(result.url).toEqual('test.com');
});
it("If there's a var with a '-' in its name", async () => {
const request = { method: 'GET', url: '{{test-url}}' };
const result = interpolateVars(request, { 'test-url': 'test.com' }, null, null);
expect(result.url).toEqual('test.com');
});
it("If there's a var with a '_' in its name", async () => {
const request = { method: 'GET', url: '{{test_url}}' };
const result = interpolateVars(request, { test_url: 'test.com' }, null, null);
expect(result.url).toEqual('test.com');
});
it('If there are multiple variables', async () => {
const body =
'{\n "firstElem": {{body-var-1}},\n "secondElem": [{{body.var.2}}],\n "thirdElem": {\n "fourthElem": {{body_var_3}},\n "{{varAsKey}}": {{valueForKey}} }}';
const expectedBody =
'{\n "firstElem": Test1,\n "secondElem": [Test2],\n "thirdElem": {\n "fourthElem": Test3,\n "TestKey": TestValueForKey }}';
const request = { method: 'POST', url: 'test', data: body, headers: { 'content-type': 'json' } };
const result = interpolateVars(
request,
{
'body-var-1': 'Test1',
'body.var.2': 'Test2',
body_var_3: 'Test3',
varAsKey: 'TestKey',
valueForKey: 'TestValueForKey'
},
null,
null
);
expect(result.data).toEqual(expectedBody);
});
});
describe('With process environment variables', () => {
/*
* It should NOT turn process env vars into literal segments.
* Otherwise, Handlebars will try to access the var literally
*/
it("If there's a var that starts with 'process.env.'", async () => {
const request = { method: 'GET', url: '{{process.env.TEST_VAR}}' };
const result = interpolateVars(request, null, null, { TEST_VAR: 'test.com' });
expect(result.url).toEqual('test.com');
});
});
});
describe('Does NOT interpolate string', () => {
describe('With environment variables', () => {
it('If the var is escaped', async () => {
const request = { method: 'GET', url: `\\{{test.url}}` };
const result = interpolateVars(request, { 'test.url': 'test.com' }, null, null);
expect(result.url).toEqual('{{test.url}}');
});
it("If it's not a var (no braces)", async () => {
const request = { method: 'GET', url: 'test' };
const result = interpolateVars(request, { 'test.url': 'test.com' }, null, null);
expect(result.url).toEqual('test');
});
it("If it's not a var (only 1 set of braces)", async () => {
const request = { method: 'GET', url: '{test.url}' };
const result = interpolateVars(request, { 'test.url': 'test.com' }, null, null);
expect(result.url).toEqual('{test.url}');
});
it("If it's not a var (1 opening & 2 closing braces)", async () => {
const request = { method: 'GET', url: '{test.url}}' };
const result = interpolateVars(request, { 'test.url': 'test.com' }, null, null);
expect(result.url).toEqual('{test.url}}');
});
it('If there are no variables (multiple)', async () => {
let gqlBody = `{"query":"mutation {\\n test(input: { native: { firstElem: \\"{should-not-get-interpolated}\\", secondElem: \\"{should-not-get-interpolated}}"}}) {\\n __typename\\n ... on TestType {\\n id\\n identifier\\n }\\n }\\n}","variables":"{}"}`;
const request = { method: 'POST', url: 'test', data: gqlBody };
const result = interpolateVars(request, { 'should-not-get-interpolated': 'ERROR' }, null, null);
expect(result.data).toEqual(gqlBody);
});
});
describe('With process environment variables', () => {
it("If there's a var that doesn't start with 'process.env.'", async () => {
const request = { method: 'GET', url: '{{process-env-TEST_VAR}}' };
const result = interpolateVars(request, null, null, { TEST_VAR: 'test.com' });
expect(result.url).toEqual('');
});
});
});
describe('Throws an error', () => {
it("If there's a var with an invalid character in its name", async () => {
'!@#%^&*()[{]}=+,<>;\\|'.split('').forEach((character) => {
const request = { method: 'GET', url: `{{test${character}Url}}` };
expect(() => interpolateVars(request, { [`test${character}Url`]: 'test.com' }, null, null)).toThrow(
/Parse error.*/
);
});
});
});
});

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