mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
86 Commits
fix/sideba
...
fix/oauth-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
148d3f0e7d | ||
|
|
75e17610f0 | ||
|
|
154c45d87d | ||
|
|
0bf169562b | ||
|
|
967b073ded | ||
|
|
725dfeacac | ||
|
|
923d26ce56 | ||
|
|
7e258003d5 | ||
|
|
7689288763 | ||
|
|
81faa57808 | ||
|
|
bac9616de4 | ||
|
|
9ab1ed3d90 | ||
|
|
408c9d4a4e | ||
|
|
ebafdd813c | ||
|
|
6642f4d0b0 | ||
|
|
4f75474c87 | ||
|
|
e5b7aa5ab4 | ||
|
|
875df38501 | ||
|
|
a724f010ff | ||
|
|
f9423d1238 | ||
|
|
51e36519f7 | ||
|
|
bd0894ede0 | ||
|
|
b1e6a707bf | ||
|
|
c51381888a | ||
|
|
8b1b18cc39 | ||
|
|
707ed63be6 | ||
|
|
bc0bb64400 | ||
|
|
4c110900c1 | ||
|
|
65ed6d3cfb | ||
|
|
7b28b05bc1 | ||
|
|
b0f27d01b9 | ||
|
|
3351bf990a | ||
|
|
07fff423bb | ||
|
|
36d10ab480 | ||
|
|
c918c679d7 | ||
|
|
7e3386b1b8 | ||
|
|
f4162e1ce6 | ||
|
|
e6a48a73bf | ||
|
|
fceb99edc2 | ||
|
|
ebc105d42e | ||
|
|
32d56f6942 | ||
|
|
e4a1fca3b1 | ||
|
|
59ff9bdafb | ||
|
|
071ee9ab2e | ||
|
|
176646f983 | ||
|
|
d76a574c51 | ||
|
|
734ee16fe1 | ||
|
|
33594bdcec | ||
|
|
2acfe60a5f | ||
|
|
aecaab84dd | ||
|
|
45264bfcc5 | ||
|
|
b01b8d7bc4 | ||
|
|
58a38ac5a1 | ||
|
|
7328988e59 | ||
|
|
39a6fc837d | ||
|
|
5b1b1b5541 | ||
|
|
578fa72dc8 | ||
|
|
4708e8e589 | ||
|
|
2a9386ef6b | ||
|
|
9483dbf4af | ||
|
|
0b436e2c9f | ||
|
|
9005e17eb5 | ||
|
|
efe94d9c90 | ||
|
|
848825f16a | ||
|
|
4d60425a05 | ||
|
|
c03fe301f8 | ||
|
|
c8371410c2 | ||
|
|
dcbf38bf61 | ||
|
|
a57ecde1d0 | ||
|
|
791843174e | ||
|
|
1174f22d88 | ||
|
|
8300abe086 | ||
|
|
a3809ce4b9 | ||
|
|
adb46110dd | ||
|
|
7cc4c0993e | ||
|
|
1030d02ac7 | ||
|
|
d616be7271 | ||
|
|
afd49d146f | ||
|
|
97e43c4489 | ||
|
|
f9af22d586 | ||
|
|
8590bacd79 | ||
|
|
a7d1a349e3 | ||
|
|
d03d8f01a1 | ||
|
|
97c700beba | ||
|
|
b6a27bc66c | ||
|
|
76a2889206 |
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -58,6 +58,8 @@ jobs:
|
||||
run: npm run test --workspace=packages/bruno-converters
|
||||
- name: Test Package bruno-electron
|
||||
run: npm run test --workspace=packages/bruno-electron
|
||||
- name: Test Package bruno-requests
|
||||
run: npm run test --workspace=packages/bruno-requests
|
||||
|
||||
cli-test:
|
||||
name: CLI Tests
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -48,6 +48,7 @@ yarn-error.log*
|
||||
bruno.iml
|
||||
.idea
|
||||
.vscode
|
||||
.cursor
|
||||
|
||||
# Playwright
|
||||
/blob-report/
|
||||
|
||||
BIN
assets/images/landing-2-dark.png
Normal file
BIN
assets/images/landing-2-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 346 KiB |
BIN
assets/images/landing-2-light.png
Normal file
BIN
assets/images/landing-2-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 347 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 813 KiB After Width: | Height: | Size: 584 KiB |
@@ -3,7 +3,7 @@
|
||||
|
||||
### برونو - بيئة تطوير مفتوحة المصدر لاستكشاف واختبار واجهات برمجة التطبيقات (APIs).
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### ব্রুনো - API অন্বেষণ এবং পরীক্ষা করার জন্য ওপেনসোর্স IDE।
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - 开源 IDE,用于探索和测试 API。
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Opensource IDE zum Erkunden und Testen von APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE de código abierto para explorar y probar APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### برونو یا Bruno - محیط توسعه متن باز برای تست و توسعه API ها
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE Opensource pour explorer et tester des APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Opensource IDE per esplorare e testare gli APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - API の検証・動作テストのためのオープンソース IDE.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### ბრუნო - ღია წყაროების IDE API-ების შესწავლისა და ტესტირებისათვის.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - API 탐색 및 테스트를 위한 오픈소스 IDE.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Open source IDE voor het verkennen en testen van API's.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Otwartoźródłowe IDE do eksploracji i testów APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE de código aberto para explorar e testar APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - Mediu integrat de dezvoltare cu sursă deschisă pentru explorarea și testarea API-urilor.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE с открытым исходным кодом для изучения и тестирования API.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - API'leri keşfetmek ve test etmek için açık kaynaklı IDE.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - IDE із відкритим кодом для тестування та дослідження API
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
### Bruno - 探索和測試 API 的開源 IDE 工具
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
|
||||
520
package-lock.json
generated
520
package-lock.json
generated
@@ -29,7 +29,7 @@
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@opencollection/types": "~0.6.0",
|
||||
"@opencollection/types": "~0.7.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",
|
||||
@@ -6100,9 +6100,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@opencollection/types": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.6.0.tgz",
|
||||
"integrity": "sha512-nasB4/1hIZ61xp2dnnZWhdH83f0t800VrSl3G2q+BtHabBqN/IG+j9BMOJg0hYZjAVx+Yhl1njkzUqkiX5+Q0g==",
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@opencollection/types/-/types-0.7.0.tgz",
|
||||
"integrity": "sha512-CSwdaHNPa2bNNBAOy++t6W9gBTExUJZW3aPkWyhAjasusThbvjymD/0uCLR50gCXSs0ezv61jsd19m9x+2DMtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -6458,56 +6458,6 @@
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@postman/form-data": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@postman/form-data/-/form-data-3.1.1.tgz",
|
||||
"integrity": "sha512-vjh8Q2a8S6UCm/KKs31XFJqEEgmbjBmpPNVV2eVav6905wyFAwaUOBGA1NPBI4ERH9MMZc6w0umFgM6WbEPMdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@postman/tough-cookie": {
|
||||
"version": "4.1.3-postman.1",
|
||||
"resolved": "https://registry.npmjs.org/@postman/tough-cookie/-/tough-cookie-4.1.3-postman.1.tgz",
|
||||
"integrity": "sha512-txpgUqZOnWYnUHZpHjkfb0IwVH4qJmyq77pPnJLlfhMtdCLMFTEeQHlzQiK906aaNCe4NEB5fGJHo9uzGbFMeA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"psl": "^1.1.33",
|
||||
"punycode": "^2.1.1",
|
||||
"universalify": "^0.2.0",
|
||||
"url-parse": "^1.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@postman/tough-cookie/node_modules/universalify": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@postman/tunnel-agent": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@postman/tunnel-agent/-/tunnel-agent-0.6.4.tgz",
|
||||
"integrity": "sha512-CJJlq8V7rNKhAw4sBfjixKpJW00SHqebqNUQKxMoepgeWZIbdPcD+rguRcivGhS4N12PymDcKgUgSD4rVC+RjQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@prantlf/jsonlint": {
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@prantlf/jsonlint/-/jsonlint-16.0.0.tgz",
|
||||
@@ -11429,15 +11379,6 @@
|
||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asn1.js": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
|
||||
@@ -11476,6 +11417,7 @@
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
@@ -11626,15 +11568,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-sign2": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||
"integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/aws4": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz",
|
||||
@@ -12028,15 +11961,6 @@
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/big.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||
@@ -12229,15 +12153,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brotli": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
|
||||
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/browserify-aes": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
|
||||
@@ -12750,12 +12665,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/caseless": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz",
|
||||
@@ -14375,18 +14284,6 @@
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
|
||||
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
|
||||
@@ -15110,22 +15007,6 @@
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ecc-jsbn/node_modules/jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
@@ -16424,12 +16305,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extract-files": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz",
|
||||
@@ -16555,6 +16430,7 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-levenshtein": {
|
||||
@@ -17037,15 +16913,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/fork-ts-checker-webpack-plugin": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz",
|
||||
@@ -17524,15 +17391,6 @@
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/getpass": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
|
||||
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/github-markdown-css": {
|
||||
"version": "5.8.1",
|
||||
"resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.8.1.tgz",
|
||||
@@ -17815,57 +17673,12 @@
|
||||
"@grpc/grpc-js": "^1.12.6"
|
||||
}
|
||||
},
|
||||
"node_modules/har-schema": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
|
||||
"integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/har-validator": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
|
||||
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
|
||||
"deprecated": "this library is no longer supported",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.3",
|
||||
"har-schema": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/har-validator-compiled": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/har-validator-compiled/-/har-validator-compiled-1.0.0.tgz",
|
||||
"integrity": "sha512-dher7nFSx+Ef6OoqVveLClh8itAR3vd8Qx70Lh/hEgP1iGeARAolbci7Y8JBrHIYgFCT6xRdvvL16AR9Zh07Dw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/har-validator/node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/har-validator/node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -18338,20 +18151,6 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/http-signature": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz",
|
||||
"integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"jsprim": "^2.0.2",
|
||||
"sshpk": "^1.14.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/http2-wrapper": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
|
||||
@@ -19168,12 +18967,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-valid-path": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-valid-path/-/is-valid-path-0.1.1.tgz",
|
||||
@@ -19236,12 +19029,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isstream": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
@@ -20822,12 +20609,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/json-schema": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
|
||||
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
|
||||
"license": "(AFL-2.1 OR BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
@@ -20851,7 +20632,9 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"license": "ISC"
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
@@ -20892,44 +20675,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/jsprim": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
|
||||
"integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "1.0.0",
|
||||
"extsprintf": "1.3.0",
|
||||
"json-schema": "0.4.0",
|
||||
"verror": "1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jsprim/node_modules/extsprintf": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsprim/node_modules/verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
@@ -22030,15 +21775,6 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mustache": "bin/mustache"
|
||||
}
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||
@@ -22259,44 +21995,6 @@
|
||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-vault": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/node-vault/-/node-vault-0.10.2.tgz",
|
||||
"integrity": "sha512-//uc9/YImE7Dx0QHdwMiAzLaOumiKUnOUP8DymgtkZ8nsq6/V2LKvEu6kw91Lcruw8lWUfj4DO7CIXNPRWBuuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4",
|
||||
"mustache": "^4.2.0",
|
||||
"postman-request": "^2.88.1-postman.33",
|
||||
"tv4": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-vault/node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-vault/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -22367,15 +22065,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oauth-sign": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -23090,12 +22779,6 @@
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -24075,57 +23758,6 @@
|
||||
"node": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postman-request": {
|
||||
"version": "2.88.1-postman.40",
|
||||
"resolved": "https://registry.npmjs.org/postman-request/-/postman-request-2.88.1-postman.40.tgz",
|
||||
"integrity": "sha512-uE4AiIqhjtHKp4pj9ei7fkdfNXEX9IqDBlK1plGAQne6y79UUlrTdtYLhwXoO0AMOvqyl9Ar+BU6Eo6P/MPgfg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@postman/form-data": "~3.1.1",
|
||||
"@postman/tough-cookie": "~4.1.3-postman.1",
|
||||
"@postman/tunnel-agent": "^0.6.4",
|
||||
"aws-sign2": "~0.7.0",
|
||||
"aws4": "^1.12.0",
|
||||
"brotli": "^1.3.3",
|
||||
"caseless": "~0.12.0",
|
||||
"combined-stream": "~1.0.6",
|
||||
"extend": "~3.0.2",
|
||||
"forever-agent": "~0.6.1",
|
||||
"har-validator": "~5.1.3",
|
||||
"http-signature": "~1.3.1",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"oauth-sign": "~0.9.0",
|
||||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.5.3",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"stream-length": "^1.0.2",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
}
|
||||
},
|
||||
"node_modules/postman-request/node_modules/qs": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
|
||||
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/postman-request/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -24427,6 +24059,7 @@
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
||||
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
@@ -24471,6 +24104,7 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -27386,37 +27020,6 @@
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/sshpk": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
|
||||
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asn1": "~0.2.3",
|
||||
"assert-plus": "^1.0.0",
|
||||
"bcrypt-pbkdf": "^1.0.0",
|
||||
"dashdash": "^1.12.0",
|
||||
"ecc-jsbn": "~0.1.1",
|
||||
"getpass": "^0.1.1",
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.0.2",
|
||||
"tweetnacl": "~0.14.0"
|
||||
},
|
||||
"bin": {
|
||||
"sshpk-conv": "bin/sshpk-conv",
|
||||
"sshpk-sign": "bin/sshpk-sign",
|
||||
"sshpk-verify": "bin/sshpk-verify"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sshpk/node_modules/jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stable": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
||||
@@ -27587,21 +27190,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-length": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stream-length/-/stream-length-1.0.2.tgz",
|
||||
"integrity": "sha512-aI+qKFiwoDV4rsXiS7WRoCt+v2RX1nUj17+KJC5r2gfh5xoSJIfP6Y3Do/HtvesFcTSWthIuJ3l1cvKQY/+nZg==",
|
||||
"license": "WTFPL",
|
||||
"dependencies": {
|
||||
"bluebird": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-length/node_modules/bluebird": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
|
||||
"integrity": "sha512-UfFSr22dmHPQqPP9XWHRhq+gWnHCYguQGkXQlbyPtW5qTnhFWA8/iXg765tH0cAjy7l/zPJ1aBTO0g5XgA7kvQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
@@ -29184,12 +28772,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -29454,6 +29036,7 @@
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"devOptional": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
@@ -30477,7 +30060,7 @@
|
||||
"polished": "^4.3.1",
|
||||
"posthog-node": "4.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"qs": "^6.11.0",
|
||||
"qs": "^6.14.1",
|
||||
"query-string": "^7.0.1",
|
||||
"react": "19.0.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
@@ -30492,6 +30075,7 @@
|
||||
"react-player": "^2.16.0",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"react-virtuoso": "^4.18.1",
|
||||
"sass": "^1.46.0",
|
||||
"semver": "^7.7.1",
|
||||
"shell-quote": "^1.8.3",
|
||||
@@ -31932,6 +31516,31 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/react-virtuoso": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.18.1.tgz",
|
||||
"integrity": "sha512-KF474cDwaSb9+SJ380xruBB4P+yGWcVkcu26HtMqYNMTYlYbrNy8vqMkE+GpAApPPufJqgOLMoWMFG/3pJMXUA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16 || >=17 || >= 18 || >= 19",
|
||||
"react-dom": ">=16 || >=17 || >= 18 || >=19"
|
||||
}
|
||||
},
|
||||
"packages/bruno-app/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
@@ -32017,7 +31626,7 @@
|
||||
"iconv-lite": "^0.6.3",
|
||||
"js-yaml": "^4.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"qs": "^6.11.0",
|
||||
"qs": "^6.14.1",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"xmlbuilder": "^15.1.1",
|
||||
"yargs": "^17.6.2"
|
||||
@@ -33067,6 +32676,21 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"packages/bruno-cli/node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"packages/bruno-common": {
|
||||
"name": "@usebruno/common",
|
||||
"version": "0.1.0",
|
||||
@@ -33081,6 +32705,7 @@
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"babel-jest": "^29.7.0",
|
||||
"form-data": "^4.0.0",
|
||||
"is-ip": "^5.0.1",
|
||||
"moment": "^2.29.4",
|
||||
"rollup": "3.29.5",
|
||||
@@ -33651,9 +33276,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-converters/node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -33756,7 +33381,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"nanoid": "3.3.8",
|
||||
"qs": "^6.11.0",
|
||||
"qs": "^6.14.1",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"uuid": "^9.0.0",
|
||||
@@ -35242,6 +34867,21 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"packages/bruno-electron/node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"packages/bruno-electron/node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
@@ -35491,7 +35131,6 @@
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "3.3.8",
|
||||
"node-fetch": "^2.7.0",
|
||||
"node-vault": "^0.10.2",
|
||||
"path": "^0.12.7",
|
||||
"quickjs-emscripten": "^0.29.2",
|
||||
"tv4": "^1.3.0",
|
||||
@@ -35606,7 +35245,10 @@
|
||||
"debug": "^4.4.3",
|
||||
"google-protobuf": "^4.0.0",
|
||||
"grpc-js-reflection-client": "^1.3.0",
|
||||
"http-proxy-agent": "~7.0.2",
|
||||
"https-proxy-agent": "~7.0.6",
|
||||
"is-ip": "^5.0.1",
|
||||
"socks-proxy-agent": "~8.0.5",
|
||||
"system-ca": "^2.0.1",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"ws": "^8.18.3"
|
||||
@@ -35706,9 +35348,7 @@
|
||||
"version": "0.7.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "3.3.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"nanoid": "3.3.8",
|
||||
"yup": "^0.32.11"
|
||||
}
|
||||
},
|
||||
@@ -36003,4 +35643,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@opencollection/types": "~0.6.0",
|
||||
"@opencollection/types": "~0.7.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",
|
||||
@@ -78,6 +78,7 @@
|
||||
"build:electron:rpm": "./scripts/build-electron.sh rpm",
|
||||
"build:electron:snap": "./scripts/build-electron.sh snap",
|
||||
"watch:common": "npm run watch --workspace=packages/bruno-common",
|
||||
"watch:requests": "npm run watch --workspace=packages/bruno-requests",
|
||||
"test:codegen": "node playwright/codegen.ts",
|
||||
"test:e2e": "playwright test --project=default",
|
||||
"test:e2e:ssl": "playwright test --project=ssl",
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"polished": "^4.3.1",
|
||||
"posthog-node": "4.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"qs": "^6.11.0",
|
||||
"qs": "^6.14.1",
|
||||
"query-string": "^7.0.1",
|
||||
"react": "19.0.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
@@ -84,6 +84,7 @@
|
||||
"react-player": "^2.16.0",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"react-virtuoso": "^4.18.1",
|
||||
"sass": "^1.46.0",
|
||||
"semver": "^7.7.1",
|
||||
"shell-quote": "^1.8.3",
|
||||
|
||||
@@ -18,9 +18,9 @@ import ImportWorkspace from 'components/WorkspaceSidebar/ImportWorkspace';
|
||||
|
||||
import IconBottombarToggle from 'components/Icons/IconBottombarToggle/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { toTitleCase } from 'utils/common/index';
|
||||
import ResponseLayoutToggle from 'components/ResponsePane/ResponseLayoutToggle';
|
||||
import { isMacOS, isWindowsOS, isLinuxOS } from 'utils/common/platform';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const getOsClass = () => {
|
||||
if (isMacOS()) return 'os-mac';
|
||||
@@ -29,6 +29,12 @@ const getOsClass = () => {
|
||||
return 'os-other';
|
||||
};
|
||||
|
||||
// Helper to get display name for workspace
|
||||
export const getWorkspaceDisplayName = (name) => {
|
||||
if (!name) return 'Untitled Workspace';
|
||||
return name;
|
||||
};
|
||||
|
||||
const AppTitleBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||
@@ -115,7 +121,7 @@ const AppTitleBar = () => {
|
||||
const WorkspaceName = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="workspace-name-container" {...props}>
|
||||
<span className="workspace-name">{toTitleCase(activeWorkspace?.name) || 'Default Workspace'}</span>
|
||||
<span data-testid="workspace-name" className={classNames('workspace-name', { 'italic text-muted': !activeWorkspace?.name })}>{getWorkspaceDisplayName(activeWorkspace?.name)}</span>
|
||||
<IconChevronDown size={14} stroke={1.5} className="chevron-icon" />
|
||||
</div>
|
||||
);
|
||||
@@ -127,7 +133,7 @@ const AppTitleBar = () => {
|
||||
|
||||
const handleWorkspaceSwitch = (workspaceUid) => {
|
||||
dispatch(switchWorkspace(workspaceUid));
|
||||
toast.success(`Switched to ${workspaces.find((w) => w.uid === workspaceUid)?.name}`);
|
||||
toast.success(`Switched to ${getWorkspaceDisplayName(workspaces.find((w) => w.uid === workspaceUid)?.name)}`);
|
||||
};
|
||||
|
||||
const handleOpenWorkspace = async () => {
|
||||
@@ -178,7 +184,7 @@ const AppTitleBar = () => {
|
||||
|
||||
return {
|
||||
id: workspace.uid,
|
||||
label: toTitleCase(workspace.name),
|
||||
label: getWorkspaceDisplayName(workspace.name),
|
||||
onClick: () => handleWorkspaceSwitch(workspace.uid),
|
||||
className: `workspace-item ${isActive ? 'active' : ''}`,
|
||||
rightSection: (
|
||||
@@ -190,11 +196,7 @@ const AppTitleBar = () => {
|
||||
label={isPinned ? 'Unpin workspace' : 'Pin workspace'}
|
||||
size="sm"
|
||||
>
|
||||
{isPinned ? (
|
||||
<IconPinned size={14} stroke={1.5} />
|
||||
) : (
|
||||
<IconPin size={14} stroke={1.5} />
|
||||
)}
|
||||
{isPinned ? <IconPinned size={14} stroke={1.5} /> : <IconPin size={14} stroke={1.5} />}
|
||||
</ActionIcon>
|
||||
)}
|
||||
{isActive && <IconCheck size={16} stroke={1.5} className="check-icon" />}
|
||||
@@ -247,12 +249,7 @@ const AppTitleBar = () => {
|
||||
<div className="titlebar-content">
|
||||
{/* Left section: Home + Workspace */}
|
||||
<div className="titlebar-left">
|
||||
<ActionIcon
|
||||
onClick={handleHomeClick}
|
||||
label="Home"
|
||||
size="lg"
|
||||
className="home-button"
|
||||
>
|
||||
<ActionIcon onClick={handleHomeClick} label="Home" size="lg" className="home-button">
|
||||
<IconHome size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import React from 'react';
|
||||
import { isEqual, escapeRegExp } from 'lodash';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import { setupAutoComplete } from 'utils/codemirror/autocomplete';
|
||||
import { setupAutoComplete, showRootHints } from 'utils/codemirror/autocomplete';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as jsonlint from '@prantlf/jsonlint';
|
||||
import { JSHINT } from 'jshint';
|
||||
@@ -111,8 +111,12 @@ export default class CodeEditor extends React.Component {
|
||||
: cm.replaceSelection(' ', 'end');
|
||||
},
|
||||
'Shift-Tab': 'indentLess',
|
||||
'Ctrl-Space': 'autocomplete',
|
||||
'Cmd-Space': 'autocomplete',
|
||||
'Ctrl-Space': (cm) => {
|
||||
showRootHints(cm, this.props.showHintsFor);
|
||||
},
|
||||
'Cmd-Space': (cm) => {
|
||||
showRootHints(cm, this.props.showHintsFor);
|
||||
},
|
||||
'Ctrl-Y': 'foldAll',
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
|
||||
@@ -259,10 +259,6 @@ const StyledWrapper = styled.div`
|
||||
height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
pre {
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
.w-full.h-full.relative.flex {
|
||||
height: 100% !important;
|
||||
@@ -321,7 +317,7 @@ const StyledWrapper = styled.div`
|
||||
height: 100% !important;
|
||||
max-height: 400px !important;
|
||||
padding: 0.5rem !important;
|
||||
|
||||
|
||||
.network-logs-pre {
|
||||
color: ${(props) => props.theme.console.messageColor} !important;
|
||||
font-size: ${(props) => props.theme.font.size.xs} !important;
|
||||
|
||||
@@ -59,7 +59,7 @@ const CreateEnvironment = ({ collection, onClose, onEnvironmentCreated }) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="sm"
|
||||
size="md"
|
||||
title="Create Environment"
|
||||
confirmText="Create"
|
||||
handleConfirm={onSubmit}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useRef, useMemo, useEffect } from 'react';
|
||||
import { TableVirtuoso } from 'react-virtuoso';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { get } from 'lodash';
|
||||
import { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons';
|
||||
@@ -18,14 +19,28 @@ import { getGlobalEnvironmentVariables, flattenItems, isItemARequest } from 'uti
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning';
|
||||
import { sensitiveFields } from './constants';
|
||||
|
||||
const TableRow = React.memo(({ children, item }) => <tr key={item.uid} data-testid={`env-var-row-${item.name}`}>{children}</tr>, (prevProps, nextProps) => {
|
||||
const prevUid = prevProps?.item?.uid;
|
||||
const nextUid = nextProps?.item?.uid;
|
||||
return prevUid === nextUid && prevProps.children === nextProps.children;
|
||||
});
|
||||
|
||||
const MIN_H = 35 * 2; // 2 rows worth of height
|
||||
|
||||
const EnvironmentVariables = ({ environment, setIsModified, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
|
||||
const [tableHeight, setTableHeight] = React.useState(MIN_H);
|
||||
|
||||
const environmentsDraft = collection?.environmentsDraft;
|
||||
const hasDraftForThisEnv = environmentsDraft?.environmentUid === environment.uid;
|
||||
|
||||
const handleTotalHeightChanged = React.useCallback((h) => {
|
||||
setTableHeight(h);
|
||||
}, []);
|
||||
|
||||
// Track environment changes for draft restoration
|
||||
const prevEnvUidRef = React.useRef(null);
|
||||
const mountedRef = React.useRef(false);
|
||||
@@ -384,111 +399,114 @@ const EnvironmentVariables = ({ environment, setIsModified, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td className="text-center"></td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td className="text-center">Secret</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formik.values.map((variable, index) => {
|
||||
const isLastRow = index === formik.values.length - 1;
|
||||
const isEmptyRow = !variable.name || variable.name.trim() === '';
|
||||
const isLastEmptyRow = isLastRow && isEmptyRow;
|
||||
<TableVirtuoso
|
||||
className="table-container"
|
||||
style={{ height: tableHeight }}
|
||||
components={{ TableRow }}
|
||||
data={formik.values}
|
||||
totalListHeightChanged={handleTotalHeightChanged}
|
||||
fixedHeaderContent={() => (
|
||||
<tr>
|
||||
<td className="text-center"></td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td className="text-center">Secret</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
)}
|
||||
fixedItemHeight={35}
|
||||
computeItemKey={(index, variable) => variable.uid}
|
||||
itemContent={(index, variable) => {
|
||||
const isLastRow = index === formik.values.length - 1;
|
||||
const isEmptyRow = !variable.name || variable.name.trim() === '';
|
||||
const isLastEmptyRow = isLastRow && isEmptyRow;
|
||||
|
||||
return (
|
||||
<tr key={variable.uid} data-testid={`env-var-row-${variable.name}`}>
|
||||
<td className="text-center">
|
||||
{!isLastEmptyRow && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
placeholder={isLastEmptyRow ? 'Name' : ''}
|
||||
onChange={(e) => handleNameChange(index, e)}
|
||||
onBlur={() => handleNameBlur(index)}
|
||||
onKeyDown={(e) => handleNameKeyDown(index, e)}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} index={index} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<MultiLineEditor
|
||||
theme={storedTheme}
|
||||
collection={_collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
isSecret={variable.secret}
|
||||
readOnly={typeof variable.value !== 'string'}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
{typeof variable.value !== 'string' && (
|
||||
<span className="ml-2 flex items-center">
|
||||
<IconInfoCircle id={`${variable.uid}-disabled-info-icon`} className="text-muted" size={16} />
|
||||
<Tooltip
|
||||
anchorId={`${variable.uid}-disabled-info-icon`}
|
||||
content="Non-string values set via scripts are read-only and can only be updated through scripts."
|
||||
place="top"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{!variable.secret && hasSensitiveUsage(variable.name) && (
|
||||
<SensitiveFieldWarning
|
||||
fieldName={variable.name}
|
||||
warningMessage="This variable is used in sensitive fields. Mark it as a secret for security"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{!isLastEmptyRow && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{!isLastEmptyRow && (
|
||||
<button onClick={() => handleRemoveVar(variable.uid)}>
|
||||
<IconTrash strokeWidth={1.5} size={18} />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<td className="text-center">
|
||||
{!isLastEmptyRow && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
placeholder={isLastEmptyRow ? 'Name' : ''}
|
||||
onChange={(e) => handleNameChange(index, e)}
|
||||
onBlur={() => handleNameBlur(index)}
|
||||
onKeyDown={(e) => handleNameKeyDown(index, e)}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} index={index} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<MultiLineEditor
|
||||
theme={storedTheme}
|
||||
collection={_collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
isSecret={variable.secret}
|
||||
readOnly={typeof variable.value !== 'string'}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
{typeof variable.value !== 'string' && (
|
||||
<span className="ml-2 flex items-center">
|
||||
<IconInfoCircle id={`${variable.uid}-disabled-info-icon`} className="text-muted" size={16} />
|
||||
<Tooltip
|
||||
anchorId={`${variable.uid}-disabled-info-icon`}
|
||||
content="Non-string values set via scripts are read-only and can only be updated through scripts."
|
||||
place="top"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{!variable.secret && hasSensitiveUsage(variable.name) && (
|
||||
<SensitiveFieldWarning
|
||||
fieldName={variable.name}
|
||||
warningMessage="This variable is used in sensitive fields. Mark it as a secret for security"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{!isLastEmptyRow && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{!isLastEmptyRow && (
|
||||
<button onClick={() => handleRemoveVar(variable.uid)}>
|
||||
<IconTrash strokeWidth={1.5} size={18} />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="button-container">
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -191,9 +191,12 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.environment-name-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
@@ -210,12 +213,14 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-left: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.creating {
|
||||
.environment-name-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
@@ -232,6 +237,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-left: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
|
||||
import { updateFolderAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { updateFolderAuth as _updateFolderAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
|
||||
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
|
||||
@@ -20,7 +20,7 @@ import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
|
||||
import { humanizeRequestAuthMode, getTreePathFromCollectionToItem } from 'utils/collections/index';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const GrantTypeComponentMap = ({ collection, folder }) => {
|
||||
const GrantTypeComponentMap = ({ collection, folder, updateFolderAuth }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const save = () => {
|
||||
@@ -90,6 +90,13 @@ const Auth = ({ collection, folder }) => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
const updateFolderAuth = ({ itemUid, ...rest }) => {
|
||||
return _updateFolderAuth({
|
||||
...rest,
|
||||
folderUid: folder.uid
|
||||
});
|
||||
};
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'basic': {
|
||||
@@ -178,7 +185,7 @@ const Auth = ({ collection, folder }) => {
|
||||
collection={collection}
|
||||
item={folder}
|
||||
/>
|
||||
<GrantTypeComponentMap collection={collection} folder={folder} />
|
||||
<GrantTypeComponentMap collection={collection} folder={folder} updateFolderAuth={updateFolderAuth} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ const ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
}, [apikeyAuth]);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<StyledWrapper className="w-full">
|
||||
<label className="block mb-1">Key</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
|
||||
@@ -52,7 +52,7 @@ const BasicAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<StyledWrapper className="w-full">
|
||||
<label className="block mb-1">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
|
||||
@@ -38,7 +38,7 @@ const BearerAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<StyledWrapper className="w-full">
|
||||
<label className="block mb-1">Token</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
|
||||
@@ -73,7 +73,7 @@ const GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center gap-2.5 my-4">
|
||||
<div className="flex items-center gap-2.5 mb-4">
|
||||
<div className="flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md">
|
||||
<IconKey size={14} className="oauth2-icon" />
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ const OAuth2 = ({ item, collection }) => {
|
||||
let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {});
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<StyledWrapper className="w-full">
|
||||
<GrantTypeSelector item={item} request={request} updateAuth={updateAuth} collection={collection} />
|
||||
<GrantTypeComponentMap item={item} collection={collection} />
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -52,7 +52,7 @@ const WsseAuth = ({ item, collection, updateAuth, request, save }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<StyledWrapper className="w-full">
|
||||
<label className="block mb-1">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
|
||||
@@ -6,7 +6,6 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.method-dropdown-trigger {
|
||||
|
||||
@@ -60,7 +60,7 @@ const StyledWrapper = styled.div`
|
||||
|
||||
.proto-file-dropdown-reflection-message {
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: ${(props) => props.theme.overlay.overlay1};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -300,7 +300,7 @@ const GrpcQueryUrl = ({ item, collection, handleRun }) => {
|
||||
<span className="text-xs font-medium" style={{ color: theme.request.grpc }}>gRPC</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center w-full input-container h-full relative">
|
||||
<div className="flex items-center w-full input-container h-full relative overflow-auto">
|
||||
<SingleLineEditor
|
||||
ref={editorRef}
|
||||
value={url}
|
||||
@@ -313,117 +313,118 @@ const GrpcQueryUrl = ({ item, collection, handleRun }) => {
|
||||
item={item}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="flex items-center h-full mx-2 gap-3" id="send-request">
|
||||
<MethodDropdown
|
||||
grpcMethods={grpcMethods}
|
||||
selectedGrpcMethod={selectedGrpcMethod}
|
||||
onMethodSelect={handleGrpcMethodSelect}
|
||||
onMethodDropdownCreate={onMethodDropdownCreate}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 gap-3" id="send-request">
|
||||
<ProtoFileDropdown
|
||||
collection={collection}
|
||||
item={item}
|
||||
isReflectionMode={isReflectionMode}
|
||||
protoFilePath={protoFilePath}
|
||||
showProtoDropdown={showProtoDropdown}
|
||||
setShowProtoDropdown={setShowProtoDropdown}
|
||||
onProtoDropdownCreate={onProtoDropdownCreate}
|
||||
onReflectionModeToggle={handleReflectionModeToggle}
|
||||
onProtoFileLoad={handleProtoFileLoad}
|
||||
<ProtoFileDropdown
|
||||
collection={collection}
|
||||
item={item}
|
||||
isReflectionMode={isReflectionMode}
|
||||
protoFilePath={protoFilePath}
|
||||
showProtoDropdown={showProtoDropdown}
|
||||
setShowProtoDropdown={setShowProtoDropdown}
|
||||
onProtoDropdownCreate={onProtoDropdownCreate}
|
||||
onReflectionModeToggle={handleReflectionModeToggle}
|
||||
onProtoFileLoad={handleProtoFileLoad}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="infotip"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isReflectionMode) {
|
||||
handleReflection(url, true);
|
||||
} else if (protoFilePath) {
|
||||
handleProtoFileLoad(protoFilePath, true);
|
||||
} else {
|
||||
toast.error('No proto file selected');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconRefresh
|
||||
color={theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
className={`${(isReflectionMode ? reflectionManagement.isLoadingMethods : protoFileManagement.isLoadingMethods) ? 'animate-spin' : 'cursor-pointer'}`}
|
||||
data-testid="refresh-methods-icon"
|
||||
/>
|
||||
|
||||
<div
|
||||
className="infotip"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isReflectionMode) {
|
||||
handleReflection(url, true);
|
||||
} else if (protoFilePath) {
|
||||
handleProtoFileLoad(protoFilePath, true);
|
||||
} else {
|
||||
toast.error('No proto file selected');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconRefresh
|
||||
color={theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
className={`${(isReflectionMode ? reflectionManagement.isLoadingMethods : protoFileManagement.isLoadingMethods) ? 'animate-spin' : 'cursor-pointer'}`}
|
||||
data-testid="refresh-methods-icon"
|
||||
/>
|
||||
<span className="infotip-text text-xs">
|
||||
{isReflectionMode ? 'Refresh server reflection' : 'Refresh proto file methods'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="infotip"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleGrpcurl(url);
|
||||
}}
|
||||
>
|
||||
<IconCode
|
||||
color={theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
/>
|
||||
<span className="infotip-text text-xs">Generate grpcurl command</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="infotip"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!item.draft) return;
|
||||
onSave();
|
||||
}}
|
||||
>
|
||||
<IconDeviceFloppy
|
||||
color={item.draft ? theme.draftColor : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
className={`${item.draft ? 'cursor-pointer' : 'cursor-default'}`}
|
||||
/>
|
||||
<span className="infotip-text text-xs">
|
||||
Save <span className="shortcut">({saveShortcut})</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isConnectionActive && isStreamingMethod && (
|
||||
<div className="connection-controls relative flex items-center h-full gap-3">
|
||||
<div className="infotip" onClick={handleCancelConnection} data-testid="grpc-cancel-connection-button">
|
||||
<IconX color={theme.requestTabs.icon.color} strokeWidth={1.5} size={20} className="cursor-pointer" />
|
||||
<span className="infotip-text text-xs">Cancel</span>
|
||||
</div>
|
||||
|
||||
{isClientStreamingMethod && (
|
||||
<div onClick={handleEndConnection} data-testid="grpc-end-connection-button">
|
||||
<IconCheck
|
||||
color={theme.colors.text.green}
|
||||
strokeWidth={2}
|
||||
size={20}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!isConnectionActive || !isStreamingMethod) && (
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
data-testid="grpc-send-request-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRun(e);
|
||||
}}
|
||||
>
|
||||
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={20} />
|
||||
</div>
|
||||
)}
|
||||
<span className="infotip-text text-xs">
|
||||
{isReflectionMode ? 'Refresh server reflection' : 'Refresh proto file methods'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="infotip"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleGrpcurl(url);
|
||||
}}
|
||||
>
|
||||
<IconCode
|
||||
color={theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
/>
|
||||
<span className="infotip-text text-xs">Generate grpcurl command</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="infotip"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!item.draft) return;
|
||||
onSave();
|
||||
}}
|
||||
>
|
||||
<IconDeviceFloppy
|
||||
color={item.draft ? theme.draftColor : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
className={`${item.draft ? 'cursor-pointer' : 'cursor-default'}`}
|
||||
/>
|
||||
<span className="infotip-text text-xs">
|
||||
Save <span className="shortcut">({saveShortcut})</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isConnectionActive && isStreamingMethod && (
|
||||
<div className="connection-controls relative flex items-center h-full gap-3">
|
||||
<div className="infotip" onClick={handleCancelConnection} data-testid="grpc-cancel-connection-button">
|
||||
<IconX color={theme.requestTabs.icon.color} strokeWidth={1.5} size={20} className="cursor-pointer" />
|
||||
<span className="infotip-text text-xs">Cancel</span>
|
||||
</div>
|
||||
|
||||
{isClientStreamingMethod && (
|
||||
<div onClick={handleEndConnection} data-testid="grpc-end-connection-button">
|
||||
<IconCheck
|
||||
color={theme.colors.text.green}
|
||||
strokeWidth={2}
|
||||
size={20}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!isConnectionActive || !isStreamingMethod) && (
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
data-testid="grpc-send-request-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRun(e);
|
||||
}}
|
||||
>
|
||||
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={20} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isConnectionActive && isStreamingMethod && (
|
||||
<div className="connection-status-strip"></div>
|
||||
|
||||
@@ -76,6 +76,9 @@ const GrpcAuth = ({ item, collection }) => {
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'none': {
|
||||
return <div>No Auth</div>;
|
||||
}
|
||||
case 'basic': {
|
||||
return <BasicAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;
|
||||
}
|
||||
@@ -98,7 +101,7 @@ const GrpcAuth = ({ item, collection }) => {
|
||||
if (source && supportedGrpcAuthModes.includes(source.auth?.mode)) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div className="flex flex-row w-full gap-2">
|
||||
<div>Auth inherited from {source.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
|
||||
</div>
|
||||
@@ -107,7 +110,7 @@ const GrpcAuth = ({ item, collection }) => {
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div className="flex flex-row w-full gap-2">
|
||||
<div>Inherited auth not supported by gRPC. Using no auth instead.</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -122,9 +125,6 @@ const GrpcAuth = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full overflow-y-scroll">
|
||||
<div className="flex flex-grow justify-start items-center">
|
||||
<GrpcAuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
{getAuthView()}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,35 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
|
||||
.content-indicator {
|
||||
color: ${(props) => props.theme.text}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import React, { useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import GrpcBody from 'components/RequestPane/GrpcBody';
|
||||
import GrpcAuth from './GrpcAuth/index';
|
||||
import GrpcAuthMode from './GrpcAuth/GrpcAuthMode/index';
|
||||
import StatusDot from 'components/StatusDot/index';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { find, get } from 'lodash';
|
||||
import find from 'lodash/find';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import { useEffect } from 'react';
|
||||
import { getPropertyFromDraftOrRequest } from 'utils/collections/index';
|
||||
import ResponsiveTabs from 'ui/ResponsiveTabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const GrpcRequestPane = ({ item, collection, handleRun }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const rightContentRef = useRef(null);
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const requestPaneTab = focusedTab?.requestPaneTab;
|
||||
|
||||
const selectTab = (tab) => {
|
||||
const selectTab = useCallback((tab) => {
|
||||
dispatch(
|
||||
updateRequestPaneTab({
|
||||
uid: item.uid,
|
||||
requestPaneTab: tab
|
||||
})
|
||||
);
|
||||
};
|
||||
}, [dispatch, item.uid]);
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
const tabPanel = useMemo(() => {
|
||||
switch (requestPaneTab) {
|
||||
case 'body': {
|
||||
return <GrpcBody item={item} collection={collection} hideModeSelector={true} hidePrettifyButton={true} handleRun={handleRun} />;
|
||||
}
|
||||
@@ -45,22 +48,7 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!activeTabUid) {
|
||||
return <div>Something went wrong</div>;
|
||||
}
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === focusedTab.requestPaneTab
|
||||
});
|
||||
};
|
||||
}, [requestPaneTab, item, collection, handleRun]);
|
||||
|
||||
const body = getPropertyFromDraftOrRequest(item, 'request.body');
|
||||
const headers = getPropertyFromDraftOrRequest(item, 'request.headers');
|
||||
@@ -74,44 +62,80 @@ const GrpcRequestPane = ({ item, collection, handleRun }) => {
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
const isClientStreaming = request.methodType === 'client-streaming' || request.methodType === 'bidi-streaming';
|
||||
|
||||
const allTabs = useMemo(() => {
|
||||
const getMessageIndicator = () => {
|
||||
if (grpcMessagesCount > 0) {
|
||||
return isClientStreaming ? (
|
||||
<sup className="ml-[.125rem] font-medium">{grpcMessagesCount}</sup>
|
||||
) : (
|
||||
<StatusDot />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'body',
|
||||
label: 'Message',
|
||||
indicator: getMessageIndicator()
|
||||
},
|
||||
{
|
||||
key: 'headers',
|
||||
label: 'Metadata',
|
||||
indicator: activeHeadersLength > 0 ? <sup className="ml-[.125rem] font-medium">{activeHeadersLength}</sup> : null
|
||||
},
|
||||
{
|
||||
key: 'auth',
|
||||
label: 'Auth',
|
||||
indicator: auth?.mode && auth.mode !== 'none' ? <StatusDot type="default" /> : null
|
||||
},
|
||||
{
|
||||
key: 'docs',
|
||||
label: 'Docs',
|
||||
indicator: docs && docs.length > 0 ? <StatusDot type="default" /> : null
|
||||
}
|
||||
];
|
||||
}, [grpcMessagesCount, isClientStreaming, activeHeadersLength, auth?.mode, docs]);
|
||||
|
||||
// Initialize tab to 'body' if no tab is currently set
|
||||
useEffect(() => {
|
||||
// Only set the tab to 'body' if no tab is currently set
|
||||
if (!focusedTab?.requestPaneTab) {
|
||||
if (activeTabUid && focusedTab?.uid && !requestPaneTab) {
|
||||
selectTab('body');
|
||||
}
|
||||
}, []);
|
||||
}, [activeTabUid, focusedTab?.uid, requestPaneTab, selectTab]);
|
||||
|
||||
// Return error for truly missing active/focused tabs
|
||||
if (!activeTabUid || !focusedTab?.uid) {
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
// Return null during initialization while requestPaneTab is being set by useEffect
|
||||
if (!requestPaneTab) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rightContent = requestPaneTab === 'auth' ? (
|
||||
<div ref={rightContentRef} className="flex flex-grow justify-start items-center">
|
||||
<GrpcAuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
|
||||
Message
|
||||
{grpcMessagesCount > 0 && (
|
||||
isClientStreaming ? (
|
||||
<sup className="ml-[.125rem] font-medium">{grpcMessagesCount}</sup>
|
||||
) : (
|
||||
<StatusDot />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Metadata
|
||||
{activeHeadersLength > 0 && <sup className="ml-[.125rem] font-medium">{activeHeadersLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
{auth.mode !== 'none' && <StatusDot type="default" />}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
{docs && docs.length > 0 && <StatusDot type="default" />}
|
||||
</div>
|
||||
</div>
|
||||
<ResponsiveTabs
|
||||
tabs={allTabs}
|
||||
activeTab={requestPaneTab}
|
||||
onTabSelect={selectTab}
|
||||
rightContent={rightContent}
|
||||
rightContentRef={rightContent ? rightContentRef : null}
|
||||
/>
|
||||
|
||||
<section
|
||||
className={classnames('flex w-full flex-1 h-full mt-4')}
|
||||
className="flex w-full flex-1 h-full mt-4"
|
||||
>
|
||||
<HeightBoundContainer>
|
||||
{getTabPanel(focusedTab.requestPaneTab)}
|
||||
{tabPanel}
|
||||
</HeightBoundContainer>
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -95,7 +95,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
|
||||
const curlCommandRegex = /^\s*curl\s/i;
|
||||
if (!curlCommandRegex.test(pastedData)) {
|
||||
toast.error('Invalid cURL command');
|
||||
// Not a curl command, allow normal paste behavior
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
@@ -375,7 +375,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
</div>
|
||||
<div
|
||||
id="request-url"
|
||||
className="h-full w-full flex flex-row input-container"
|
||||
className="h-full w-full flex flex-row input-container overflow-auto"
|
||||
>
|
||||
<SingleLineEditor
|
||||
ref={editorRef}
|
||||
@@ -391,53 +391,54 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
item={item}
|
||||
showNewlineArrow={true}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||
<div
|
||||
title="Generate Code"
|
||||
className="infotip mr-3"
|
||||
onClick={(e) => {
|
||||
handleGenerateCode(e);
|
||||
}}
|
||||
>
|
||||
<IconCode color={theme.requestTabs.icon.color} strokeWidth={1.5} size={20} className="cursor-pointer" />
|
||||
<span className="infotiptext text-xs">Generate Code</span>
|
||||
</div>
|
||||
<div
|
||||
title="Save Request"
|
||||
className="infotip mr-3"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!hasChanges) return;
|
||||
onSave();
|
||||
}}
|
||||
>
|
||||
<IconDeviceFloppy
|
||||
color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
className={`${hasChanges ? 'cursor-pointer' : 'cursor-default'}`}
|
||||
/>
|
||||
<span className="infotiptext text-xs">
|
||||
Save <span className="shortcut">({saveShortcut})</span>
|
||||
</span>
|
||||
</div>
|
||||
{isLoading || item.response?.stream?.running ? (
|
||||
<IconSquareRoundedX
|
||||
color={theme.requestTabPanel.url.iconDanger}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
data-testid="cancel-request-icon"
|
||||
onClick={handleCancelRequest}
|
||||
/>
|
||||
) : (
|
||||
<IconArrowRight
|
||||
color={theme.requestTabPanel.url.icon}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
data-testid="send-arrow-icon"
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className="flex items-center h-full mx-2 gap-3 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||
<div
|
||||
title="Generate Code"
|
||||
className="infotip"
|
||||
onClick={(e) => {
|
||||
handleGenerateCode(e);
|
||||
}}
|
||||
>
|
||||
<IconCode color={theme.requestTabs.icon.color} strokeWidth={1.5} size={20} className="cursor-pointer" />
|
||||
<span className="infotiptext text-xs">Generate Code</span>
|
||||
</div>
|
||||
<div
|
||||
title="Save Request"
|
||||
className="infotip"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!hasChanges) return;
|
||||
onSave();
|
||||
}}
|
||||
>
|
||||
<IconDeviceFloppy
|
||||
color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
className={`${hasChanges ? 'cursor-pointer' : 'cursor-default'}`}
|
||||
/>
|
||||
<span className="infotiptext text-xs">
|
||||
Save <span className="shortcut">({saveShortcut})</span>
|
||||
</span>
|
||||
</div>
|
||||
{isLoading || item.response?.stream?.running ? (
|
||||
<IconSquareRoundedX
|
||||
color={theme.requestTabPanel.url.iconDanger}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
data-testid="cancel-request-icon"
|
||||
onClick={handleCancelRequest}
|
||||
/>
|
||||
) : (
|
||||
<IconArrowRight
|
||||
color={theme.requestTabPanel.url.icon}
|
||||
strokeWidth={1.5}
|
||||
size={20}
|
||||
data-testid="send-arrow-icon"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{generateCodeItemModalOpen && (
|
||||
<GenerateCodeItem
|
||||
|
||||
@@ -1,34 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
|
||||
.content-indicator {
|
||||
color: ${(props) => props.theme.text}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import WSAuthMode from './WSAuthMode';
|
||||
import BearerAuth from '../../Auth/BearerAuth';
|
||||
import BasicAuth from '../../Auth/BasicAuth';
|
||||
import ApiKeyAuth from '../../Auth/ApiKeyAuth';
|
||||
@@ -68,6 +67,9 @@ const WSAuth = ({ item, collection }) => {
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'none': {
|
||||
return <div>No Auth</div>;
|
||||
}
|
||||
case 'basic': {
|
||||
return <BasicAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;
|
||||
}
|
||||
@@ -80,7 +82,7 @@ const WSAuth = ({ item, collection }) => {
|
||||
case 'oauth2': {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div className="flex flex-row w-full gap-2">
|
||||
<div>
|
||||
OAuth 2 not <strong>yet</strong> supported by WebSockets. Using no auth instead.
|
||||
</div>
|
||||
@@ -95,7 +97,7 @@ const WSAuth = ({ item, collection }) => {
|
||||
if (source && supportedAuthModes.includes(source.auth?.mode)) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div className="flex flex-row w-full gap-2">
|
||||
<div> Auth inherited from {source.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
|
||||
</div>
|
||||
@@ -104,7 +106,7 @@ const WSAuth = ({ item, collection }) => {
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div className="flex flex-row w-full gap-2">
|
||||
<div>Inherited auth not supported by WebSockets. Using no auth instead.</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -119,9 +121,6 @@ const WSAuth = ({ item, collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full overflow-y-scroll">
|
||||
<div className="flex flex-grow justify-start items-center">
|
||||
<WSAuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
{getAuthView()}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import classnames from 'classnames';
|
||||
import React, { useMemo, useCallback, useRef } from 'react';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import StatusDot from 'components/StatusDot/index';
|
||||
import { find } from 'lodash';
|
||||
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import ResponsiveTabs from 'ui/ResponsiveTabs';
|
||||
import { getPropertyFromDraftOrRequest } from 'utils/collections/index';
|
||||
import WsBody from '../WsBody/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import WSAuth from './WSAuth';
|
||||
import WSAuthMode from './WSAuth/WSAuthMode';
|
||||
import WSSettingsPane from '../WSSettingsPane/index';
|
||||
|
||||
const WSRequestPane = ({ item, collection, handleRun }) => {
|
||||
@@ -18,15 +19,59 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(updateRequestPaneTab({
|
||||
uid: item.uid,
|
||||
requestPaneTab: tab
|
||||
}));
|
||||
};
|
||||
const rightContentRef = useRef(null);
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const requestPaneTab = focusedTab?.requestPaneTab;
|
||||
|
||||
const selectTab = useCallback(
|
||||
(tab) => {
|
||||
dispatch(updateRequestPaneTab({
|
||||
uid: item.uid,
|
||||
requestPaneTab: tab
|
||||
}));
|
||||
},
|
||||
[dispatch, item.uid]
|
||||
);
|
||||
|
||||
const headers = getPropertyFromDraftOrRequest(item, 'request.headers');
|
||||
const docs = getPropertyFromDraftOrRequest(item, 'request.docs');
|
||||
const auth = getPropertyFromDraftOrRequest(item, 'request.auth');
|
||||
|
||||
const activeHeadersLength = headers.filter((header) => header.enabled).length;
|
||||
|
||||
const allTabs = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'body',
|
||||
label: 'Message',
|
||||
indicator: null
|
||||
},
|
||||
{
|
||||
key: 'headers',
|
||||
label: 'Headers',
|
||||
indicator: activeHeadersLength > 0 ? <sup className="ml-[.125rem] font-medium">{activeHeadersLength}</sup> : null
|
||||
},
|
||||
{
|
||||
key: 'auth',
|
||||
label: 'Auth',
|
||||
indicator: auth.mode !== 'none' ? <StatusDot type="default" /> : null
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
label: 'Settings',
|
||||
indicator: null
|
||||
},
|
||||
{
|
||||
key: 'docs',
|
||||
label: 'Docs',
|
||||
indicator: docs && docs.length > 0 ? <StatusDot type="default" /> : null
|
||||
}
|
||||
];
|
||||
}, [activeHeadersLength, auth.mode, docs]);
|
||||
|
||||
const tabPanel = useMemo(() => {
|
||||
switch (requestPaneTab) {
|
||||
case 'body': {
|
||||
return (
|
||||
<WsBody
|
||||
@@ -54,61 +99,30 @@ const WSRequestPane = ({ item, collection, handleRun }) => {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [requestPaneTab, item, collection, handleRun]);
|
||||
|
||||
if (!activeTabUid) {
|
||||
return <div>Something went wrong</div>;
|
||||
}
|
||||
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
if (!focusedTab || !focusedTab.uid || !focusedTab.requestPaneTab) {
|
||||
if (!activeTabUid || !focusedTab?.uid || !requestPaneTab) {
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === focusedTab.requestPaneTab
|
||||
});
|
||||
};
|
||||
|
||||
const headers = getPropertyFromDraftOrRequest(item, 'request.headers');
|
||||
const docs = getPropertyFromDraftOrRequest(item, 'request.docs');
|
||||
const auth = getPropertyFromDraftOrRequest(item, 'request.auth');
|
||||
|
||||
const activeHeadersLength = headers.filter((header) => header.enabled).length;
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusedTab?.requestPaneTab) {
|
||||
selectTab('body');
|
||||
}
|
||||
}, []);
|
||||
const rightContent = requestPaneTab === 'auth' ? (
|
||||
<div ref={rightContentRef} className="flex flex-grow justify-start items-center">
|
||||
<WSAuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
|
||||
Message
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
{activeHeadersLength > 0 && <sup className="ml-[.125rem] font-medium">{activeHeadersLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
{auth.mode !== 'none' && <StatusDot type="default" />}
|
||||
</div>
|
||||
<div className={getTabClassname('settings')} role="tab" onClick={() => selectTab('settings')}>
|
||||
Settings
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
{docs && docs.length > 0 && <StatusDot type="default" />}
|
||||
</div>
|
||||
</div>
|
||||
<section
|
||||
className={classnames('flex w-full flex-1 h-full mt-4')}
|
||||
>
|
||||
<HeightBoundContainer>{getTabPanel(focusedTab.requestPaneTab)}</HeightBoundContainer>
|
||||
<ResponsiveTabs
|
||||
tabs={allTabs}
|
||||
activeTab={requestPaneTab}
|
||||
onTabSelect={selectTab}
|
||||
rightContent={rightContent}
|
||||
rightContentRef={rightContent ? rightContentRef : null}
|
||||
/>
|
||||
|
||||
<section className="flex w-full flex-1 h-full mt-4">
|
||||
<HeightBoundContainer>{tabPanel}</HeightBoundContainer>
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -123,7 +123,7 @@ const WsQueryUrl = ({ item, collection, handleRun }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center h-full">
|
||||
<div className="flex items-center input-container flex-1 w-full input-container pr-2 h-full relative">
|
||||
<div className="flex items-center input-container flex-1 w-full h-full relative">
|
||||
<div className="flex items-center justify-center px-[10px]">
|
||||
<span className="text-xs font-medium method-ws">WS</span>
|
||||
</div>
|
||||
@@ -138,9 +138,9 @@ const WsQueryUrl = ({ item, collection, handleRun }) => {
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer">
|
||||
<div className="flex items-center h-full cursor-pointer gap-3 mx-3">
|
||||
<div
|
||||
className="infotip mr-3"
|
||||
className="infotip"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!hasChanges) return;
|
||||
@@ -159,7 +159,7 @@ const WsQueryUrl = ({ item, collection, handleRun }) => {
|
||||
</div>
|
||||
|
||||
{connectionStatus === 'connected' && (
|
||||
<div className="connection-controls relative flex items-center h-full gap-3 mr-3">
|
||||
<div className="connection-controls relative flex items-center h-full">
|
||||
<div className="infotip" onClick={(e) => handleDisconnect(e, true)}>
|
||||
<IconPlugConnectedX
|
||||
color={theme.colors.text.danger}
|
||||
@@ -173,7 +173,7 @@ const WsQueryUrl = ({ item, collection, handleRun }) => {
|
||||
)}
|
||||
|
||||
{connectionStatus !== 'connected' && (
|
||||
<div className="connection-controls relative flex items-center h-full gap-3 mr-3">
|
||||
<div className="connection-controls relative flex items-center h-full">
|
||||
<div className="infotip" onClick={handleConnect}>
|
||||
<IconPlugConnected
|
||||
className={classnames('cursor-pointer', {
|
||||
|
||||
@@ -14,6 +14,10 @@ const Wrapper = styled.div`
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.scroll-chevrons.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabs-scroll-container {
|
||||
overflow-x: auto;
|
||||
overflow-y: clip;
|
||||
@@ -192,10 +196,6 @@ const Wrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
&.has-chevrons ul {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.special-tab-icon {
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
}
|
||||
|
||||
@@ -103,14 +103,9 @@ const RequestTabs = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const getRootClassname = () => {
|
||||
return classnames({
|
||||
'has-chevrons': showChevrons
|
||||
});
|
||||
};
|
||||
// Todo: Must support ephemeral requests
|
||||
return (
|
||||
<StyledWrapper className={getRootClassname()}>
|
||||
<StyledWrapper>
|
||||
{newRequestModalOpen && (
|
||||
<NewRequest collectionUid={activeCollection?.uid} onClose={() => setNewRequestModalOpen(false)} />
|
||||
)}
|
||||
@@ -118,12 +113,11 @@ const RequestTabs = () => {
|
||||
<>
|
||||
<CollectionToolBar collection={activeCollection} />
|
||||
<div className="flex items-center gap-2 pl-2" ref={collectionTabsRef}>
|
||||
|
||||
{showChevrons ? (
|
||||
<div className={classnames('scroll-chevrons', { hidden: !showChevrons })}>
|
||||
<ActionIcon size="lg" onClick={leftSlide} aria-label="Left Chevron" style={{ marginBottom: '3px' }}>
|
||||
<IconChevronLeft size={18} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
) : null}
|
||||
</div>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab mr-1" onClick={createNewTab}>
|
||||
<div className="flex items-center home-icon-container">
|
||||
@@ -175,11 +169,11 @@ const RequestTabs = () => {
|
||||
</ActionIcon>
|
||||
)}
|
||||
|
||||
{showChevrons ? (
|
||||
<div className={classnames('scroll-chevrons', { hidden: !showChevrons })}>
|
||||
<ActionIcon size="lg" onClick={rightSlide} aria-label="Right Chevron" style={{ marginBottom: '3px' }}>
|
||||
<IconChevronRight size={18} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
) : null}
|
||||
</div>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <li className="select-none new-tab choose-request">
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -61,7 +61,7 @@ const ResponseExampleUrlBar = ({ item, collection, editMode, onSave, exampleUid
|
||||
|
||||
<div
|
||||
id="response-example-url"
|
||||
className="response-example-url flex items-center flex-1 h-6"
|
||||
className="response-example-url flex items-center flex-1 h-6 min-w-0 overflow-hidden"
|
||||
>
|
||||
<SingleLineEditor
|
||||
value={url}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import find from 'lodash/find';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import Tab from 'components/Tab';
|
||||
import ResponseLayoutToggle from 'components/ResponsePane/ResponseLayoutToggle';
|
||||
import StatusCode from 'components/ResponsePane/StatusCode';
|
||||
@@ -10,7 +13,20 @@ import StyledWrapper from './StyledWrapper';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
|
||||
const ResponseExampleResponsePane = ({ item, collection, editMode, exampleUid, onSave }) => {
|
||||
const [activeTab, setActiveTab] = useState('response');
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
|
||||
// Get the focused tab for reading persisted tab state
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
const activeTab = focusedTab?.responsePaneTab || 'response';
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(updateResponsePaneTab({
|
||||
uid: exampleUid,
|
||||
responsePaneTab: tab
|
||||
}));
|
||||
};
|
||||
|
||||
const exampleData = useMemo(() => {
|
||||
return item.draft ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid) || {} : get(item, 'examples', []).find((e) => e.uid === exampleUid) || {};
|
||||
@@ -67,7 +83,7 @@ const ResponseExampleResponsePane = ({ item, collection, editMode, exampleUid, o
|
||||
name={tab.name}
|
||||
label={tab.label}
|
||||
isActive={activeTab === tab.name}
|
||||
onClick={setActiveTab}
|
||||
onClick={selectTab}
|
||||
count={tab.count}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import classnames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import Overlay from '../Overlay';
|
||||
@@ -15,13 +14,14 @@ import StyledWrapper from './StyledWrapper';
|
||||
import ResponseTrailers from './ResponseTrailers';
|
||||
import GrpcQueryResult from './GrpcQueryResult';
|
||||
import ResponseLayoutToggle from '../ResponseLayoutToggle';
|
||||
import Tab from 'components/Tab';
|
||||
import ResponsiveTabs from 'ui/ResponsiveTabs';
|
||||
|
||||
const GrpcResponsePane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isLoading = ['queued', 'sending'].includes(item.requestState);
|
||||
const rightContentRef = useRef(null);
|
||||
|
||||
const requestTimeline = [...(collection?.timeline || [])].filter((obj) => {
|
||||
if (obj.itemUid === item.uid) return true;
|
||||
@@ -38,6 +38,38 @@ const GrpcResponsePane = ({ item, collection }) => {
|
||||
|
||||
const response = item.response || {};
|
||||
|
||||
const metadataCount = Array.isArray(response.metadata) ? response.metadata.length : 0;
|
||||
const trailersCount = Array.isArray(response.trailers) ? response.trailers.length : 0;
|
||||
const responsesCount = Array.isArray(response.responses) ? response.responses.length : 0;
|
||||
|
||||
const allTabs = [
|
||||
{
|
||||
key: 'response',
|
||||
label: 'Response',
|
||||
indicator:
|
||||
responsesCount > 0 ? (
|
||||
<sup data-testid="grpc-tab-response-count" className="ml-1 font-medium">
|
||||
{responsesCount}
|
||||
</sup>
|
||||
) : null
|
||||
},
|
||||
{
|
||||
key: 'headers',
|
||||
label: 'Metadata',
|
||||
indicator: metadataCount > 0 ? <sup className="ml-1 font-medium">{metadataCount}</sup> : null
|
||||
},
|
||||
{
|
||||
key: 'trailers',
|
||||
label: 'Trailers',
|
||||
indicator: trailersCount > 0 ? <sup className="ml-1 font-medium">{trailersCount}</sup> : null
|
||||
},
|
||||
{
|
||||
key: 'timeline',
|
||||
label: 'Timeline',
|
||||
indicator: null
|
||||
}
|
||||
];
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
@@ -83,66 +115,40 @@ const GrpcResponsePane = ({ item, collection }) => {
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
const tabConfig = [
|
||||
{
|
||||
name: 'response',
|
||||
label: 'Response',
|
||||
count: Array.isArray(response.responses) ? response.responses.length : 0
|
||||
},
|
||||
{
|
||||
name: 'headers',
|
||||
label: 'Metadata',
|
||||
count: Array.isArray(response.metadata) ? response.metadata.length : 0
|
||||
},
|
||||
{
|
||||
name: 'trailers',
|
||||
label: 'Trailers',
|
||||
count: Array.isArray(response.trailers) ? response.trailers.length : 0
|
||||
},
|
||||
{
|
||||
name: 'timeline',
|
||||
label: 'Timeline'
|
||||
}
|
||||
];
|
||||
const rightContent = !isLoading ? (
|
||||
<div ref={rightContentRef} className="flex items-center">
|
||||
{focusedTab?.responsePaneTab === 'timeline' ? (
|
||||
<>
|
||||
<ResponseLayoutToggle />
|
||||
<ClearTimeline item={item} collection={collection} />
|
||||
</>
|
||||
) : item?.response ? (
|
||||
<>
|
||||
<ResponseLayoutToggle />
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<GrpcStatusCode
|
||||
status={response.statusCode}
|
||||
text={response.statusText}
|
||||
details={response.statusDescription}
|
||||
/>
|
||||
<ResponseTime duration={response.duration} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center pl-3 pr-4 tabs" role="tablist" data-testid="grpc-response-tabs">
|
||||
{tabConfig.map((tab) => (
|
||||
<Tab
|
||||
key={tab.name}
|
||||
name={tab.name}
|
||||
label={tab.label}
|
||||
isActive={focusedTab.responsePaneTab === tab.name}
|
||||
onClick={selectTab}
|
||||
count={tab.count}
|
||||
/>
|
||||
))}
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
{focusedTab?.responsePaneTab === 'timeline' ? (
|
||||
<>
|
||||
<ResponseLayoutToggle />
|
||||
<ClearTimeline item={item} collection={collection} />
|
||||
</>
|
||||
) : item?.response ? (
|
||||
<>
|
||||
<ResponseLayoutToggle />
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<GrpcStatusCode
|
||||
status={response.statusCode}
|
||||
text={response.statusText}
|
||||
details={response.statusDescription}
|
||||
/>
|
||||
<ResponseTime duration={response.duration} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="px-4">
|
||||
<ResponsiveTabs
|
||||
tabs={allTabs}
|
||||
activeTab={focusedTab.responsePaneTab}
|
||||
onTabSelect={selectTab}
|
||||
rightContent={rightContent}
|
||||
rightContentRef={rightContentRef}
|
||||
/>
|
||||
</div>
|
||||
<section
|
||||
className="flex flex-col flex-grow pl-3 pr-4 h-0 mt-4"
|
||||
>
|
||||
<section className="flex flex-col flex-grow px-4 h-0 mt-4">
|
||||
{isLoading ? <Overlay item={item} collection={collection} /> : null}
|
||||
{!item?.response ? (
|
||||
focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? (
|
||||
|
||||
@@ -10,12 +10,14 @@ const LargeResponseWarning = ({ item, responseSize, onRevealResponse }) => {
|
||||
const { ipcRenderer } = window;
|
||||
const response = item.response || {};
|
||||
|
||||
const saveResponseToFile = () => {
|
||||
const downloadResponseToFile = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-response-to-file', response, item.requestSent.url)
|
||||
.then(() => {
|
||||
toast.success('Response saved to file');
|
||||
.invoke('renderer:save-response-to-file', response, item.requestSent.url, item.pathname)
|
||||
.then((result) => {
|
||||
if (result && result.success) {
|
||||
toast.success('Response downloaded to file');
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -72,13 +74,13 @@ const LargeResponseWarning = ({ item, responseSize, onRevealResponse }) => {
|
||||
<Button
|
||||
icon={<IconDownload size={18} strokeWidth={1.5} />}
|
||||
iconPosition="left"
|
||||
onClick={saveResponseToFile}
|
||||
onClick={downloadResponseToFile}
|
||||
disabled={!response.dataBuffer}
|
||||
title="Save response to file"
|
||||
title="Download response to file"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
>
|
||||
Save
|
||||
Download
|
||||
</Button>
|
||||
<Button
|
||||
icon={<IconCopy size={18} strokeWidth={1.5} />}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { isValidHtml } from 'utils/common/index';
|
||||
import { escapeHtml, isValidHtmlSnippet } from 'utils/response/index';
|
||||
import { escapeHtml } from 'utils/response/index';
|
||||
|
||||
const HtmlPreview = React.memo(({ data, baseUrl }) => {
|
||||
const webviewContainerRef = useRef(null);
|
||||
@@ -31,7 +30,7 @@ const HtmlPreview = React.memo(({ data, baseUrl }) => {
|
||||
return () => mutationObserver.disconnect();
|
||||
}, []);
|
||||
|
||||
if (isValidHtml(data) || isValidHtmlSnippet(data)) {
|
||||
const renderHtmlPreview = (data, baseUrl, isDragging, webviewContainerRef) => {
|
||||
const htmlContent = data.includes('<head>')
|
||||
? data.replace('<head>', `<head><base href="${escapeHtml(baseUrl)}">`)
|
||||
: `<head><base href="${escapeHtml(baseUrl)}"></head>${data}`;
|
||||
@@ -52,7 +51,7 @@ const HtmlPreview = React.memo(({ data, baseUrl }) => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// For all other data types, render safely as formatted text
|
||||
let displayContent = '';
|
||||
@@ -60,18 +59,12 @@ const HtmlPreview = React.memo(({ data, baseUrl }) => {
|
||||
displayContent = String(data);
|
||||
} else if (typeof data === 'object') {
|
||||
displayContent = JSON.stringify(data, null);
|
||||
} else if (typeof data === 'string') {
|
||||
displayContent = data;
|
||||
} else {
|
||||
displayContent = String(data);
|
||||
}
|
||||
|
||||
return (
|
||||
<pre
|
||||
className="bg-white font-mono text-[13px] whitespace-pre-wrap break-words overflow-auto overflow-x-hidden p-4 text-[#24292f] w-full max-w-full h-full box-border relative"
|
||||
>
|
||||
{displayContent}
|
||||
</pre>
|
||||
<>{renderHtmlPreview(displayContent, baseUrl, isDragging, webviewContainerRef)}</>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -49,11 +49,11 @@ export const useInitialResponseFormat = (dataBuffer, headers) => {
|
||||
|
||||
// Wait until both content types are available
|
||||
if (detectedContentType === null || contentType === undefined) {
|
||||
return { initialFormat: null, initialTab: null };
|
||||
return { initialFormat: null, initialTab: null, contentType: contentType };
|
||||
}
|
||||
|
||||
const initial = getDefaultResponseFormat(contentType);
|
||||
return { initialFormat: initial.format, initialTab: initial.tab };
|
||||
return { initialFormat: initial.format, initialTab: initial.tab, contentType: contentType };
|
||||
}, [dataBuffer, headers]);
|
||||
};
|
||||
|
||||
@@ -66,6 +66,7 @@ export const useResponsePreviewFormatOptions = (dataBuffer, headers) => {
|
||||
const byteFormatTypes = ['image', 'video', 'audio', 'pdf', 'zip'];
|
||||
|
||||
const isByteFormatType = (contentType) => {
|
||||
if (contentType.toLowerCase().includes('svg')) return false; // SVG is text-based
|
||||
return byteFormatTypes.some((type) => contentType.includes(type));
|
||||
};
|
||||
|
||||
@@ -203,7 +204,7 @@ const QueryResult = ({
|
||||
dataBuffer={dataBuffer}
|
||||
formattedData={formattedData}
|
||||
item={item}
|
||||
contentType={contentType}
|
||||
contentType={detectedContentType ?? contentType}
|
||||
previewMode={previewMode}
|
||||
codeMirrorMode={codeMirrorMode}
|
||||
collection={collection}
|
||||
|
||||
@@ -24,7 +24,12 @@ const ResponseDownload = forwardRef(({ item, children }, ref) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer
|
||||
.invoke('renderer:save-response-to-file', response, item?.requestSent?.url, item.pathname)
|
||||
.then(resolve)
|
||||
.then((result) => {
|
||||
if (result && result.success) {
|
||||
toast.success('Response downloaded to file');
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(get(err, 'error.message') || 'Something went wrong!');
|
||||
reject(err);
|
||||
|
||||
@@ -245,7 +245,7 @@ const GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData,
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`${eventClass} pl-1`}>
|
||||
<StyledWrapper className={`${eventClass} pl-1 mb-2`}>
|
||||
<div className="event-header" onClick={toggleCollapse}>
|
||||
{isCollapsed ? <IconChevronRight size={16} strokeWidth={1.5} /> : <IconChevronDown size={16} strokeWidth={1.5} />}
|
||||
<div className="event-icon-container">
|
||||
|
||||
@@ -53,6 +53,17 @@ const StyledWrapper = styled.div`
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
div.tabs .action-icon {
|
||||
color: ${(props) => props.theme.dropdown.iconColor};
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.text};
|
||||
opacity: 1;
|
||||
background-color: ${(props) => props.theme.workspace.button.bg};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
|
||||
.empty-state {
|
||||
padding: 1rem;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useRef, useEffect, useCallback, memo } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconExclamationCircle, IconChevronRight, IconInfoCircle, IconChevronDown, IconArrowUpRight, IconArrowDownLeft } from '@tabler/icons';
|
||||
import CodeEditor from 'components/CodeEditor/index';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useRef } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
const getContentMeta = (content) => {
|
||||
if (typeof content === 'object') {
|
||||
@@ -61,8 +59,7 @@ const TypeIcon = ({ type }) => {
|
||||
}[type];
|
||||
};
|
||||
|
||||
const WSMessageItem = ({ message, inFocus }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const WSMessageItem = memo(({ message, isOpen, onToggle }) => {
|
||||
const [showHex, setShowHex] = useState(false);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const { displayedTheme } = useTheme();
|
||||
@@ -82,21 +79,23 @@ const WSMessageItem = ({ message, inFocus }) => {
|
||||
const dateDiff = Date.now() - new Date(message.timestamp).getTime();
|
||||
if (dateDiff < 1000 * 10) {
|
||||
setIsNew(true);
|
||||
setTimeout(() => {
|
||||
const timer = setTimeout(() => {
|
||||
notified.current = true;
|
||||
setIsNew(false);
|
||||
}, 2500);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [message]);
|
||||
}, [message.timestamp]);
|
||||
|
||||
const canOpenMessage = !isInfo && !isError;
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!canOpenMessage) return;
|
||||
onToggle?.(message.timestamp);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(node) => {
|
||||
if (!node) return;
|
||||
if (inFocus) node.scrollIntoView();
|
||||
}}
|
||||
className={classnames('ws-message flex flex-col p-2', {
|
||||
'ws-incoming': isIncoming,
|
||||
'ws-outgoing': isOutgoing,
|
||||
@@ -111,10 +110,7 @@ const WSMessageItem = ({ message, inFocus }) => {
|
||||
'cursor-pointer': canOpenMessage,
|
||||
'cursor-not-allowed': !canOpenMessage
|
||||
})}
|
||||
onClick={(e) => {
|
||||
if (!canOpenMessage) return;
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<div className="flex min-w-0 shrink">
|
||||
<span className="message-type-icon">
|
||||
@@ -176,23 +172,87 @@ const WSMessageItem = ({ message, inFocus }) => {
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const WSMessagesList = ({ messages = [] }) => {
|
||||
const virtuosoRef = useRef(null);
|
||||
const [scrollerElement, setScrollerElement] = useState(null);
|
||||
const [openMessages, setOpenMessages] = useState(new Set());
|
||||
const userScrolledAwayRef = useRef(false);
|
||||
|
||||
// Toggle message open/closed state by timestamp
|
||||
const handleMessageToggle = useCallback((timestamp) => {
|
||||
setOpenMessages((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(timestamp)) {
|
||||
next.delete(timestamp);
|
||||
} else {
|
||||
next.add(timestamp);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrollerElement) return;
|
||||
|
||||
const handleWheel = (e) => {
|
||||
// deltaY < 0 means scrolling up
|
||||
if (e.deltaY < 0) {
|
||||
userScrolledAwayRef.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
scrollerElement.addEventListener('wheel', handleWheel, { passive: true });
|
||||
|
||||
return () => {
|
||||
scrollerElement.removeEventListener('wheel', handleWheel);
|
||||
};
|
||||
}, [scrollerElement]);
|
||||
|
||||
const handleAtBottomStateChange = useCallback((atBottom) => {
|
||||
if (atBottom) {
|
||||
// User scrolled back to bottom, re-enable auto-scroll
|
||||
userScrolledAwayRef.current = false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const followOutput = useCallback((isAtBottom) => {
|
||||
// Don't auto-scroll if user has scrolled away or has messages open
|
||||
if (userScrolledAwayRef.current || openMessages.size > 0) {
|
||||
return false;
|
||||
}
|
||||
if (isAtBottom) {
|
||||
return 'smooth';
|
||||
}
|
||||
return false;
|
||||
}, [openMessages.size]);
|
||||
|
||||
const renderItem = useCallback((_, msg) => {
|
||||
const isOpen = openMessages.has(msg.timestamp);
|
||||
return <WSMessageItem message={msg} isOpen={isOpen} onToggle={handleMessageToggle} />;
|
||||
}, [openMessages, handleMessageToggle]);
|
||||
|
||||
const computeItemKey = useCallback((_, msg) => {
|
||||
return msg.seq ?? msg.timestamp;
|
||||
}, []);
|
||||
|
||||
const WSMessagesList = ({ order = -1, messages = [] }) => {
|
||||
if (!messages.length) {
|
||||
return <StyledWrapper><div className="empty-state">No messages yet.</div></StyledWrapper>;
|
||||
}
|
||||
|
||||
// sort based on order, seq was newly added and might be missing in some cases and when missing,
|
||||
// the timestamp will be used instead
|
||||
const ordered = messages.toSorted((x, y) => ((x.seq ?? x.timestamp) - (y.seq ?? y.timestamp)) * order);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="ws-messages-list flex flex-col">
|
||||
{ordered.map((msg, idx, src) => {
|
||||
const inFocus = order === -1 ? src.length - 1 === idx : idx === 0;
|
||||
return <WSMessageItem key={msg.seq ? msg.seq : msg.timestamp} inFocus={inFocus} id={idx} message={msg} />;
|
||||
})}
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
scrollerRef={setScrollerElement}
|
||||
data={messages}
|
||||
itemContent={renderItem}
|
||||
computeItemKey={computeItemKey}
|
||||
followOutput={followOutput}
|
||||
initialTopMostItemIndex={messages.length - 1}
|
||||
atBottomStateChange={handleAtBottomStateChange}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
@@ -11,13 +11,12 @@ import ClearTimeline from '../ClearTimeline';
|
||||
import ResponseClear from '../ResponseClear';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ResponseLayoutToggle from '../ResponseLayoutToggle';
|
||||
import Tab from 'components/Tab';
|
||||
import ResponsiveTabs from 'ui/ResponsiveTabs';
|
||||
import WSMessagesList from './WSMessagesList';
|
||||
import WSResponseSortOrder from './WSResponseSortOrder';
|
||||
import WSResponseHeaders from './WSResponseHeaders';
|
||||
|
||||
const WSResult = ({ response }) => {
|
||||
return <WSMessagesList order={response?.sortOrder} messages={response.responses || []} />;
|
||||
return <WSMessagesList messages={response.responses || []} />;
|
||||
};
|
||||
|
||||
const WSResponsePane = ({ item, collection }) => {
|
||||
@@ -25,6 +24,7 @@ const WSResponsePane = ({ item, collection }) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isLoading = ['queued', 'sending'].includes(item.requestState);
|
||||
const rightContentRef = useRef(null);
|
||||
|
||||
const requestTimeline = [...(collection?.timeline || [])].filter((obj) => {
|
||||
if (obj.itemUid === item.uid) return true;
|
||||
@@ -39,6 +39,29 @@ const WSResponsePane = ({ item, collection }) => {
|
||||
|
||||
const response = item.response || {};
|
||||
|
||||
const messagesCount = Array.isArray(response.responses) ? response.responses.length : 0;
|
||||
const headersCount = response.headers ? Object.keys(response.headers).length : 0;
|
||||
|
||||
const allTabs = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'response',
|
||||
label: 'Messages',
|
||||
indicator: messagesCount > 0 ? <sup className="ml-1 font-medium">{messagesCount}</sup> : null
|
||||
},
|
||||
{
|
||||
key: 'headers',
|
||||
label: 'Headers',
|
||||
indicator: headersCount > 0 ? <sup className="ml-1 font-medium">{headersCount}</sup> : null
|
||||
},
|
||||
{
|
||||
key: 'timeline',
|
||||
label: 'Timeline',
|
||||
indicator: null
|
||||
}
|
||||
];
|
||||
}, [messagesCount, headersCount]);
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
@@ -81,62 +104,40 @@ const WSResponsePane = ({ item, collection }) => {
|
||||
return <div className="pb-4 px-4">An error occurred!</div>;
|
||||
}
|
||||
|
||||
const tabConfig = [
|
||||
{
|
||||
name: 'response',
|
||||
label: 'Messages',
|
||||
count: Array.isArray(response.responses) ? response.responses.length : 0
|
||||
},
|
||||
{
|
||||
name: 'headers',
|
||||
label: 'Headers',
|
||||
count: response.headers ? Object.keys(response.headers).length : 0
|
||||
},
|
||||
{
|
||||
name: 'timeline',
|
||||
label: 'Timeline'
|
||||
}
|
||||
];
|
||||
const rightContent = !isLoading ? (
|
||||
<div ref={rightContentRef} className="flex items-center">
|
||||
{focusedTab?.responsePaneTab === 'timeline' ? (
|
||||
<>
|
||||
<ResponseLayoutToggle />
|
||||
<ClearTimeline item={item} collection={collection} />
|
||||
</>
|
||||
) : item?.response ? (
|
||||
<>
|
||||
<ResponseLayoutToggle />
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<WSStatusCode
|
||||
status={response.statusCode}
|
||||
text={response.statusText}
|
||||
details={response.statusDescription}
|
||||
/>
|
||||
<ResponseTime duration={response.duration} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center pl-3 pr-4 tabs" role="tablist">
|
||||
{tabConfig.map((tab) => (
|
||||
<Tab
|
||||
key={tab.name}
|
||||
name={tab.name}
|
||||
label={tab.label}
|
||||
isActive={focusedTab.responsePaneTab === tab.name}
|
||||
onClick={selectTab}
|
||||
count={tab.count}
|
||||
/>
|
||||
))}
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
{focusedTab?.responsePaneTab === 'timeline' ? (
|
||||
<>
|
||||
<ResponseLayoutToggle />
|
||||
<ClearTimeline item={item} collection={collection} />
|
||||
</>
|
||||
) : item?.response ? (
|
||||
<>
|
||||
<ResponseLayoutToggle />
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<WSResponseSortOrder item={item} collection={collection} />
|
||||
<WSStatusCode
|
||||
status={response.statusCode}
|
||||
text={response.statusText}
|
||||
details={response.statusDescription}
|
||||
/>
|
||||
<ResponseTime duration={response.duration} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="px-4">
|
||||
<ResponsiveTabs
|
||||
tabs={allTabs}
|
||||
activeTab={focusedTab.responsePaneTab}
|
||||
onTabSelect={selectTab}
|
||||
rightContent={rightContent}
|
||||
rightContentRef={rightContentRef}
|
||||
/>
|
||||
</div>
|
||||
<section
|
||||
className="flex flex-col flex-grow pl-3 pr-4 h-0 mt-4"
|
||||
>
|
||||
<section className="flex flex-col flex-grow px-4 h-0 mt-4">
|
||||
{isLoading ? <Overlay item={item} collection={collection} /> : null}
|
||||
{!item?.response ? (
|
||||
focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? (
|
||||
|
||||
@@ -42,9 +42,12 @@ const ResponsePane = ({ item, collection }) => {
|
||||
const focusedTab = find(tabs, (t) => t.uid === activeTabUid);
|
||||
|
||||
// Initialize format and tab only once when data loads.
|
||||
const { initialFormat, initialTab } = useInitialResponseFormat(response?.dataBuffer, response?.headers);
|
||||
const { initialFormat, initialTab, contentType } = useInitialResponseFormat(response?.dataBuffer, response?.headers);
|
||||
const previewFormatOptions = useResponsePreviewFormatOptions(response?.dataBuffer, response?.headers);
|
||||
|
||||
// Track previous response headers to detect when content-type changes
|
||||
const previousContentRef = useRef(contentType);
|
||||
|
||||
const persistedFormat = focusedTab?.responseFormat;
|
||||
const persistedViewTab = focusedTab?.responseViewTab;
|
||||
|
||||
@@ -56,13 +59,19 @@ const ResponsePane = ({ item, collection }) => {
|
||||
if (!focusedTab || initialFormat === null || initialTab === null) {
|
||||
return;
|
||||
}
|
||||
if (persistedFormat === null) {
|
||||
|
||||
// Check if response headers (content-type) changed using deep comparison
|
||||
const contentTypeChanged = contentType !== previousContentRef.current;
|
||||
if (contentTypeChanged) {
|
||||
previousContentRef.current = contentType;
|
||||
}
|
||||
if (contentTypeChanged || persistedFormat === null) {
|
||||
dispatch(updateResponseFormat({ uid: item.uid, responseFormat: initialFormat }));
|
||||
}
|
||||
if (persistedViewTab === null) {
|
||||
if (contentTypeChanged || persistedViewTab === null) {
|
||||
dispatch(updateResponseViewTab({ uid: item.uid, responseViewTab: initialTab }));
|
||||
}
|
||||
}, [initialFormat, initialTab, persistedFormat, persistedViewTab, focusedTab, item.uid, dispatch]);
|
||||
}, [contentType, initialFormat, initialTab, persistedFormat, persistedViewTab, focusedTab, item.uid, dispatch]);
|
||||
|
||||
const handleFormatChange = useCallback((newFormat) => {
|
||||
dispatch(updateResponseFormat({ uid: item.uid, responseFormat: newFormat }));
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
|
||||
.search-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
input#search-input {
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: ${(props) => props.theme.input.focusBorder};
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.input.placeholder.color};
|
||||
opacity: ${(props) => props.theme.input.placeholder.opacity};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IconSearch, IconX } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const SearchInput = ({
|
||||
searchText,
|
||||
@@ -17,9 +18,9 @@ const SearchInput = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`relative px-2 ${className}`}>
|
||||
<StyledWrapper className={`px-2 ${className}`}>
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
<span className="text-gray-500">
|
||||
<span className="search-icon">
|
||||
<IconSearch size={16} strokeWidth={1.5} />
|
||||
</span>
|
||||
</div>
|
||||
@@ -50,7 +51,7 @@ const SearchInput = ({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,14 @@ const CloneCollection = ({ onClose, collectionUid }) => {
|
||||
const [isEditing, toggleEditing] = useState(false);
|
||||
const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const defaultLocation = get(preferences, 'general.defaultCollectionLocation', '');
|
||||
const workspaces = useSelector((state) => state.workspaces?.workspaces || []);
|
||||
const workspaceUid = useSelector((state) => state.workspaces?.activeWorkspaceUid);
|
||||
const activeWorkspace = workspaces.find((w) => w.uid === workspaceUid);
|
||||
const isDefaultWorkspace = activeWorkspace?.type === 'default';
|
||||
|
||||
const defaultLocation = isDefaultWorkspace
|
||||
? get(preferences, 'general.defaultCollectionLocation', '')
|
||||
: (activeWorkspace?.pathname ? `${activeWorkspace.pathname}/collections` : '');
|
||||
const { name } = collection;
|
||||
|
||||
const formik = useFormik({
|
||||
|
||||
@@ -110,12 +110,14 @@ const GenerateCodeItem = ({ collectionUid, item, onClose, isExample = false, exa
|
||||
// Resolve auth inheritance
|
||||
const resolvedRequest = resolveInheritedAuth(item, collection);
|
||||
|
||||
// Create the final item for code generation
|
||||
// requestData.request contains either the normal request or example request data.
|
||||
// We explicitly set auth from resolvedRequest to ensure inherited auth
|
||||
// (from folders/collection) is resolved correctly in generated code.
|
||||
const finalItem = {
|
||||
...item,
|
||||
request: {
|
||||
...resolvedRequest,
|
||||
...requestData.request,
|
||||
auth: resolvedRequest.auth,
|
||||
url: finalUrl
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,71 +1,38 @@
|
||||
import { interpolate } from '@usebruno/common';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { isPlainObject, mapValues } from 'lodash-es';
|
||||
|
||||
export const interpolateHeaders = (headers = [], variables = {}) => {
|
||||
return headers.map((header) => ({
|
||||
...header,
|
||||
name: interpolate(header.name, variables),
|
||||
value: interpolate(header.value, variables)
|
||||
}));
|
||||
};
|
||||
/**
|
||||
* Traverses an object and interpolates any strings it finds.
|
||||
*/
|
||||
export const interpolateObject = (obj, variables) => {
|
||||
const seen = new WeakSet();
|
||||
|
||||
export const interpolateBody = (body, variables = {}) => {
|
||||
if (!body) return null;
|
||||
const walk = (value) => {
|
||||
if (value == null) return value;
|
||||
|
||||
const interpolatedBody = cloneDeep(body);
|
||||
if (typeof value === 'string') {
|
||||
return interpolate(value, variables);
|
||||
}
|
||||
|
||||
switch (body.mode) {
|
||||
case 'json':
|
||||
let parsed = body.json;
|
||||
// If it's already a string, use it directly; if it's an object, stringify it first
|
||||
if (typeof parsed === 'object') {
|
||||
parsed = JSON.stringify(parsed);
|
||||
if (typeof value === 'object') {
|
||||
if (seen.has(value)) {
|
||||
throw new Error(
|
||||
'Circular reference detected during interpolation.'
|
||||
);
|
||||
}
|
||||
parsed = interpolate(parsed, variables, { escapeJSONStrings: true });
|
||||
try {
|
||||
const jsonObj = JSON.parse(parsed);
|
||||
interpolatedBody.json = JSON.stringify(jsonObj, null, 2);
|
||||
} catch {
|
||||
interpolatedBody.json = parsed;
|
||||
}
|
||||
break;
|
||||
seen.add(value);
|
||||
}
|
||||
|
||||
case 'text':
|
||||
interpolatedBody.text = interpolate(body.text, variables);
|
||||
break;
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(walk);
|
||||
}
|
||||
|
||||
case 'xml':
|
||||
interpolatedBody.xml = interpolate(body.xml, variables);
|
||||
break;
|
||||
if (isPlainObject(value)) {
|
||||
return mapValues(value, walk);
|
||||
}
|
||||
|
||||
case 'sparql':
|
||||
interpolatedBody.sparql = interpolate(body.sparql, variables);
|
||||
break;
|
||||
return value;
|
||||
};
|
||||
|
||||
case 'formUrlEncoded':
|
||||
interpolatedBody.formUrlEncoded = Array.isArray(body.formUrlEncoded)
|
||||
? body.formUrlEncoded.map((param) => ({
|
||||
...param,
|
||||
value: param.enabled ? interpolate(param.value, variables) : param.value
|
||||
}))
|
||||
: [];
|
||||
break;
|
||||
|
||||
case 'multipartForm':
|
||||
interpolatedBody.multipartForm = Array.isArray(body.multipartForm)
|
||||
? body.multipartForm.map((param) => ({
|
||||
...param,
|
||||
value:
|
||||
param.type === 'text' && param.enabled
|
||||
? interpolate(param.value, variables)
|
||||
: param.value
|
||||
}))
|
||||
: [];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return interpolatedBody;
|
||||
return walk(obj);
|
||||
};
|
||||
|
||||
@@ -1,35 +1,84 @@
|
||||
import { interpolateHeaders, interpolateBody } from './interpolation';
|
||||
import { interpolateObject } from './interpolation';
|
||||
|
||||
describe('interpolation utils', () => {
|
||||
describe('interpolateHeaders', () => {
|
||||
it('should interpolate variables in header name and value while preserving other props', () => {
|
||||
const headers = [
|
||||
{ uid: '1', name: 'X-{{var}}', value: 'value-{{var}}', enabled: true }
|
||||
];
|
||||
const variables = { var: 'test' };
|
||||
|
||||
const result = interpolateHeaders(headers, variables);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
uid: '1',
|
||||
name: 'X-test',
|
||||
value: 'value-test',
|
||||
enabled: true
|
||||
describe('interpolateObject', () => {
|
||||
it('should interpolate variables across all data types and nesting levels', () => {
|
||||
const complexRequest = {
|
||||
url: 'https://{{host}}/api',
|
||||
method: 'POST',
|
||||
headers: [{ name: 'X-{{headerName}}', value: '{{headerValue}}', enabled: true }],
|
||||
auth: {
|
||||
basic: {
|
||||
username: '{{user}}',
|
||||
password: 'pass-{{passVar}}'
|
||||
}
|
||||
},
|
||||
body: {
|
||||
mode: 'json',
|
||||
json: '{"id": "{{id}}"}'
|
||||
},
|
||||
params: {
|
||||
someArray: ['tag-{{id}}', 'stable'],
|
||||
value: 100,
|
||||
enabled: true,
|
||||
isNull: null
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interpolateBody', () => {
|
||||
it('should interpolate JSON body strings and keep formatting', () => {
|
||||
const body = {
|
||||
mode: 'json',
|
||||
json: '{"name": "{{username}}"}'
|
||||
};
|
||||
const variables = { username: 'bruno' };
|
||||
|
||||
const result = interpolateBody(body, variables);
|
||||
expect(result.json).toBe('{\n "name": "bruno"\n}');
|
||||
const variables = {
|
||||
host: 'api.example.com',
|
||||
headerName: 'App-ID',
|
||||
headerValue: 'val-123',
|
||||
user: 'admin',
|
||||
passVar: 'secure',
|
||||
id: '99'
|
||||
};
|
||||
|
||||
const result = interpolateObject(complexRequest, variables);
|
||||
|
||||
expect(result).toEqual({
|
||||
url: 'https://api.example.com/api',
|
||||
method: 'POST',
|
||||
headers: [{ name: 'X-App-ID', value: 'val-123', enabled: true }],
|
||||
auth: {
|
||||
basic: {
|
||||
username: 'admin',
|
||||
password: 'pass-secure'
|
||||
}
|
||||
},
|
||||
body: {
|
||||
mode: 'json',
|
||||
json: '{"id": "99"}'
|
||||
},
|
||||
params: {
|
||||
someArray: ['tag-99', 'stable'],
|
||||
value: 100,
|
||||
enabled: true,
|
||||
isNull: null
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should not iterate endlessly for circular references', () => {
|
||||
const variables = { x: 'ok' };
|
||||
|
||||
const obj = { value: '{{x}}' };
|
||||
obj.self = obj;
|
||||
|
||||
expect(() => interpolateObject(obj, variables)).toThrow('Circular reference detected during interpolation.');
|
||||
});
|
||||
|
||||
it('should leave the placeholder intact if the variable is missing', () => {
|
||||
const variables = { known: 'value' };
|
||||
const obj = {
|
||||
field: '{{known}} and {{missing}}'
|
||||
};
|
||||
|
||||
const result = interpolateObject(obj, variables);
|
||||
|
||||
expect(result).toEqual({
|
||||
field: 'value and {{missing}}'
|
||||
});
|
||||
});
|
||||
|
||||
it('should interpolate text body', () => {
|
||||
@@ -37,12 +86,12 @@ describe('interpolation utils', () => {
|
||||
mode: 'text',
|
||||
text: 'Hello {{name}}'
|
||||
};
|
||||
const result = interpolateBody(body, { name: 'World' });
|
||||
const result = interpolateObject(body, { name: 'World' });
|
||||
expect(result.text).toBe('Hello World');
|
||||
});
|
||||
|
||||
it('should return null when body is null', () => {
|
||||
expect(interpolateBody(null, { a: 1 })).toBeNull();
|
||||
expect(interpolateObject(null, { a: 1 })).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { buildHarRequest } from 'utils/codegenerator/har';
|
||||
import { getAuthHeaders } from 'utils/codegenerator/auth';
|
||||
import { getAllVariables, getTreePathFromCollectionToItem, mergeHeaders } from 'utils/collections/index';
|
||||
import { interpolateHeaders, interpolateBody } from './interpolation';
|
||||
import { getAllVariables, getTreePathFromCollectionToItem, mergeHeaders } from 'utils/collections';
|
||||
import { interpolateObject } from './interpolation';
|
||||
import { get } from 'lodash';
|
||||
|
||||
const generateSnippet = ({ language, item, collection, shouldInterpolate = false }) => {
|
||||
@@ -11,7 +11,11 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
|
||||
|
||||
const variables = getAllVariables(collection, item);
|
||||
|
||||
const request = item.request;
|
||||
let request = item.request;
|
||||
|
||||
if (shouldInterpolate) {
|
||||
request = interpolateObject(request, variables);
|
||||
}
|
||||
|
||||
// Get the request tree path and merge headers
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
@@ -20,18 +24,10 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
|
||||
// Add auth headers if needed
|
||||
if (request.auth && request.auth.mode !== 'none') {
|
||||
const collectionAuth = collection?.draft?.root ? get(collection, 'draft.root.request.auth', null) : get(collection, 'root.request.auth', null);
|
||||
const authHeaders = getAuthHeaders(collectionAuth, request.auth);
|
||||
const authHeaders = getAuthHeaders(collectionAuth, request.auth, collection, item);
|
||||
headers = [...headers, ...authHeaders];
|
||||
}
|
||||
|
||||
// Interpolate headers and body if needed
|
||||
if (shouldInterpolate) {
|
||||
headers = interpolateHeaders(headers, variables);
|
||||
if (request.body) {
|
||||
request.body = interpolateBody(request.body, variables);
|
||||
}
|
||||
}
|
||||
|
||||
// Build HAR request
|
||||
const harRequest = buildHarRequest({
|
||||
request,
|
||||
@@ -40,9 +36,8 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
|
||||
|
||||
// Generate snippet using HTTPSnippet
|
||||
const snippet = new HTTPSnippet(harRequest);
|
||||
const result = snippet.convert(language.target, language.client);
|
||||
|
||||
return result;
|
||||
return snippet.convert(language.target, language.client);
|
||||
} catch (error) {
|
||||
console.error('Error generating code snippet:', error);
|
||||
return 'Error generating code snippet';
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getAuthHeaders } from 'utils/codegenerator/auth';
|
||||
|
||||
jest.mock('httpsnippet', () => {
|
||||
return {
|
||||
HTTPSnippet: jest.fn().mockImplementation((harRequest) => ({
|
||||
@@ -56,7 +58,9 @@ jest.mock('utils/collections/index', () => {
|
||||
...collection?.processEnvVariables,
|
||||
baseUrl: 'https://api.example.com',
|
||||
apiKey: 'secret-key-123',
|
||||
userId: '12345'
|
||||
userId: '12345',
|
||||
user: 'admin',
|
||||
pass: 'secret123'
|
||||
})),
|
||||
getTreePathFromCollectionToItem: jest.fn(() => [])
|
||||
};
|
||||
@@ -146,11 +150,8 @@ describe('Snippet Generator - Simple Tests', () => {
|
||||
shouldInterpolate: true
|
||||
});
|
||||
|
||||
const expectedBody = `{
|
||||
"message": "Hello World",
|
||||
"count": 42
|
||||
}`;
|
||||
expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedBody}'`);
|
||||
const expectedBody = `{"message": "Hello World", "count": 42}`;
|
||||
expect(result).toBe(`curl -X POST https://api.example.com/data -H "Content-Type: application/json" -d '${expectedBody}'`);
|
||||
});
|
||||
|
||||
it('should handle GET requests', () => {
|
||||
@@ -203,11 +204,8 @@ describe('Snippet Generator - Simple Tests', () => {
|
||||
});
|
||||
|
||||
// Body should have interpolated variables with proper formatting
|
||||
const expectedBody = `{
|
||||
"message": "Hello World",
|
||||
"count": 42
|
||||
}`;
|
||||
expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedBody}'`);
|
||||
const expectedBody = `{"message": "Hello World", "count": 42}`;
|
||||
expect(result).toBe(`curl -X POST https://api.example.com/data -H "Content-Type: application/json" -d '${expectedBody}'`);
|
||||
});
|
||||
|
||||
it('should handle complex nested JSON body', () => {
|
||||
@@ -269,7 +267,7 @@ describe('Snippet Generator - Simple Tests', () => {
|
||||
}
|
||||
}, null, 2);
|
||||
|
||||
expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedComplexBody}'`);
|
||||
expect(result).toBe(`curl -X POST https://api.example.com/data -H "Content-Type: application/json" -d '${expectedComplexBody}'`);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', () => {
|
||||
@@ -369,13 +367,9 @@ describe('Snippet Generator - Simple Tests', () => {
|
||||
shouldInterpolate: true
|
||||
});
|
||||
|
||||
const expectedInterpolatedBody = `{
|
||||
"name": "John Smith",
|
||||
"email": "john@test.com",
|
||||
"age": 30
|
||||
}`;
|
||||
const expectedInterpolatedBody = `{"name": "John Smith", "email": "john@test.com", "age": 30}`;
|
||||
|
||||
expect(result).toBe(`curl -X POST https://api.test.com/{{endpoint}} -H "Content-Type: application/json" -d '${expectedInterpolatedBody}'`);
|
||||
expect(result).toBe(`curl -X POST https://api.test.com/users -H "Content-Type: application/json" -d '${expectedInterpolatedBody}'`);
|
||||
});
|
||||
|
||||
it('should NOT interpolate when shouldInterpolate is false', () => {
|
||||
@@ -424,7 +418,61 @@ describe('Snippet Generator - Simple Tests', () => {
|
||||
shouldInterpolate: false
|
||||
});
|
||||
|
||||
expect(result).toBe('curl -X POST https://api.test.com/{{endpoint}} -H "Content-Type: application/json" -d \'{"name": "{{userName}}", "email": "{{userEmail}}", "age": {{userAge}}}\'');
|
||||
expect(result).toBe(
|
||||
'curl -X POST https://api.test.com/{{endpoint}} -H "Content-Type: application/json" -d \'{"name": "{{userName}}", "email": "{{userEmail}}", "age": {{userAge}}}\''
|
||||
);
|
||||
});
|
||||
|
||||
it('should interpolate basic auth credentials correctly', () => {
|
||||
const item = {
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com',
|
||||
auth: {
|
||||
mode: 'basic',
|
||||
basic: {
|
||||
username: '{{user}}',
|
||||
password: '{{pass}}'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const collection = {
|
||||
root: {
|
||||
request: {
|
||||
vars: {
|
||||
req: [
|
||||
{ name: 'user', value: 'admin', enabled: true },
|
||||
{ name: 'pass', value: 'secret123', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { HTTPSnippet: mockedHTTPSnippet } = require('httpsnippet');
|
||||
const { getAuthHeaders: actualGetAuthHeaders } = jest.requireActual('utils/codegenerator/auth');
|
||||
getAuthHeaders.mockImplementation(actualGetAuthHeaders);
|
||||
|
||||
const language = { target: 'shell', client: 'curl' };
|
||||
|
||||
generateSnippet({
|
||||
language,
|
||||
item,
|
||||
collection,
|
||||
shouldInterpolate: true
|
||||
});
|
||||
|
||||
const harRequest = mockedHTTPSnippet.mock.calls[0][0];
|
||||
|
||||
// "admin:secret123" encoded is "YWRtaW46c2VjcmV0MTIz"
|
||||
expect(harRequest.headers).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: 'Authorization',
|
||||
value: 'Basic YWRtaW46c2VjcmV0MTIz'
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -554,3 +602,223 @@ describe('generateSnippet with edge-case bodies', () => {
|
||||
expect(result).toMatch(/^curl -X POST/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateSnippet with OAuth2 authentication', () => {
|
||||
const language = { target: 'shell', client: 'curl' };
|
||||
const baseCollection = { root: { request: { auth: { mode: 'none' }, headers: [] } } };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Mock getAuthHeaders to return OAuth2 headers based on the auth config
|
||||
const authUtils = require('utils/codegenerator/auth');
|
||||
authUtils.getAuthHeaders.mockImplementation((collectionRootAuth, requestAuth, collection = null, item = null) => {
|
||||
if (requestAuth?.mode === 'oauth2') {
|
||||
const oauth2Config = requestAuth.oauth2 || {};
|
||||
const tokenPlacement = oauth2Config.tokenPlacement || 'header';
|
||||
// Use the actual value from config, defaulting to 'Bearer' only if undefined
|
||||
// Empty string should be preserved to test no-prefix scenarios
|
||||
const tokenHeaderPrefix = oauth2Config.tokenHeaderPrefix !== undefined
|
||||
? oauth2Config.tokenHeaderPrefix
|
||||
: 'Bearer';
|
||||
let accessToken = oauth2Config.accessToken || '<access_token>';
|
||||
|
||||
// If collection and item are provided, try to look up stored credentials
|
||||
if (collection && item && collection.oauth2Credentials) {
|
||||
const grantType = oauth2Config.grantType || '';
|
||||
const urlToLookup = grantType === 'implicit'
|
||||
? oauth2Config.authorizationUrl || ''
|
||||
: oauth2Config.accessTokenUrl || '';
|
||||
const credentialsId = oauth2Config.credentialsId || 'credentials';
|
||||
const collectionUid = collection.uid;
|
||||
|
||||
if (urlToLookup && collectionUid) {
|
||||
// Look up stored credentials (simplified - assumes URL is already interpolated in test data)
|
||||
const credentialsData = collection.oauth2Credentials.find(
|
||||
(creds) =>
|
||||
creds?.url === urlToLookup
|
||||
&& creds?.collectionUid === collectionUid
|
||||
&& creds?.credentialsId === credentialsId
|
||||
);
|
||||
|
||||
if (credentialsData?.credentials?.access_token) {
|
||||
accessToken = credentialsData.credentials.access_token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenPlacement === 'header') {
|
||||
// Always trim the final result for consistent formatting
|
||||
const headerValue = tokenHeaderPrefix
|
||||
? `${tokenHeaderPrefix} ${accessToken}`.trim()
|
||||
: accessToken.trim();
|
||||
return [
|
||||
{
|
||||
enabled: true,
|
||||
name: 'Authorization',
|
||||
value: headerValue
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
});
|
||||
|
||||
it('should include OAuth2 Bearer token in Authorization header when tokenPlacement is header', () => {
|
||||
const item = {
|
||||
uid: 'oauth-req',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/users',
|
||||
headers: [],
|
||||
auth: {
|
||||
mode: 'oauth2',
|
||||
oauth2: {
|
||||
grantType: 'client_credentials',
|
||||
tokenPlacement: 'header',
|
||||
tokenHeaderPrefix: 'Bearer',
|
||||
accessToken: 'test-access-token-123'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
|
||||
|
||||
const harUtils = require('utils/codegenerator/har');
|
||||
const harCall = harUtils.buildHarRequest.mock.calls[0][0];
|
||||
expect(harCall.headers).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'Authorization',
|
||||
value: 'Bearer test-access-token-123'
|
||||
})
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should use custom tokenHeaderPrefix when provided', () => {
|
||||
const item = {
|
||||
uid: 'oauth-req-custom',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/users',
|
||||
headers: [],
|
||||
auth: {
|
||||
mode: 'oauth2',
|
||||
oauth2: {
|
||||
grantType: 'client_credentials',
|
||||
tokenPlacement: 'header',
|
||||
tokenHeaderPrefix: 'OAuth',
|
||||
accessToken: 'custom-token-456'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
|
||||
|
||||
const harUtils = require('utils/codegenerator/har');
|
||||
const harCall = harUtils.buildHarRequest.mock.calls[0][0];
|
||||
expect(harCall.headers).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'Authorization',
|
||||
value: 'OAuth custom-token-456'
|
||||
})
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should not include Authorization header when tokenPlacement is url', () => {
|
||||
const item = {
|
||||
uid: 'oauth-req-url',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/users',
|
||||
headers: [],
|
||||
auth: {
|
||||
mode: 'oauth2',
|
||||
oauth2: {
|
||||
grantType: 'client_credentials',
|
||||
tokenPlacement: 'url',
|
||||
tokenQueryKey: 'access_token',
|
||||
accessToken: 'token-in-url'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
|
||||
|
||||
const harUtils = require('utils/codegenerator/har');
|
||||
const harCall = harUtils.buildHarRequest.mock.calls[0][0];
|
||||
const authHeader = harCall.headers.find((h) => h.name === 'Authorization');
|
||||
expect(authHeader).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should use placeholder when accessToken is not available', () => {
|
||||
const item = {
|
||||
uid: 'oauth-req-placeholder',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/users',
|
||||
headers: [],
|
||||
auth: {
|
||||
mode: 'oauth2',
|
||||
oauth2: {
|
||||
grantType: 'client_credentials',
|
||||
tokenPlacement: 'header',
|
||||
tokenHeaderPrefix: 'Bearer'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
|
||||
|
||||
const harUtils = require('utils/codegenerator/har');
|
||||
const harCall = harUtils.buildHarRequest.mock.calls[0][0];
|
||||
expect(harCall.headers).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'Authorization',
|
||||
value: 'Bearer <access_token>'
|
||||
})
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty tokenHeaderPrefix', () => {
|
||||
const item = {
|
||||
uid: 'oauth-req-no-prefix',
|
||||
request: {
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/users',
|
||||
headers: [],
|
||||
auth: {
|
||||
mode: 'oauth2',
|
||||
oauth2: {
|
||||
grantType: 'client_credentials',
|
||||
tokenPlacement: 'header',
|
||||
tokenHeaderPrefix: '',
|
||||
accessToken: 'token-without-prefix'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
|
||||
|
||||
const harUtils = require('utils/codegenerator/har');
|
||||
const harCall = harUtils.buildHarRequest.mock.calls[0][0];
|
||||
expect(harCall.headers).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'Authorization',
|
||||
value: 'token-without-prefix'
|
||||
})
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,30 @@ const StyledWrapper = styled.div`
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
border-radius: ${(props) => props.theme.border.radius.md};
|
||||
overflow: hidden;
|
||||
border: 1px solid ${(props) => props.theme.border.border1};
|
||||
|
||||
.preview-label {
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 500;
|
||||
color: #3b82f6;
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
border: 1px dashed rgba(59, 130, 246, 0.4);
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.features {
|
||||
li {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -9,6 +9,7 @@ import { IconBook, IconCheck, IconAlertTriangle, IconLoader2 } from '@tabler/ico
|
||||
|
||||
import Modal from 'components/Modal';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import demoImage from './demo.png';
|
||||
import { useApp } from 'providers/App';
|
||||
import { transformCollectionToSaveToExportAsFile, findCollectionByUid, areItemsLoading } from 'utils/collections/index';
|
||||
import { brunoToOpenCollection } from '@usebruno/converters';
|
||||
@@ -141,12 +142,16 @@ const GenerateDocumentation = ({ onClose, collectionUid }) => {
|
||||
<IconBook size={18} />
|
||||
<span>Interactive API Documentation</span>
|
||||
</h3>
|
||||
<p className="description mb-5">
|
||||
Generate a standalone HTML file containing interactive documentation for your API collection.
|
||||
This file can be hosted anywhere or shared with your team.
|
||||
<p className="description mb-4">
|
||||
Generate a standalone HTML file that can be hosted anywhere or shared with your team.
|
||||
</p>
|
||||
|
||||
<ul className="features flex flex-col list-none gap-2.5 p-0 mb-5">
|
||||
<div className="preview-container relative mb-4">
|
||||
<span className="preview-label absolute">Sample Output</span>
|
||||
<img src={demoImage} alt="Documentation preview" className="preview-image" />
|
||||
</div>
|
||||
|
||||
<ul className="features flex flex-col list-none gap-2 p-0 mb-4">
|
||||
{FEATURES.map((feature) => (
|
||||
<li key={feature} className="flex items-center gap-2.5">
|
||||
<IconCheck size={16} className="check-icon flex-shrink-0" />
|
||||
@@ -156,7 +161,7 @@ const GenerateDocumentation = ({ onClose, collectionUid }) => {
|
||||
</ul>
|
||||
|
||||
<p className="note m-0">
|
||||
The generated file does not embed all assets. It loads OpenCollection’s JavaScript and CSS files from a CDN when viewing docs, which requires an internet connection.
|
||||
The generated file loads OpenCollection's JavaScript and CSS files from a CDN, which requires an internet connection.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -14,6 +14,18 @@ const StyledWrapper = styled.div`
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
}
|
||||
|
||||
.discussion-link {
|
||||
margin-left: 0.5rem;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.textLink};
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.report-issue-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -224,7 +224,19 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) =>
|
||||
</p>
|
||||
</Help>
|
||||
{formik.values.format === 'yml' && (
|
||||
<span className="beta-badge">Beta</span>
|
||||
<>
|
||||
<span className="beta-badge">Beta</span>
|
||||
<a
|
||||
href="#"
|
||||
className="discussion-link"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.open('https://github.com/usebruno/bruno/discussions/6634', '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
>
|
||||
Join the discussion
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
<select
|
||||
@@ -240,21 +252,6 @@ const CreateCollection = ({ onClose, defaultLocation: propDefaultLocation }) =>
|
||||
{formik.touched.format && formik.errors.format ? (
|
||||
<div className="text-red-500">{formik.errors.format}</div>
|
||||
) : null}
|
||||
{formik.values.format === 'yml' && (
|
||||
<div className="mt-2">
|
||||
<a
|
||||
href="#"
|
||||
className="report-issue-link"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.open('https://github.com/usebruno/bruno/discussions/6466', '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
>
|
||||
<IconExternalLink size={14} strokeWidth={1.5} />
|
||||
<span>Report an issue</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end items-center mt-8 bruno-modal-footer">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { TableVirtuoso } from 'react-virtuoso';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
@@ -19,6 +20,14 @@ import { Tooltip } from 'react-tooltip';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const MIN_H = 35 * 2;
|
||||
|
||||
const TableRow = React.memo(({ children, item }) => <tr key={item.uid} data-testid={`env-var-row-${item.name}`}>{children}</tr>, (prevProps, nextProps) => {
|
||||
const prevUid = prevProps?.item?.uid;
|
||||
const nextUid = nextProps?.item?.uid;
|
||||
return prevUid === nextUid && prevProps.children === nextProps.children;
|
||||
});
|
||||
|
||||
const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentVariables, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
@@ -28,6 +37,12 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
|
||||
const hasDraftForThisEnv = globalEnvironmentDraft?.environmentUid === environment.uid;
|
||||
|
||||
const [tableHeight, setTableHeight] = React.useState(MIN_H);
|
||||
|
||||
const handleTotalHeightChanged = React.useCallback((h) => {
|
||||
setTableHeight(h);
|
||||
}, []);
|
||||
|
||||
// Track environment changes for draft restoration
|
||||
const prevEnvUidRef = React.useRef(null);
|
||||
const mountedRef = React.useRef(false);
|
||||
@@ -322,109 +337,108 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td className="text-center"></td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td className="text-center">Secret</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formik.values.map((variable, index) => {
|
||||
const isLastRow = index === formik.values.length - 1;
|
||||
const isEmptyRow = !variable.name || variable.name.trim() === '';
|
||||
const isLastEmptyRow = isLastRow && isEmptyRow;
|
||||
<TableVirtuoso
|
||||
className="table-container"
|
||||
style={{ height: tableHeight }}
|
||||
totalListHeightChanged={handleTotalHeightChanged}
|
||||
data={formik.values}
|
||||
fixedItemHeight={35}
|
||||
components={{ TableRow }}
|
||||
computeItemKey={(index, variable) => variable.uid}
|
||||
fixedHeaderContent={() => (
|
||||
<tr>
|
||||
<td className="text-center"></td>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td className="text-center">Secret</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
)}
|
||||
itemContent={(index, variable) => {
|
||||
const isLastRow = index === formik.values.length - 1;
|
||||
const isEmptyRow = !variable.name || variable.name.trim() === '';
|
||||
const isLastEmptyRow = isLastRow && isEmptyRow;
|
||||
|
||||
return (
|
||||
<tr key={variable.uid}>
|
||||
<td className="text-center">
|
||||
{!isLastEmptyRow && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
placeholder={isLastEmptyRow ? 'Name' : ''}
|
||||
onChange={(e) => handleNameChange(index, e)}
|
||||
onBlur={() => handleNameBlur(index)}
|
||||
onKeyDown={(e) => handleNameKeyDown(index, e)}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} index={index} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<MultiLineEditor
|
||||
theme={storedTheme}
|
||||
collection={_collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
isSecret={variable.secret}
|
||||
readOnly={typeof variable.value !== 'string'}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
{typeof variable.value !== 'string' && (
|
||||
<span className="ml-2 flex items-center">
|
||||
<IconInfoCircle
|
||||
id={`${variable.uid}-disabled-info-icon`}
|
||||
className="text-muted"
|
||||
size={16}
|
||||
/>
|
||||
<Tooltip
|
||||
anchorId={`${variable.uid}-disabled-info-icon`}
|
||||
content="Non-string values set via scripts are read-only and can only be updated through scripts."
|
||||
place="top"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{!isLastEmptyRow && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{!isLastEmptyRow && (
|
||||
<button onClick={() => handleRemoveVar(variable.uid)}>
|
||||
<IconTrash strokeWidth={1.5} size={18} />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<td className="text-center">
|
||||
{!isLastEmptyRow && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={variable.name}
|
||||
placeholder={isLastEmptyRow ? 'Name' : ''}
|
||||
onChange={(e) => handleNameChange(index, e)}
|
||||
onBlur={() => handleNameBlur(index)}
|
||||
onKeyDown={(e) => handleNameKeyDown(index, e)}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} index={index} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex flex-row flex-nowrap items-center">
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<MultiLineEditor
|
||||
theme={storedTheme}
|
||||
collection={_collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
isSecret={variable.secret}
|
||||
readOnly={typeof variable.value !== 'string'}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
{typeof variable.value !== 'string' && (
|
||||
<span className="ml-2 flex items-center">
|
||||
<IconInfoCircle id={`${variable.uid}-disabled-info-icon`} className="text-muted" size={16} />
|
||||
<Tooltip
|
||||
anchorId={`${variable.uid}-disabled-info-icon`}
|
||||
content="Non-string values set via scripts are read-only and can only be updated through scripts."
|
||||
place="top"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{!isLastEmptyRow && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{!isLastEmptyRow && (
|
||||
<button onClick={() => handleRemoveVar(variable.uid)}>
|
||||
<IconTrash strokeWidth={1.5} size={18} />
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="button-container mt-5">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -191,9 +191,12 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.environment-name-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
@@ -210,12 +213,14 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-left: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.creating {
|
||||
.environment-name-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
@@ -232,6 +237,7 @@ const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-left: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inline-action-btn {
|
||||
|
||||
@@ -11,6 +11,8 @@ import WorkspaceTabs from 'components/WorkspaceTabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { getRevealInFolderLabel } from 'utils/common/platform';
|
||||
import { getWorkspaceDisplayName } from 'components/AppTitleBar';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const WorkspaceHome = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -208,7 +210,7 @@ const WorkspaceHome = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span>{activeWorkspace.name}</span>
|
||||
<span className={classNames('workspace-name', { 'italic text-muted': !activeWorkspace?.name })}>{getWorkspaceDisplayName(activeWorkspace.name)}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -324,6 +324,27 @@ const GlobalStyle = createGlobalStyle`
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-tooltip {
|
||||
padding: 4px 8px;
|
||||
background-color: ${(props) => props.theme.infoTip.bg};
|
||||
border: 1px solid ${(props) => props.theme.infoTip.border};
|
||||
box-shadow: ${(props) => props.theme.infoTip.boxShadow};
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
}
|
||||
|
||||
.CodeMirror-lint-message {
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
.CodeMirror-lint-message-warning {
|
||||
color: ${(props) => props.theme.status.warning.text};
|
||||
}
|
||||
|
||||
.CodeMirror-lint-message-error {
|
||||
color: ${(props) => props.theme.status.danger.text};
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.CodeMirror-brunoVarInfo .var-info-header {
|
||||
display: flex;
|
||||
@@ -540,6 +561,25 @@ const GlobalStyle = createGlobalStyle`
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.textLink} !important;
|
||||
}
|
||||
|
||||
// Native select styling
|
||||
select {
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
select option {
|
||||
background-color: ${(props) => props.theme.dropdown.bg};
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
}
|
||||
|
||||
select option:hover,
|
||||
select option:focus {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg} !important;
|
||||
color: ${(props) => props.theme.dropdown.color} !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default GlobalStyle;
|
||||
|
||||
@@ -4,10 +4,11 @@ import filter from 'lodash/filter';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { findCollectionByUid, flattenItems, isItemARequest, hasRequestChanges } from 'utils/collections';
|
||||
import { findCollectionByUid, flattenItems, isItemARequest, hasRequestChanges, findEnvironmentInCollection } from 'utils/collections';
|
||||
import { pluralizeWord } from 'utils/common';
|
||||
import { completeQuitFlow } from 'providers/ReduxStore/slices/app';
|
||||
import { saveMultipleRequests, saveMultipleCollections, saveMultipleFolders } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveMultipleRequests, saveMultipleCollections, saveMultipleFolders, saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import Modal from 'components/Modal';
|
||||
import Button from 'ui/Button';
|
||||
@@ -16,12 +17,15 @@ const SaveRequestsModal = ({ onClose }) => {
|
||||
const MAX_UNSAVED_ITEMS_TO_SHOW = 5;
|
||||
const collections = useSelector((state) => state.collections.collections);
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
|
||||
const globalEnvironmentDraft = useSelector((state) => state.globalEnvironments.globalEnvironmentDraft);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const allDrafts = useMemo(() => {
|
||||
const requestDrafts = [];
|
||||
const collectionDrafts = [];
|
||||
const folderDrafts = [];
|
||||
const environmentDrafts = [];
|
||||
const tabsByCollection = groupBy(tabs, (t) => t.collectionUid);
|
||||
|
||||
Object.keys(tabsByCollection).forEach((collectionUid) => {
|
||||
@@ -36,6 +40,21 @@ const SaveRequestsModal = ({ onClose }) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Check for collection environment draft
|
||||
if (collection.environmentsDraft) {
|
||||
const { environmentUid, variables } = collection.environmentsDraft;
|
||||
const environment = findEnvironmentInCollection(collection, environmentUid);
|
||||
if (environment && variables) {
|
||||
environmentDrafts.push({
|
||||
type: 'collection-environment',
|
||||
name: environment.name,
|
||||
environmentUid,
|
||||
variables,
|
||||
collectionUid: collectionUid
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for request and folder drafts
|
||||
const items = flattenItems(collection.items);
|
||||
|
||||
@@ -62,8 +81,22 @@ const SaveRequestsModal = ({ onClose }) => {
|
||||
}
|
||||
});
|
||||
|
||||
return [...collectionDrafts, ...folderDrafts, ...requestDrafts];
|
||||
}, [collections, tabs]);
|
||||
// Check for global environment draft
|
||||
if (globalEnvironmentDraft) {
|
||||
const { environmentUid, variables } = globalEnvironmentDraft;
|
||||
const environment = globalEnvironments?.find((env) => env.uid === environmentUid);
|
||||
if (environment && variables) {
|
||||
environmentDrafts.push({
|
||||
type: 'global-environment',
|
||||
name: environment.name,
|
||||
environmentUid,
|
||||
variables
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [...collectionDrafts, ...folderDrafts, ...environmentDrafts, ...requestDrafts];
|
||||
}, [collections, tabs, globalEnvironments, globalEnvironmentDraft]);
|
||||
|
||||
const totalDraftsCount = allDrafts.length;
|
||||
|
||||
@@ -84,6 +117,8 @@ const SaveRequestsModal = ({ onClose }) => {
|
||||
const collectionDrafts = allDrafts.filter((d) => d.type === 'collection');
|
||||
const folderDrafts = allDrafts.filter((d) => d.type === 'folder');
|
||||
const requestDrafts = allDrafts.filter((d) => d.type === 'request');
|
||||
const collectionEnvironmentDrafts = allDrafts.filter((d) => d.type === 'collection-environment');
|
||||
const globalEnvironmentDrafts = allDrafts.filter((d) => d.type === 'global-environment');
|
||||
|
||||
// Save all collection drafts
|
||||
if (collectionDrafts.length > 0) {
|
||||
@@ -100,6 +135,16 @@ const SaveRequestsModal = ({ onClose }) => {
|
||||
await dispatch(saveMultipleRequests(requestDrafts));
|
||||
}
|
||||
|
||||
// Save all collection environment drafts
|
||||
for (const draft of collectionEnvironmentDrafts) {
|
||||
await dispatch(saveEnvironment(draft.variables, draft.environmentUid, draft.collectionUid));
|
||||
}
|
||||
|
||||
// Save all global environment drafts
|
||||
for (const draft of globalEnvironmentDrafts) {
|
||||
await dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid: draft.environmentUid }));
|
||||
}
|
||||
|
||||
dispatch(completeQuitFlow());
|
||||
onClose();
|
||||
} catch (error) {
|
||||
@@ -134,12 +179,23 @@ const SaveRequestsModal = ({ onClose }) => {
|
||||
|
||||
<ul className="mt-4">
|
||||
{allDrafts.slice(0, MAX_UNSAVED_ITEMS_TO_SHOW).map((item, index) => {
|
||||
const prefix
|
||||
= item.type === 'collection'
|
||||
? 'Collection: '
|
||||
: item.type === 'folder'
|
||||
? 'Folder: '
|
||||
: 'Request: ';
|
||||
let prefix;
|
||||
switch (item.type) {
|
||||
case 'collection':
|
||||
prefix = 'Collection: ';
|
||||
break;
|
||||
case 'folder':
|
||||
prefix = 'Folder: ';
|
||||
break;
|
||||
case 'collection-environment':
|
||||
prefix = 'Collection Environment: ';
|
||||
break;
|
||||
case 'global-environment':
|
||||
prefix = 'Global Environment: ';
|
||||
break;
|
||||
default:
|
||||
prefix = 'Request: ';
|
||||
}
|
||||
return (
|
||||
<li key={`${item.type}-${item.collectionUid || item.uid}-${index}`} className="mt-1 text-xs">
|
||||
{prefix}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
collectionUnlinkEnvFileEvent,
|
||||
collectionUnlinkFileEvent,
|
||||
processEnvUpdateEvent,
|
||||
workspaceEnvUpdateEvent,
|
||||
requestCancelled,
|
||||
runFolderEvent,
|
||||
runRequestEvent,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { collectionAddEnvFileEvent, openCollectionEvent, hydrateCollectionWithUiStateSnapshot, mergeAndPersistEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { workspaceOpenedEvent, workspaceConfigUpdatedEvent } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { workspaceDotEnvUpdateEvent } from 'providers/ReduxStore/slices/workspaces';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { isElectron } from 'utils/common/platform';
|
||||
@@ -214,6 +216,11 @@ const useIpcEvents = () => {
|
||||
dispatch(processEnvUpdateEvent(val));
|
||||
});
|
||||
|
||||
const removeWorkspaceDotEnvUpdatesListener = ipcRenderer.on('main:workspace-dotenv-update', (val) => {
|
||||
dispatch(workspaceDotEnvUpdateEvent(val));
|
||||
dispatch(workspaceEnvUpdateEvent({ processEnvVariables: val.processEnvVariables }));
|
||||
});
|
||||
|
||||
const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => {
|
||||
console[val.type](...val.args);
|
||||
dispatch(addLog({
|
||||
@@ -293,6 +300,7 @@ const useIpcEvents = () => {
|
||||
removeRunFolderEventListener();
|
||||
removeRunRequestEventListener();
|
||||
removeProcessEnvUpdatesListener();
|
||||
removeWorkspaceDotEnvUpdatesListener();
|
||||
removeConsoleLogListener();
|
||||
removeConfigUpdatesListener();
|
||||
removeShowPreferencesListener();
|
||||
|
||||
@@ -32,7 +32,10 @@ const KeyMapping = {
|
||||
name: 'Move Tab Right'
|
||||
},
|
||||
closeAllTabs: { mac: 'command+shift+w', windows: 'ctrl+shift+w', name: 'Close All Tabs' },
|
||||
collapseSidebar: { mac: 'command+\\', windows: 'ctrl+\\', name: 'Collapse Sidebar' }
|
||||
collapseSidebar: { mac: 'command+\\', windows: 'ctrl+\\', name: 'Collapse Sidebar' },
|
||||
zoomIn: { mac: 'command+=', windows: 'ctrl+=', name: 'Zoom In' },
|
||||
zoomOut: { mac: 'command+-', windows: 'ctrl+-', name: 'Zoom Out' },
|
||||
resetZoom: { mac: 'command+0', windows: 'ctrl+0', name: 'Reset Zoom' }
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { saveRequest, saveCollectionSettings, saveFolderRoot } from '../../slices/collections/actions';
|
||||
import { saveRequest, saveCollectionSettings, saveFolderRoot, saveEnvironment } from '../../slices/collections/actions';
|
||||
import { saveGlobalEnvironment } from '../../slices/global-environments';
|
||||
import { flattenItems, isItemARequest, isItemAFolder } from 'utils/collections';
|
||||
|
||||
const actionsToIntercept = [
|
||||
@@ -46,6 +47,11 @@ const actionsToIntercept = [
|
||||
'collections/updateRequestDocs',
|
||||
'collections/runRequestEvent',
|
||||
'collections/updateCollectionPresets',
|
||||
'collections/setRequestVars',
|
||||
'collections/setRequestAssertions',
|
||||
'collections/updateItemSettings',
|
||||
'collections/addRequestTag',
|
||||
'collections/deleteRequestTag',
|
||||
|
||||
// Folder-level actions
|
||||
'collections/addFolderHeader',
|
||||
@@ -80,7 +86,11 @@ const actionsToIntercept = [
|
||||
'collections/updateCollectionDocs',
|
||||
'collections/updateCollectionClientCertificates',
|
||||
'collections/updateCollectionProtobuf',
|
||||
'collections/updateCollectionProxy'
|
||||
'collections/updateCollectionProxy',
|
||||
|
||||
// Environment draft actions
|
||||
'collections/setEnvironmentsDraft',
|
||||
'globalEnvironments/setGlobalEnvironmentDraft'
|
||||
];
|
||||
|
||||
// Simple object to track pending save timers
|
||||
@@ -100,7 +110,8 @@ const scheduleAutoSave = (key, save, interval) => {
|
||||
|
||||
// Helper to find and schedule saves for all existing drafts
|
||||
const saveExistingDrafts = (dispatch, getState, interval) => {
|
||||
const collections = getState().collections.collections;
|
||||
const state = getState();
|
||||
const collections = state.collections.collections;
|
||||
|
||||
collections.forEach((collection) => {
|
||||
// Check collection-level draft
|
||||
@@ -109,6 +120,15 @@ const saveExistingDrafts = (dispatch, getState, interval) => {
|
||||
scheduleAutoSave(key, () => dispatch(saveCollectionSettings(collection.uid, null, true)), interval);
|
||||
}
|
||||
|
||||
// Check collection environment drafts
|
||||
if (collection.environmentsDraft) {
|
||||
const { environmentUid, variables } = collection.environmentsDraft;
|
||||
if (environmentUid && variables) {
|
||||
const key = `environment-${collection.uid}-${environmentUid}`;
|
||||
scheduleAutoSave(key, () => dispatch(saveEnvironment(variables, environmentUid, collection.uid)), interval);
|
||||
}
|
||||
}
|
||||
|
||||
// Check all items (requests and folders) for drafts
|
||||
const allItems = flattenItems(collection.items);
|
||||
allItems.forEach((item) => {
|
||||
@@ -123,6 +143,77 @@ const saveExistingDrafts = (dispatch, getState, interval) => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Check global environment drafts
|
||||
const globalEnvironmentDraft = state.globalEnvironments?.globalEnvironmentDraft;
|
||||
if (globalEnvironmentDraft) {
|
||||
const { environmentUid, variables } = globalEnvironmentDraft;
|
||||
if (environmentUid && variables) {
|
||||
const key = `global-environment-${environmentUid}`;
|
||||
scheduleAutoSave(key, () => dispatch(saveGlobalEnvironment({ variables, environmentUid })), interval);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to determine entity type and create save handler
|
||||
const determineSaveHandler = (actionType, payload, dispatch, getState) => {
|
||||
const { itemUid, folderUid, collectionUid, environmentUid } = payload;
|
||||
|
||||
// Handle environment drafts
|
||||
if (actionType === 'collections/setEnvironmentsDraft') {
|
||||
if (!environmentUid || !collectionUid) return null;
|
||||
return {
|
||||
key: `environment-${collectionUid}-${environmentUid}`,
|
||||
save: () => {
|
||||
const state = getState();
|
||||
const collection = state.collections.collections.find((c) => c.uid === collectionUid);
|
||||
const draft = collection?.environmentsDraft;
|
||||
if (draft?.environmentUid === environmentUid && draft?.variables) {
|
||||
dispatch(saveEnvironment(draft.variables, environmentUid, collectionUid));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (actionType === 'globalEnvironments/setGlobalEnvironmentDraft') {
|
||||
if (!environmentUid) return null;
|
||||
return {
|
||||
key: `global-environment-${environmentUid}`,
|
||||
save: () => {
|
||||
const state = getState();
|
||||
const draft = state.globalEnvironments?.globalEnvironmentDraft;
|
||||
if (draft?.environmentUid === environmentUid && draft?.variables) {
|
||||
dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid }));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Handle folder actions
|
||||
if (folderUid) {
|
||||
return {
|
||||
key: `folder-${folderUid}`,
|
||||
save: () => dispatch(saveFolderRoot(collectionUid, folderUid, true))
|
||||
};
|
||||
}
|
||||
|
||||
// Handle request actions
|
||||
if (itemUid) {
|
||||
return {
|
||||
key: `request-${itemUid}`,
|
||||
save: () => dispatch(saveRequest(itemUid, collectionUid, true))
|
||||
};
|
||||
}
|
||||
|
||||
// Handle collection-level changes
|
||||
if (collectionUid) {
|
||||
return {
|
||||
key: `collection-${collectionUid}`,
|
||||
save: () => dispatch(saveCollectionSettings(collectionUid, null, true))
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const autosaveMiddleware = ({ dispatch, getState }) => (next) => (action) => {
|
||||
@@ -150,28 +241,9 @@ export const autosaveMiddleware = ({ dispatch, getState }) => (next) => (action)
|
||||
// Only handle actions that create dirty state
|
||||
if (!actionsToIntercept.includes(action.type)) return result;
|
||||
|
||||
const { itemUid, folderUid, collectionUid } = action.payload;
|
||||
const interval = autoSave.interval;
|
||||
|
||||
// Determine what to save based on what IDs are present
|
||||
let key, save;
|
||||
|
||||
if (itemUid) {
|
||||
// Request change
|
||||
key = `request-${itemUid}`;
|
||||
save = () => dispatch(saveRequest(itemUid, collectionUid, true));
|
||||
} else if (folderUid) {
|
||||
// Folder change
|
||||
key = `folder-${folderUid}`;
|
||||
save = () => dispatch(saveFolderRoot(collectionUid, folderUid, true));
|
||||
} else if (collectionUid) {
|
||||
// Collection change
|
||||
key = `collection-${collectionUid}`;
|
||||
save = () => dispatch(saveCollectionSettings(collectionUid, null, true));
|
||||
}
|
||||
|
||||
if (key && save) {
|
||||
scheduleAutoSave(key, save, interval);
|
||||
const handler = determineSaveHandler(action.type, action.payload, dispatch, getState);
|
||||
if (handler) {
|
||||
scheduleAutoSave(handler.key, handler.save, autoSave.interval);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
sortCollections as _sortCollections,
|
||||
updateCollectionMountStatus,
|
||||
moveCollection,
|
||||
workspaceEnvUpdateEvent,
|
||||
requestCancelled,
|
||||
resetRunResults,
|
||||
responseReceived,
|
||||
@@ -816,7 +817,6 @@ export const renameItem
|
||||
return ipcRenderer
|
||||
.invoke('renderer:rename-item-filename', { oldPath: item.pathname, newPath, newName, newFilename, collectionPathname: collection.pathname })
|
||||
.catch((err) => {
|
||||
toast.error('Failed to rename the file');
|
||||
console.error(err);
|
||||
throw new Error('Failed to rename the file');
|
||||
});
|
||||
@@ -1129,7 +1129,7 @@ export const handleCollectionItemDrop
|
||||
|
||||
// Update sequences in the target directory (if dropping adjacent)
|
||||
if (dropType === 'adjacent') {
|
||||
const targetItemSequence = targetItemDirectoryItems.findIndex((i) => i.uid === targetItemUid)?.seq;
|
||||
const targetItemSequence = targetItemDirectoryItems.find((i) => i.uid === targetItemUid)?.seq;
|
||||
|
||||
const draggedItemWithNewPathAndSequence = {
|
||||
...draggedItem,
|
||||
@@ -1176,6 +1176,14 @@ export const handleCollectionItemDrop
|
||||
});
|
||||
if (!newPathname) return;
|
||||
if (targetItemPathname?.startsWith(draggedItemPathname)) return;
|
||||
|
||||
// Discard operation if dragging a root item to the collection name (same location)
|
||||
const isTargetTheCollection = targetItemPathname === collection.pathname;
|
||||
const isDraggedItemAtRoot = draggedItemDirectory === sourceCollection;
|
||||
if (isTargetTheCollection && isDraggedItemAtRoot && !isCrossCollectionMove) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPathname !== draggedItemPathname) {
|
||||
await handleMoveToNewLocation({
|
||||
targetItem,
|
||||
@@ -1755,8 +1763,8 @@ export const saveEnvironment = (variables, environmentUid, collectionUid) => (di
|
||||
Modal Save writes what the user sees:
|
||||
- Non-ephemeral vars are saved as-is (without metadata)
|
||||
- Ephemeral vars:
|
||||
- if persistedValue exists, save that (explicit persisted case)
|
||||
- otherwise save the current UI value (treat as user-authored)
|
||||
- if persistedValue exists, save that (restore original value)
|
||||
- otherwise filter out (don't save script-created ephemeral vars)
|
||||
*/
|
||||
const persisted = buildPersistedEnvVariables(variables, { mode: 'save' });
|
||||
environment.variables = persisted;
|
||||
@@ -2247,6 +2255,7 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
|
||||
return new Promise((resolve, reject) => {
|
||||
const state = getState();
|
||||
const activeWorkspace = state.workspaces.workspaces.find((w) => w.uid === state.workspaces.activeWorkspaceUid);
|
||||
const workspaceProcessEnvVariables = activeWorkspace?.processEnvVariables || {};
|
||||
|
||||
// Check if collection already exists in Redux state
|
||||
const existingCollection = state.collections.collections.find(
|
||||
@@ -2288,6 +2297,8 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(workspaceEnvUpdateEvent({ processEnvVariables: workspaceProcessEnvVariables }));
|
||||
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
@@ -2300,6 +2311,7 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
|
||||
pathname: pathname,
|
||||
items: [],
|
||||
runtimeVariables: {},
|
||||
workspaceProcessEnvVariables,
|
||||
brunoConfig: brunoConfig
|
||||
};
|
||||
|
||||
@@ -2318,6 +2330,9 @@ export const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, ge
|
||||
);
|
||||
|
||||
if (currentWorkspace) {
|
||||
// Set collection-workspace mapping for workspace env vars
|
||||
ipcRenderer.invoke('renderer:set-collection-workspace', uid, currentWorkspace.pathname);
|
||||
|
||||
const alreadyInWorkspace = currentWorkspace.collections?.some(
|
||||
(c) => normalizePath(c.path) === normalizePath(pathname)
|
||||
);
|
||||
|
||||
@@ -334,6 +334,9 @@ export const collectionsSlice = createSlice({
|
||||
const activeEnvironment = findEnvironmentInCollection(collection, activeEnvironmentUid);
|
||||
|
||||
if (activeEnvironment) {
|
||||
const existingEnvVarNames = new Set(Object.keys(envVariables));
|
||||
|
||||
// Update or add variables that exist in envVariables
|
||||
forOwn(envVariables, (value, key) => {
|
||||
const variable = find(activeEnvironment.variables, (v) => v.name === key);
|
||||
const isPersistent = persistentEnvVariables && persistentEnvVariables[key] !== undefined;
|
||||
@@ -369,6 +372,26 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle variables that were deleted via bru.deleteEnvVar()
|
||||
activeEnvironment.variables = activeEnvironment.variables.filter((variable) => {
|
||||
// Variable still exists in envVariables after script execution - keep it
|
||||
if (existingEnvVarNames.has(variable.name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Variable was deleted via bru.deleteEnvVar() - handle based on its state
|
||||
// If variable was modified by script (has persistedValue), restore original value
|
||||
if (variable.persistedValue !== undefined) {
|
||||
variable.value = variable.persistedValue;
|
||||
variable.ephemeral = false;
|
||||
delete variable.persistedValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove variable: either ephemeral (created by scripts) or non-ephemeral deleted via API
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
collection.runtimeVariables = runtimeVariables;
|
||||
@@ -382,6 +405,12 @@ export const collectionsSlice = createSlice({
|
||||
collection.processEnvVariables = processEnvVariables;
|
||||
}
|
||||
},
|
||||
workspaceEnvUpdateEvent: (state, action) => {
|
||||
const { processEnvVariables } = action.payload;
|
||||
state.collections.forEach((collection) => {
|
||||
collection.workspaceProcessEnvVariables = processEnvVariables;
|
||||
});
|
||||
},
|
||||
requestCancelled: (state, action) => {
|
||||
const { itemUid, collectionUid, seq, timestamp } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -2309,7 +2338,7 @@ export const collectionsSlice = createSlice({
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
if (!collection) return;
|
||||
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.itemUid) : null;
|
||||
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
|
||||
if (!folder) return;
|
||||
|
||||
if (folder) {
|
||||
@@ -3125,13 +3154,13 @@ export const collectionsSlice = createSlice({
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (data.data) {
|
||||
item.response.data ||= [];
|
||||
item.response.data = [{
|
||||
item.response.data.push({
|
||||
type: 'incoming',
|
||||
seq,
|
||||
message: data.data,
|
||||
messageHexdump: hexdump(data.data),
|
||||
timestamp: timestamp || Date.now()
|
||||
}].concat(item.response.data);
|
||||
});
|
||||
}
|
||||
if (item.response.dataBuffer && item.response.dataBuffer.length && data.dataBuffer) {
|
||||
item.response.dataBuffer = Buffer.concat([Buffer.from(item.response.dataBuffer), Buffer.from(data.dataBuffer)]);
|
||||
@@ -3401,6 +3430,7 @@ export const {
|
||||
cloneItem,
|
||||
scriptEnvironmentUpdateEvent,
|
||||
processEnvUpdateEvent,
|
||||
workspaceEnvUpdateEvent,
|
||||
requestCancelled,
|
||||
responseReceived,
|
||||
runGrpcRequestEvent,
|
||||
|
||||
@@ -169,8 +169,8 @@ export const removeCollectionFromWorkspaceAction = (workspaceUid, collectionPath
|
||||
};
|
||||
|
||||
const loadWorkspaceCollectionsForSwitch = async (dispatch, workspace) => {
|
||||
const openCollectionsFunction = (collectionPaths, workspaceId) => {
|
||||
return dispatch(openMultipleCollections(collectionPaths, { workspaceId }));
|
||||
const openCollectionsFunction = (collectionPaths, workspacePath) => {
|
||||
return dispatch(openMultipleCollections(collectionPaths, { workspacePath }));
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -418,7 +418,7 @@ export const workspaceConfigUpdatedEvent = (workspacePath, workspaceUid, workspa
|
||||
|
||||
if (uniqueNewCollectionPaths.length > 0) {
|
||||
try {
|
||||
await dispatch(openMultipleCollections(uniqueNewCollectionPaths, { workspaceId: workspace.pathname }));
|
||||
await dispatch(openMultipleCollections(uniqueNewCollectionPaths, { workspacePath: workspace.pathname }));
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,14 @@ export const workspacesSlice = createSlice({
|
||||
if (workspace) {
|
||||
workspace.loadingState = loadingState;
|
||||
}
|
||||
},
|
||||
|
||||
workspaceDotEnvUpdateEvent: (state, action) => {
|
||||
const { workspaceUid, processEnvVariables } = action.payload;
|
||||
const workspace = state.workspaces.find((w) => w.uid === workspaceUid);
|
||||
if (workspace) {
|
||||
workspace.processEnvVariables = processEnvVariables;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -87,7 +95,8 @@ export const {
|
||||
updateWorkspace,
|
||||
addCollectionToWorkspace,
|
||||
removeCollectionFromWorkspace,
|
||||
updateWorkspaceLoadingState
|
||||
updateWorkspaceLoadingState,
|
||||
workspaceDotEnvUpdateEvent
|
||||
} = workspacesSlice.actions;
|
||||
|
||||
export default workspacesSlice.reducer;
|
||||
|
||||
@@ -295,7 +295,7 @@ const darkPastelTheme = {
|
||||
},
|
||||
body: {
|
||||
color: colors.TEXT,
|
||||
bg: colors.GRAY_1
|
||||
bg: colors.GRAY_2
|
||||
},
|
||||
input: {
|
||||
bg: 'transparent',
|
||||
|
||||
@@ -241,7 +241,7 @@ const darkTheme = {
|
||||
color: palette.text.BASE,
|
||||
iconColor: palette.text.SUBTEXT2,
|
||||
bg: palette.background.MANTLE,
|
||||
hoverBg: palette.background.MANTLE,
|
||||
hoverBg: palette.background.SURFACE0,
|
||||
shadow: 'none',
|
||||
border: palette.border.BORDER1,
|
||||
separator: palette.border.BORDER1,
|
||||
|
||||
@@ -10,10 +10,10 @@ const CALCULATION_DELAY_EXTENDED = 150;
|
||||
const GAP_BETWEEN_LEFT_AND_RIGHT_CONTENT = 80;
|
||||
const EXPANDABLE_HYSTERESIS = 20; // Buffer to prevent flickering at boundary
|
||||
|
||||
// Compare two tab arrays by their keys
|
||||
const areTabArraysEqual = (a, b) => {
|
||||
if (a.length !== b.length) return false;
|
||||
return a.every((tab, index) => tab.key === b[index].key);
|
||||
// Compare two key arrays for equality
|
||||
const areKeysEqual = (prevKeys, newKeys) => {
|
||||
if (prevKeys.length !== newKeys.length) return false;
|
||||
return prevKeys.every((key, i) => key === newKeys[i]);
|
||||
};
|
||||
|
||||
const ResponsiveTabs = ({
|
||||
@@ -26,8 +26,8 @@ const ResponsiveTabs = ({
|
||||
rightContentExpandedWidth, // Optional: width of the expandable element when expanded
|
||||
expandableElementIndex = -1 // Optional: index of the expandable child element (-1 means last child)
|
||||
}) => {
|
||||
const [visibleTabs, setVisibleTabs] = useState([]);
|
||||
const [overflowTabs, setOverflowTabs] = useState([]);
|
||||
const [visibleTabKeys, setVisibleTabKeys] = useState([]);
|
||||
const [overflowTabKeys, setOverflowTabKeys] = useState([]);
|
||||
const [rightSideExpandable, setRightSideExpandable] = useState(false);
|
||||
|
||||
const tabsContainerRef = useRef(null);
|
||||
@@ -79,9 +79,16 @@ const ResponsiveTabs = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Only update state if arrays actually changed (prevents infinite loops)
|
||||
setVisibleTabs((prev) => (areTabArraysEqual(prev, visible) ? prev : visible));
|
||||
setOverflowTabs((prev) => (areTabArraysEqual(prev, overflow) ? prev : overflow));
|
||||
// Extract keys and update state only if changed (prevents infinite loops)
|
||||
const visibleKeys = visible.map((t) => t.key);
|
||||
const overflowKeys = overflow.map((t) => t.key);
|
||||
|
||||
setVisibleTabKeys((prev) => {
|
||||
return areKeysEqual(prev, visibleKeys) ? prev : visibleKeys;
|
||||
});
|
||||
setOverflowTabKeys((prev) => {
|
||||
return areKeysEqual(prev, overflowKeys) ? prev : overflowKeys;
|
||||
});
|
||||
|
||||
// Only calculate expandibility if rightContentExpandedWidth is provided
|
||||
if (rightContentExpandedWidth && rightContentRef?.current) {
|
||||
@@ -206,6 +213,10 @@ const ResponsiveTabs = ({
|
||||
expandable: rightSideExpandable
|
||||
});
|
||||
|
||||
// Map stored keys to fresh tab objects from props (ensures indicators stay up-to-date)
|
||||
const visibleTabs = visibleTabKeys.map((key) => tabs.find((t) => t.key === key)).filter(Boolean);
|
||||
const overflowTabs = overflowTabKeys.map((key) => tabs.find((t) => t.key === key)).filter(Boolean);
|
||||
|
||||
// Convert overflow tabs to MenuDropdown items format
|
||||
const overflowMenuItems = useMemo(() => {
|
||||
return overflowTabs.map((tab) => ({
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import get from 'lodash/get';
|
||||
import { find } from 'lodash';
|
||||
import { interpolate } from '@usebruno/common';
|
||||
import { getAllVariables } from 'utils/collections/index';
|
||||
|
||||
export const getAuthHeaders = (collectionRootAuth, requestAuth) => {
|
||||
export const getAuthHeaders = (collectionRootAuth, requestAuth, collection = null, item = null) => {
|
||||
// Discovered edge case where code generation fails when you create a collection which has not been saved yet:
|
||||
// Collection auth therefore null, and request inherits from collection, therefore it is also null
|
||||
// TypeError: Cannot read properties of undefined (reading 'mode')
|
||||
@@ -48,6 +51,72 @@ export const getAuthHeaders = (collectionRootAuth, requestAuth) => {
|
||||
];
|
||||
}
|
||||
return [];
|
||||
case 'oauth2': {
|
||||
const oauth2Config = get(auth, 'oauth2', {});
|
||||
const tokenPlacement = get(oauth2Config, 'tokenPlacement', 'header');
|
||||
const tokenHeaderPrefix = get(oauth2Config, 'tokenHeaderPrefix', 'Bearer');
|
||||
|
||||
// Only add header if token placement is 'header'
|
||||
if (tokenPlacement === 'header') {
|
||||
// Try to get access token from persisted credentials
|
||||
let accessToken = '<access_token>';
|
||||
|
||||
if (collection && item) {
|
||||
try {
|
||||
const grantType = get(oauth2Config, 'grantType', '');
|
||||
// For implicit grant type, use authorizationUrl; for others, use accessTokenUrl
|
||||
const urlToLookup = grantType === 'implicit'
|
||||
? get(oauth2Config, 'authorizationUrl', '')
|
||||
: get(oauth2Config, 'accessTokenUrl', '');
|
||||
const credentialsId = get(oauth2Config, 'credentialsId', 'credentials');
|
||||
const collectionUid = get(collection, 'uid');
|
||||
|
||||
if (urlToLookup && collectionUid) {
|
||||
// Interpolate the URL with variables
|
||||
const variables = getAllVariables(collection, item);
|
||||
const interpolatedUrl = interpolate(urlToLookup, variables);
|
||||
|
||||
// Look up stored credentials
|
||||
const credentialsData = find(
|
||||
collection?.oauth2Credentials || [],
|
||||
(creds) =>
|
||||
creds?.url === interpolatedUrl
|
||||
&& creds?.collectionUid === collectionUid
|
||||
&& creds?.credentialsId === credentialsId
|
||||
);
|
||||
|
||||
if (credentialsData?.credentials?.access_token) {
|
||||
accessToken = credentialsData.credentials.access_token;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error retrieving OAuth2 access token:', error);
|
||||
// Fall back to placeholder if lookup fails
|
||||
}
|
||||
}
|
||||
|
||||
// Build the authorization header value
|
||||
// If tokenHeaderPrefix is empty, just use the token
|
||||
// Otherwise, use the format: "prefix token"
|
||||
// Always trim the final result for consistent formatting
|
||||
const headerValue = (
|
||||
tokenHeaderPrefix
|
||||
? `${tokenHeaderPrefix} ${accessToken}`
|
||||
: accessToken
|
||||
).trim();
|
||||
|
||||
return [
|
||||
{
|
||||
enabled: true,
|
||||
name: 'Authorization',
|
||||
value: headerValue
|
||||
}
|
||||
];
|
||||
}
|
||||
// If tokenPlacement is 'url', this function does not add any auth headers;
|
||||
// token placement in the URL/query params must be handled elsewhere.
|
||||
return [];
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ const STATIC_API_HINTS = {
|
||||
'bru.getTestResults()',
|
||||
'bru.sleep(ms)',
|
||||
'bru.getCollectionName()',
|
||||
'bru.isSafeMode()',
|
||||
'bru.getGlobalEnvVar(key)',
|
||||
'bru.setGlobalEnvVar(key, value)',
|
||||
'bru.runner',
|
||||
@@ -294,9 +295,14 @@ const calculateWordReplacementPositions = (cursor, start, end, word) => {
|
||||
* @returns {string} The determined context
|
||||
*/
|
||||
const determineWordContext = (word) => {
|
||||
if (word.startsWith('req') || word.startsWith('res') || word.startsWith('bru')) {
|
||||
const isApiHint = Object.keys(STATIC_API_HINTS).some(
|
||||
(apiRoot) => apiRoot.toLowerCase().startsWith(word.toLowerCase()) || word.toLowerCase().startsWith(apiRoot.toLowerCase())
|
||||
);
|
||||
|
||||
if (isApiHint) {
|
||||
return 'api';
|
||||
}
|
||||
|
||||
return 'anyword';
|
||||
};
|
||||
|
||||
@@ -513,6 +519,34 @@ const createStandardHintList = (filteredHints, from, to) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Show root-level API hints when the editor is empty
|
||||
* @param {Object} cm - CodeMirror instance
|
||||
* @param {string[]} showHintsFor - Array of hint types to show (e.g., ['req', 'res', 'bru'])
|
||||
* @returns {boolean} True if hints were shown, false otherwise
|
||||
*/
|
||||
export const showRootHints = (cm, showHintsFor = []) => {
|
||||
const wordInfo = getCurrentWordWithContext(cm);
|
||||
// If user is currently typing a word, let handleKeyupForAutocomplete
|
||||
// handle it instead of showing root hints.
|
||||
if (wordInfo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hints = Object.keys(STATIC_API_HINTS).filter((rootHint) => showHintsFor.includes(rootHint));
|
||||
|
||||
if (hints.length === 0) return false;
|
||||
|
||||
const cursor = cm.getCursor();
|
||||
const hintList = createStandardHintList(hints, cursor, cursor);
|
||||
|
||||
cm.showHint({
|
||||
hint: () => hintList,
|
||||
completeSingle: false
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Bruno AutoComplete Helper - Main function with context awareness
|
||||
* @param {Object} cm - CodeMirror instance
|
||||
@@ -624,7 +658,8 @@ const handleKeyupForAutocomplete = (cm, event, options) => {
|
||||
const hints = getAutoCompleteHints(cm, allVariables, anywordAutocompleteHints, options);
|
||||
|
||||
if (!hints) {
|
||||
if (cm.state.completionActive) {
|
||||
const wordInfo = getCurrentWordWithContext(cm);
|
||||
if (cm.state.completionActive && wordInfo) {
|
||||
cm.state.completionActive.close();
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -482,7 +482,7 @@ describe('Bruno Autocomplete', () => {
|
||||
mockedCodemirror.state.completionActive = mockCompletion;
|
||||
|
||||
mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 0 });
|
||||
mockedCodemirror.getLine.mockReturnValue(' ');
|
||||
mockedCodemirror.getLine.mockReturnValue('req.bodyy');
|
||||
mockedCodemirror.getRange.mockReturnValue('');
|
||||
|
||||
const mockEvent = { key: 'a' };
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* LICENSE file at https://github.com/graphql/codemirror-graphql/tree/v0.8.3
|
||||
*/
|
||||
|
||||
import { interpolate, mockDataFunctions } from '@usebruno/common';
|
||||
import { interpolate, mockDataFunctions, timeBasedDynamicVars } from '@usebruno/common';
|
||||
import { getVariableScope, isVariableSecret, getAllVariables } from 'utils/collections';
|
||||
import { updateVariableInScope } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import store from 'providers/ReduxStore';
|
||||
@@ -194,11 +194,13 @@ export const renderVarInfo = (token, options) => {
|
||||
} else if (variableName.startsWith('$')) {
|
||||
const fakerKeyword = variableName.substring(1); // Remove the $ prefix
|
||||
const fakerFunction = mockDataFunctions[fakerKeyword];
|
||||
const isTimeBased = timeBasedDynamicVars.has(fakerKeyword);
|
||||
scopeInfo = {
|
||||
type: 'dynamic',
|
||||
value: '',
|
||||
data: null,
|
||||
isValidFakerVariable: !!fakerFunction
|
||||
isValidDynamicVariable: !!fakerFunction,
|
||||
isTimeBased
|
||||
};
|
||||
} else if (variableName.startsWith('process.env.')) {
|
||||
// Check if this is a process.env variable (starts with "process.env.")
|
||||
@@ -300,8 +302,8 @@ export const renderVarInfo = (token, options) => {
|
||||
return into;
|
||||
}
|
||||
|
||||
// Show warning for invalid faker variable (starts with $ but not a valid faker function)
|
||||
if (scopeInfo.type === 'dynamic' && !scopeInfo.isValidFakerVariable) {
|
||||
// Show warning for invalid dynamic variable (starts with $ but not a valid dynamic function)
|
||||
if (scopeInfo.type === 'dynamic' && !scopeInfo.isValidDynamicVariable) {
|
||||
const warningNote = document.createElement('div');
|
||||
warningNote.className = 'var-warning-note';
|
||||
warningNote.textContent = `Unknown dynamic variable "${variableName}". Check the variable name.`;
|
||||
@@ -309,11 +311,13 @@ export const renderVarInfo = (token, options) => {
|
||||
return into;
|
||||
}
|
||||
|
||||
// For valid dynamic variables, just show the read-only note (no value display since it's generated at runtime)
|
||||
if (scopeInfo.type === 'dynamic' && scopeInfo.isValidFakerVariable) {
|
||||
// For valid dynamic variables, show appropriate read-only note based on type
|
||||
if (scopeInfo.type === 'dynamic' && scopeInfo.isValidDynamicVariable) {
|
||||
const readOnlyNote = document.createElement('div');
|
||||
readOnlyNote.className = 'var-readonly-note';
|
||||
readOnlyNote.textContent = 'Generates random value on each request';
|
||||
readOnlyNote.textContent = scopeInfo.isTimeBased
|
||||
? 'Generates current timestamp on each request'
|
||||
: 'Generates random value on each request';
|
||||
into.appendChild(readOnlyNote);
|
||||
return into;
|
||||
}
|
||||
@@ -678,54 +682,89 @@ if (!SERVER_RENDERED) {
|
||||
|
||||
const state = cm.state.brunoVarInfo;
|
||||
const options = state.options;
|
||||
let token = cm.getTokenAt(pos, true);
|
||||
|
||||
if (token) {
|
||||
const line = cm.getLine(pos.line);
|
||||
// Get the full line text where the hover happened
|
||||
const line = cm.getLine(pos.line);
|
||||
if (!line) return;
|
||||
|
||||
// Find the opening {{ before the cursor
|
||||
let start = token.start;
|
||||
while (start > 0 && !line.substring(start - 2, start).includes('{{')) {
|
||||
// Stop if we encounter }} - we've gone past the start of our variable
|
||||
if (line.substring(start - 2, start) === '}}') {
|
||||
break;
|
||||
}
|
||||
start--;
|
||||
}
|
||||
if (line.substring(start - 2, start) === '{{') {
|
||||
start = start - 2;
|
||||
// If the line doesn't even contain both braces, no need to run loops
|
||||
if (!line.includes('{{') || !line.includes('}}')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// lastIndexOf searches backward from the cursor indexOf searches forward
|
||||
if (line.lastIndexOf('{{', pos.ch) === -1 || line.indexOf('}}', pos.ch) === -1) {
|
||||
return;
|
||||
}
|
||||
let start = pos.ch;
|
||||
let end = pos.ch;
|
||||
|
||||
// ---------- Find opening '{{' to the LEFT ----------
|
||||
while (start > 0) {
|
||||
const leftTwo = line.substring(start - 2, start);
|
||||
|
||||
// If we find opening braces, stop
|
||||
if (leftTwo === '{{') {
|
||||
start -= 2;
|
||||
break;
|
||||
}
|
||||
|
||||
// Find the closing }} after the cursor
|
||||
let end = token.end;
|
||||
while (end < line.length && !line.substring(end, end + 2).includes('}}')) {
|
||||
// Stop if we encounter {{ - we've gone past the end of our variable
|
||||
if (line.substring(end, end + 2) === '{{') {
|
||||
break;
|
||||
}
|
||||
end++;
|
||||
}
|
||||
if (line.substring(end, end + 2) === '}}') {
|
||||
end = end + 2;
|
||||
// If we cross a closing braces before finding '{{', we're not inside a variable
|
||||
if (leftTwo === '}}') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the full variable string including {{ and }}
|
||||
const fullVariableString = line.substring(start, end);
|
||||
start--;
|
||||
}
|
||||
|
||||
// Only use the expanded string if it looks like a complete variable
|
||||
if (fullVariableString.startsWith('{{') && fullVariableString.endsWith('}}')) {
|
||||
token = {
|
||||
...token,
|
||||
string: fullVariableString,
|
||||
start: start,
|
||||
end: end
|
||||
};
|
||||
// If we reached the start of the line and didn't match '{{', return
|
||||
if (start < 0 || line.substring(start, start + 2) !== '{{') {
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------- Find closing '}}' to the RIGHT ----------
|
||||
while (end < line.length) {
|
||||
const rightTwo = line.substring(end, end + 2);
|
||||
|
||||
// If we find closing braces, stop
|
||||
if (rightTwo === '}}') {
|
||||
end += 2;
|
||||
break;
|
||||
}
|
||||
|
||||
const brunoVarInfo = renderVarInfo(token, options);
|
||||
if (brunoVarInfo) {
|
||||
showPopup(cm, box, brunoVarInfo);
|
||||
// If we hit another '{{' before a '}}', then this isn't a valid enclosing pair
|
||||
if (rightTwo === '{{') {
|
||||
return;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
// If we reached end-of-line without finding '}}', return
|
||||
if (end > line.length || line.substring(end - 2, end) !== '}}') {
|
||||
return;
|
||||
}
|
||||
|
||||
const fullVariableString = line.substring(start, end);
|
||||
|
||||
// Basic validation to ensure it's a non-empty variable
|
||||
if (!fullVariableString.startsWith('{{') || !fullVariableString.endsWith('}}')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent tooltips for empty variables like {{ }}
|
||||
const inner = fullVariableString.slice(2, -2).trim();
|
||||
if (!inner) return;
|
||||
|
||||
// Build a token object compatible with renderVarInfo
|
||||
const token = {
|
||||
string: fullVariableString,
|
||||
start: start,
|
||||
end: end
|
||||
};
|
||||
|
||||
const brunoVarInfo = renderVarInfo(token, options);
|
||||
if (brunoVarInfo) {
|
||||
showPopup(cm, box, brunoVarInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,11 @@ jest.mock('@usebruno/common', () => ({
|
||||
randomFirstName: jest.fn(() => 'John'),
|
||||
randomLastName: jest.fn(() => 'Doe'),
|
||||
randomEmail: jest.fn(() => 'john.doe@example.com'),
|
||||
randomUUID: jest.fn(() => '123e4567-e89b-12d3-a456-426614174000')
|
||||
}
|
||||
randomUUID: jest.fn(() => '123e4567-e89b-12d3-a456-426614174000'),
|
||||
timestamp: jest.fn(() => '1704067200'),
|
||||
isoTimestamp: jest.fn(() => '2024-01-01T00:00:00.000Z')
|
||||
},
|
||||
timeBasedDynamicVars: new Set(['timestamp', 'isoTimestamp'])
|
||||
}));
|
||||
|
||||
jest.mock('providers/ReduxStore', () => ({
|
||||
@@ -467,6 +470,29 @@ describe('renderVarInfo', () => {
|
||||
expect(warningNote).not.toBeNull();
|
||||
expect(warningNote.textContent).toContain('Unknown dynamic variable');
|
||||
});
|
||||
|
||||
it('should show time-based note for $timestamp variable', () => {
|
||||
const { readOnlyNote, scopeBadge } = setupDynamicRender('$timestamp');
|
||||
|
||||
expect(scopeBadge.textContent).toBe('Dynamic');
|
||||
expect(readOnlyNote).not.toBeNull();
|
||||
expect(readOnlyNote.textContent).toBe('Generates current timestamp on each request');
|
||||
});
|
||||
|
||||
it('should show time-based note for $isoTimestamp variable', () => {
|
||||
const { readOnlyNote, scopeBadge } = setupDynamicRender('$isoTimestamp');
|
||||
|
||||
expect(scopeBadge.textContent).toBe('Dynamic');
|
||||
expect(readOnlyNote).not.toBeNull();
|
||||
expect(readOnlyNote.textContent).toBe('Generates current timestamp on each request');
|
||||
});
|
||||
|
||||
it('should show random note for non-time-based dynamic variables', () => {
|
||||
const { readOnlyNote } = setupDynamicRender('$randomEmail');
|
||||
|
||||
expect(readOnlyNote).not.toBeNull();
|
||||
expect(readOnlyNote.textContent).toBe('Generates random value on each request');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuth2 variable rendering', () => {
|
||||
|
||||
@@ -59,7 +59,18 @@ function markUrls(editor, linkify, linkClass, linkHint) {
|
||||
const matches = linkify.match(lineContent);
|
||||
if (!matches) continue;
|
||||
|
||||
const variablePatterns = [];
|
||||
const variablePattern = /\{\{[^}]*\}\}/g;
|
||||
let varMatch;
|
||||
while ((varMatch = variablePattern.exec(lineContent)) !== null) {
|
||||
variablePatterns.push({ start: varMatch.index, end: varMatch.index + varMatch[0].length });
|
||||
}
|
||||
matches.forEach(({ index, lastIndex, url }) => {
|
||||
const isInVariable = variablePatterns.some(
|
||||
({ start, end }) => index < end && lastIndex > start
|
||||
);
|
||||
if (isInVariable) return;
|
||||
|
||||
try {
|
||||
editor.markText(
|
||||
{ line: lineNum, ch: index },
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user