mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 01:18:32 +00:00
Compare commits
345 Commits
oauth2_add
...
v2.15.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
536b7393db | ||
|
|
172479edad | ||
|
|
486b91894c | ||
|
|
ca8ef36f9f | ||
|
|
7ed474c8ba | ||
|
|
b0405b1e1a | ||
|
|
c2d000e805 | ||
|
|
6aaccabc04 | ||
|
|
daf23c9e2d | ||
|
|
f952688032 | ||
|
|
f429fa94e3 | ||
|
|
fb420fcea4 | ||
|
|
cc3d6a961a | ||
|
|
27c37192b2 | ||
|
|
faa2ef5de2 | ||
|
|
c05d56fd21 | ||
|
|
b4d19ab8ca | ||
|
|
0cedf48e68 | ||
|
|
4e7bc1a351 | ||
|
|
9d3c8b2401 | ||
|
|
39dfd8d360 | ||
|
|
460832f3ed | ||
|
|
50442d960d | ||
|
|
2ac41806a2 | ||
|
|
e9111c0529 | ||
|
|
48a09f6f50 | ||
|
|
e613e4cbcd | ||
|
|
4631eda281 | ||
|
|
3f7ab31b2b | ||
|
|
27a7b623c7 | ||
|
|
95bc670d8c | ||
|
|
6d8f428140 | ||
|
|
ed18cb6d90 | ||
|
|
bb83fbfb9d | ||
|
|
ddfdeda4d6 | ||
|
|
adb0b90457 | ||
|
|
8c7888533a | ||
|
|
0a188575a0 | ||
|
|
2be602d16c | ||
|
|
8ec1925b9f | ||
|
|
d28f2f32e9 | ||
|
|
76a1532695 | ||
|
|
efad149afc | ||
|
|
2d2a17c90f | ||
|
|
3d8d93f20d | ||
|
|
94c33e6833 | ||
|
|
2ef451c80b | ||
|
|
044fcce49f | ||
|
|
dffb600dab | ||
|
|
99478b7068 | ||
|
|
252fd386b7 | ||
|
|
b982f6db16 | ||
|
|
3b4e5686b8 | ||
|
|
2ef1a1948b | ||
|
|
f2273821b0 | ||
|
|
8a22f6acb8 | ||
|
|
6049530634 | ||
|
|
5784b04129 | ||
|
|
fec37f43e0 | ||
|
|
b8fef7b796 | ||
|
|
04f8dba1b1 | ||
|
|
cd1500bd01 | ||
|
|
e8a8b5d220 | ||
|
|
bc3dfc59f6 | ||
|
|
2c399ca33c | ||
|
|
ccac4d6112 | ||
|
|
fc5093eab4 | ||
|
|
631b05330d | ||
|
|
be34c86c47 | ||
|
|
67c9f1373e | ||
|
|
6628f95677 | ||
|
|
44ed0b01d8 | ||
|
|
45cfbc5c49 | ||
|
|
14bece8696 | ||
|
|
9e19244665 | ||
|
|
f439f2de9a | ||
|
|
e844d35b03 | ||
|
|
26e140aca0 | ||
|
|
7bd6a9a915 | ||
|
|
e1045372d5 | ||
|
|
914b858024 | ||
|
|
36e9a9c137 | ||
|
|
995899dedb | ||
|
|
408dd6bccf | ||
|
|
ab7ead91d5 | ||
|
|
a186df3ac4 | ||
|
|
3fe5299d8e | ||
|
|
57c08350b6 | ||
|
|
68b2625259 | ||
|
|
1719cee440 | ||
|
|
ab4e9eb3bd | ||
|
|
b15c421270 | ||
|
|
24a36bc355 | ||
|
|
1ac128e35c | ||
|
|
4510cc3414 | ||
|
|
f3cb0d4bae | ||
|
|
7b183887ce | ||
|
|
fa28ab9b50 | ||
|
|
60b437ef9d | ||
|
|
1ef8852a01 | ||
|
|
17cc70f36e | ||
|
|
2deee11718 | ||
|
|
bdc8f391b7 | ||
|
|
1656e951fb | ||
|
|
d8adb59d04 | ||
|
|
de05fb6137 | ||
|
|
3e3884a6af | ||
|
|
23843bb621 | ||
|
|
c85a1ec1a5 | ||
|
|
68cbb7d9df | ||
|
|
396ff2b196 | ||
|
|
6826e98945 | ||
|
|
e47d1ed353 | ||
|
|
08c182a875 | ||
|
|
f3afb4bf84 | ||
|
|
21e8615247 | ||
|
|
6e8751a27a | ||
|
|
c9a96ee94f | ||
|
|
b69db7b44b | ||
|
|
73caaef42b | ||
|
|
e68b2ae3b7 | ||
|
|
cc7f1ea58f | ||
|
|
6e8cd55b76 | ||
|
|
384aabf2af | ||
|
|
a15dcdb133 | ||
|
|
18848cdb26 | ||
|
|
29b90a7e0d | ||
|
|
4fbe371eb0 | ||
|
|
6fd2b8be6d | ||
|
|
be7f92d77f | ||
|
|
c5325c732f | ||
|
|
a538b27f24 | ||
|
|
77bb8f40fe | ||
|
|
8f1f5e3861 | ||
|
|
e9251a1f3f | ||
|
|
3a011b2a18 | ||
|
|
fa1498e2a8 | ||
|
|
77681ca51e | ||
|
|
045141efaf | ||
|
|
c997b91698 | ||
|
|
986d5b0b2a | ||
|
|
a2a521477a | ||
|
|
8e70adcbf9 | ||
|
|
87296776fa | ||
|
|
9df70cd759 | ||
|
|
8f9fb3b3c9 | ||
|
|
6d018f5648 | ||
|
|
789d0b23c0 | ||
|
|
81e1e403e4 | ||
|
|
ad2add4026 | ||
|
|
02554c3ad9 | ||
|
|
62815e3429 | ||
|
|
9859b69559 | ||
|
|
440c688bbb | ||
|
|
416eb754b7 | ||
|
|
b85d6efa60 | ||
|
|
19dea18629 | ||
|
|
636901c23d | ||
|
|
a4b1941817 | ||
|
|
7d8fde9180 | ||
|
|
4197304bf9 | ||
|
|
b75422a010 | ||
|
|
e9f03c46c7 | ||
|
|
73e828621f | ||
|
|
2becf49542 | ||
|
|
4c3a9928bc | ||
|
|
b694a41c96 | ||
|
|
ff9a4d97e3 | ||
|
|
6ab6e5ed57 | ||
|
|
3837a7612c | ||
|
|
6589dc51cd | ||
|
|
509f4da667 | ||
|
|
9d2b070ed9 | ||
|
|
d0c524cd9a | ||
|
|
74f0f67795 | ||
|
|
45664bdb65 | ||
|
|
98cb2df3fe | ||
|
|
d478102b30 | ||
|
|
924bc2e79e | ||
|
|
c2d40fe99f | ||
|
|
944674d208 | ||
|
|
0c30357b01 | ||
|
|
ce40949564 | ||
|
|
c6ce40c245 | ||
|
|
6890bbee70 | ||
|
|
4993c61e29 | ||
|
|
a66e849cfb | ||
|
|
9f47200e7b | ||
|
|
10739c32c4 | ||
|
|
c1853e613b | ||
|
|
c393dfe5d6 | ||
|
|
cf17539a47 | ||
|
|
608a9d1954 | ||
|
|
3a04d43ffe | ||
|
|
5c9a391cc6 | ||
|
|
df4b7c1337 | ||
|
|
db6a639c15 | ||
|
|
85319769a5 | ||
|
|
8d2f087206 | ||
|
|
1cc3a6432a | ||
|
|
28907a203f | ||
|
|
6204e90e9c | ||
|
|
1d0ba135ff | ||
|
|
3c72975314 | ||
|
|
3fa9fea6a4 | ||
|
|
239f1dc9f5 | ||
|
|
28e37d8f6f | ||
|
|
8b28070695 | ||
|
|
4ae55b8f1a | ||
|
|
8bad0262c6 | ||
|
|
c7029d1cda | ||
|
|
bb44d9e193 | ||
|
|
14966f6e6c | ||
|
|
56f0741121 | ||
|
|
b1840d189d | ||
|
|
aacb1e0b8e | ||
|
|
fa0f3b3b7b | ||
|
|
2a00add966 | ||
|
|
41e0615f77 | ||
|
|
191a997b05 | ||
|
|
123fe7d542 | ||
|
|
187f5ca011 | ||
|
|
e1b4043ca5 | ||
|
|
9c9cfdf0b2 | ||
|
|
daf6a6d5d6 | ||
|
|
95a2ca9558 | ||
|
|
f359303927 | ||
|
|
65f52961c5 | ||
|
|
2a3db96c9b | ||
|
|
a1a7c9a136 | ||
|
|
c15d47c0dc | ||
|
|
e4f8945e89 | ||
|
|
e6c136d2bb | ||
|
|
6f8c543ee3 | ||
|
|
40b44de294 | ||
|
|
f24e1e78fe | ||
|
|
87d8c5ccb7 | ||
|
|
17d5629627 | ||
|
|
4321846dbd | ||
|
|
f3d4ac84d8 | ||
|
|
de52ceea48 | ||
|
|
65e69e77b3 | ||
|
|
fb2ca8937e | ||
|
|
e2da072e8b | ||
|
|
90492d6e79 | ||
|
|
5393e3b496 | ||
|
|
9fc885839f | ||
|
|
dbfbde43cf | ||
|
|
1aa4e27ab5 | ||
|
|
2b6da56c3c | ||
|
|
c08827b0c0 | ||
|
|
841d977725 | ||
|
|
56629663dc | ||
|
|
27cbb194bf | ||
|
|
cfec4a9e1b | ||
|
|
a7f6d669af | ||
|
|
e57162b79a | ||
|
|
03abbc585f | ||
|
|
be730a8c4f | ||
|
|
194d904284 | ||
|
|
86b3c65dcd | ||
|
|
c9fe9813db | ||
|
|
70d65d87c5 | ||
|
|
0bce203851 | ||
|
|
5b716cbe60 | ||
|
|
a6b0b6c117 | ||
|
|
3c656270b3 | ||
|
|
1bc7a1f655 | ||
|
|
5a10322608 | ||
|
|
2864ddaa72 | ||
|
|
c2f3d8e7da | ||
|
|
1fd61f0601 | ||
|
|
033c5cc0f7 | ||
|
|
db35e7059c | ||
|
|
cd80332de9 | ||
|
|
1902329226 | ||
|
|
b25569d29a | ||
|
|
de4674dcc4 | ||
|
|
457a2f83e7 | ||
|
|
ae3d5a5515 | ||
|
|
3b74e0da86 | ||
|
|
985b5ed20c | ||
|
|
188a2e63e3 | ||
|
|
01839c8e5f | ||
|
|
648581ded5 | ||
|
|
bf38cc0f51 | ||
|
|
abddc98767 | ||
|
|
3fa05d32cb | ||
|
|
eb0accdf21 | ||
|
|
6f57633572 | ||
|
|
e7c33f7eef | ||
|
|
1620c24557 | ||
|
|
bd9d2eabe1 | ||
|
|
990bbdb813 | ||
|
|
00636a5a31 | ||
|
|
c526eacd6b | ||
|
|
9a2836129f | ||
|
|
b8d67d9232 | ||
|
|
bcf4673a64 | ||
|
|
6c52c07494 | ||
|
|
1de9203dd5 | ||
|
|
de48c93e8d | ||
|
|
ba56e87375 | ||
|
|
cb7f61ee4b | ||
|
|
6bcb850b6e | ||
|
|
dc56c00309 | ||
|
|
1220a5f159 | ||
|
|
3046327fa7 | ||
|
|
c1c617bfeb | ||
|
|
6632407a34 | ||
|
|
447b3046b3 | ||
|
|
2666e7fee0 | ||
|
|
f9ca0e2f5a | ||
|
|
5dd90e1386 | ||
|
|
5e9cec38f0 | ||
|
|
ed1a072ba1 | ||
|
|
5f938d77b4 | ||
|
|
f5b4dbd1a1 | ||
|
|
8c72a6094b | ||
|
|
325d03b92f | ||
|
|
54c41c861e | ||
|
|
22a77b90f9 | ||
|
|
af894b5bbb | ||
|
|
48934ef74a | ||
|
|
9c16ebcda3 | ||
|
|
2ed51bb984 | ||
|
|
aec9ee6265 | ||
|
|
04d1e50f98 | ||
|
|
e74c78ea8b | ||
|
|
e71ee3eff5 | ||
|
|
da2f2519ec | ||
|
|
8bd2216bf0 | ||
|
|
4cfc28cd73 | ||
|
|
7efaa427ca | ||
|
|
cffa37ed50 | ||
|
|
bcf61f507a | ||
|
|
325b573da9 | ||
|
|
b3a0234ec3 | ||
|
|
4b5c7dcca6 | ||
|
|
d2888daa88 | ||
|
|
ec9d63219f | ||
|
|
9173ffbdee | ||
|
|
5f112a318d | ||
|
|
8c6ce2e084 | ||
|
|
b02f6b61ee |
66
.coderabbit.yaml
Normal file
66
.coderabbit.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
|
||||
|
||||
language: 'en-US'
|
||||
early_access: false
|
||||
tone_instructions: 'You are an expert code reviewer in TypeScript, JavaScript, NodeJS, and ElectronJS. You work in an enterprise software developer team, providing concise and clear code review advice. You only elaborate or provide detailed explanations when requested.'
|
||||
|
||||
knowledge_base:
|
||||
opt_out: false
|
||||
code_guidelines:
|
||||
enabled: true
|
||||
filePatterns:
|
||||
- '**/CODING_STANDARDS.md'
|
||||
|
||||
reviews:
|
||||
profile: 'chill'
|
||||
request_changes_workflow: false
|
||||
high_level_summary: true
|
||||
poem: true
|
||||
review_status: true
|
||||
collapse_walkthrough: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
drafts: false
|
||||
base_branches: ['main', 'release/*']
|
||||
path_instructions:
|
||||
- path: 'tests/**/**.*'
|
||||
instructions: |
|
||||
Review the following e2e test code written using the Playwright test library. Ensure that:
|
||||
- Follow best practices for Playwright code and e2e automation
|
||||
- Try to reduce usage of `page.waitForTimeout();` in code unless absolutely necessary and the locator cannot be found using existing `expect()` playwright calls
|
||||
- Avoid using `page.pause()` in code
|
||||
- Use locator variables for locators
|
||||
- Avoid using test.only
|
||||
- Use multiple assertions
|
||||
- Promote the use of `test.step` as much as possible so the generated reports are easier to read
|
||||
- Ensure that the `fixtures` like the collections are nested inside the `fixtures` folder
|
||||
|
||||
|
||||
|
||||
**Fixture Example***: Here's an example of possible fixture and test pair
|
||||
```
|
||||
.
|
||||
├── fixtures
|
||||
│ └── collection
|
||||
│ ├── base.bru
|
||||
│ ├── bruno.json
|
||||
│ ├── collection.bru
|
||||
│ ├── ws-test-request-with-headers.bru
|
||||
│ ├── ws-test-request-with-subproto.bru
|
||||
│ └── ws-test-request.bru
|
||||
├── connection.spec.ts # <- Depends on the collection in ./fixtures/collection
|
||||
├── headers.spec.ts
|
||||
├── persistence.spec.ts
|
||||
├── variable-interpolation
|
||||
│ ├── fixtures
|
||||
│ │ └── collection
|
||||
│ │ ├── environments
|
||||
│ │ ├── bruno.json
|
||||
│ │ └── ws-interpolation-test.bru
|
||||
│ ├── init-user-data
|
||||
│ └── variable-interpolation.spec.ts # <- Depends on the collection in ./variable-interpolation/fixtures/collection
|
||||
└── subproto.spec.ts
|
||||
```
|
||||
|
||||
chat:
|
||||
auto_reply: true
|
||||
26
.github/actions/common/setup-node-deps/action.yml
vendored
Normal file
26
.github/actions/common/setup-node-deps/action.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: 'Setup Node Dependencies'
|
||||
description: 'Install Node.js and npm dependencies'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: v22.17.0
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './package-lock.json'
|
||||
|
||||
- name: Install node dependencies
|
||||
shell: bash
|
||||
run: npm ci --legacy-peer-deps
|
||||
|
||||
- name: Build libraries
|
||||
shell: bash
|
||||
run: |
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
npm run build:bruno-filestore
|
||||
36
.github/actions/ssl/linux/run-basic-ssl-cli-tests/action.yml
vendored
Normal file
36
.github/actions/ssl/linux/run-basic-ssl-cli-tests/action.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: 'Run Basic SSL CLI Tests - Linux'
|
||||
description: 'Run basic SSL CLI tests on Linux'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Run CLI tests
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# navigate to basic SSL test collection directory
|
||||
cd tests/ssl/basic-ssl/collections/badssl
|
||||
|
||||
echo "basic ssl success"
|
||||
# should pass
|
||||
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --insecure --format junit
|
||||
xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1
|
||||
|
||||
echo "with default/system ca certs"
|
||||
# should pass
|
||||
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --format junit
|
||||
xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1
|
||||
|
||||
# navigate to self-signed SSL test collection directory
|
||||
cd ../self-signed-badssl
|
||||
|
||||
echo "self-signed ssl with validation disabled"
|
||||
# should pass
|
||||
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --insecure --format junit
|
||||
xmllint --xpath 'count(//testsuite[@errors="0"])' junit3.xml | grep -q "^1$" || exit 1
|
||||
|
||||
echo "self-signed ssl with default/system ca certs"
|
||||
echo "request will error"
|
||||
# should fail
|
||||
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --format junit 2>/dev/null || true
|
||||
xmllint --xpath 'count(//testsuite[@errors="1"])' junit4.xml | grep -q "^1$" || exit 1
|
||||
33
.github/actions/ssl/linux/run-custom-ca-certs-cli-tests/action.yml
vendored
Normal file
33
.github/actions/ssl/linux/run-custom-ca-certs-cli-tests/action.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: 'Run Custom CA Certs CLI Tests - Linux'
|
||||
description: 'Run custom CA certs CLI tests on Linux'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Run CLI tests
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# navigate to CA certificates test collection directory
|
||||
cd tests/ssl/custom-ca-certs/collection
|
||||
|
||||
echo "custom valid ca cert"
|
||||
# should pass
|
||||
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --cacert ../server/certs/ca-cert.pem --ignore-truststore --format junit
|
||||
xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1
|
||||
|
||||
echo "custom valid ca cert with defaults"
|
||||
# should pass
|
||||
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --cacert ../server/certs/ca-cert.pem --format junit
|
||||
xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1
|
||||
|
||||
echo "custom invalid ca cert"
|
||||
echo "request will error"
|
||||
# should fail
|
||||
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --cacert ../server/certs/ca-key.pem --ignore-truststore --format junit 2>/dev/null || true
|
||||
xmllint --xpath 'count(//testsuite[@errors="1"])' junit3.xml | grep -q "^1$" || exit 1
|
||||
|
||||
echo "custom invalid ca cert with defaults"
|
||||
# should pass
|
||||
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --cacert ../server/certs/ca-key.pem --format junit
|
||||
xmllint --xpath 'count(//testsuite[@errors="0"])' junit4.xml | grep -q "^1$" || exit 1
|
||||
19
.github/actions/ssl/linux/run-ssl-e2e-tests/action.yml
vendored
Normal file
19
.github/actions/ssl/linux/run-ssl-e2e-tests/action.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: 'Run SSL E2E Tests - Linux'
|
||||
description: 'Run SSL E2E tests on Linux'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Run E2E tests
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
xvfb-run npm run test:e2e:ssl
|
||||
|
||||
- name: Upload Playwright Report
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report-linux
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
26
.github/actions/ssl/linux/setup-ca-certs/action.yml
vendored
Normal file
26
.github/actions/ssl/linux/setup-ca-certs/action.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: 'Setup CA Certificates - Linux'
|
||||
description: 'Setup CA certificates and start test server for custom CA certs tests on Linux'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup CA certificates
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
cd tests/ssl/custom-ca-certs/server
|
||||
|
||||
echo "running certificate setup"
|
||||
node scripts/generate-certs.js
|
||||
|
||||
- name: Start test server
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
cd tests/ssl/custom-ca-certs/server
|
||||
|
||||
echo "starting server in background"
|
||||
node index.js &
|
||||
|
||||
echo "server started with PID: $!"
|
||||
15
.github/actions/ssl/linux/setup-feature-specific-deps/action.yml
vendored
Normal file
15
.github/actions/ssl/linux/setup-feature-specific-deps/action.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: 'Setup Custom CA Certs Feature Dependencies - Linux'
|
||||
description: 'Setup feature-specific dependencies for custom CA certs tests on Linux'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Install additional OS dependencies for custom CA certs
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get --no-install-recommends install -y \
|
||||
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
|
||||
xvfb libxml2-utils
|
||||
|
||||
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
36
.github/actions/ssl/macos/run-basic-ssl-cli-tests/action.yml
vendored
Normal file
36
.github/actions/ssl/macos/run-basic-ssl-cli-tests/action.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: 'Run Basic SSL CLI Tests - macOS'
|
||||
description: 'Run basic SSL CLI tests on macOS'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Run CLI tests
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# navigate to basic SSL test collection directory
|
||||
cd tests/ssl/basic-ssl/collections/badssl
|
||||
|
||||
echo "basic ssl success"
|
||||
# should pass
|
||||
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --insecure --format junit
|
||||
xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1
|
||||
|
||||
echo "with default/system ca certs"
|
||||
# should pass
|
||||
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --format junit
|
||||
xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1
|
||||
|
||||
# navigate to self-signed SSL test collection directory
|
||||
cd ../self-signed-badssl
|
||||
|
||||
echo "self-signed ssl with validation disabled"
|
||||
# should pass
|
||||
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --insecure --format junit
|
||||
xmllint --xpath 'count(//testsuite[@errors="0"])' junit3.xml | grep -q "^1$" || exit 1
|
||||
|
||||
echo "self-signed ssl with default/system ca certs"
|
||||
echo "request will error"
|
||||
# should fail
|
||||
node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --format junit 2>/dev/null || true
|
||||
xmllint --xpath 'count(//testsuite[@errors="1"])' junit4.xml | grep -q "^1$" || exit 1
|
||||
33
.github/actions/ssl/macos/run-custom-ca-certs-cli-tests/action.yml
vendored
Normal file
33
.github/actions/ssl/macos/run-custom-ca-certs-cli-tests/action.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: 'Run Custom CA Certs CLI Tests - macOS'
|
||||
description: 'Run custom CA certs CLI tests on macOS'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Run CLI tests
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# navigate to CA certificates test collection directory
|
||||
cd tests/ssl/custom-ca-certs/collection
|
||||
|
||||
echo "custom valid ca cert"
|
||||
# should pass
|
||||
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --cacert ../server/certs/ca-cert.pem --ignore-truststore --format junit
|
||||
xmllint --xpath 'count(//testsuite[@errors="0"])' junit1.xml | grep -q "^1$" || exit 1
|
||||
|
||||
echo "custom valid ca cert with defaults"
|
||||
# should pass
|
||||
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --cacert ../server/certs/ca-cert.pem --format junit
|
||||
xmllint --xpath 'count(//testsuite[@errors="0"])' junit2.xml | grep -q "^1$" || exit 1
|
||||
|
||||
echo "custom invalid ca cert"
|
||||
echo "request will error"
|
||||
# should fail
|
||||
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --cacert ../server/certs/ca-key.pem --ignore-truststore --format junit 2>/dev/null || true
|
||||
xmllint --xpath 'count(//testsuite[@errors="1"])' junit3.xml | grep -q "^1$" || exit 1
|
||||
|
||||
echo "custom invalid ca cert with defaults"
|
||||
# should pass
|
||||
node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --cacert ../server/certs/ca-key.pem --format junit
|
||||
xmllint --xpath 'count(//testsuite[@errors="0"])' junit4.xml | grep -q "^1$" || exit 1
|
||||
17
.github/actions/ssl/macos/run-ssl-e2e-tests/action.yml
vendored
Normal file
17
.github/actions/ssl/macos/run-ssl-e2e-tests/action.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: 'Run SSL E2E Tests - macOS'
|
||||
description: 'Run SSL E2E tests on macOS'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Run E2E tests
|
||||
shell: bash
|
||||
run: |
|
||||
npm run test:e2e:ssl
|
||||
|
||||
- name: Upload Playwright Report
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report-macos
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
26
.github/actions/ssl/macos/setup-ca-certs/action.yml
vendored
Normal file
26
.github/actions/ssl/macos/setup-ca-certs/action.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: 'Setup CA Certificates - macOS'
|
||||
description: 'Setup CA certificates and start test server for custom CA certs tests on macOS'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup CA certificates
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
cd tests/ssl/custom-ca-certs/server
|
||||
|
||||
echo "running certificate setup"
|
||||
node scripts/generate-certs.js
|
||||
|
||||
- name: Start test server
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
cd tests/ssl/custom-ca-certs/server
|
||||
|
||||
echo "starting server in background"
|
||||
node index.js &
|
||||
|
||||
echo "server started with PID: $!"
|
||||
9
.github/actions/ssl/macos/setup-feature-specific-deps/action.yml
vendored
Normal file
9
.github/actions/ssl/macos/setup-feature-specific-deps/action.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
name: 'Setup Custom CA Certs Feature Dependencies - macOS'
|
||||
description: 'Setup feature-specific dependencies for custom CA certs tests on macOS'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Install additional OS dependencies for custom CA certs
|
||||
shell: bash
|
||||
run: |
|
||||
brew install libxml2
|
||||
50
.github/actions/ssl/windows/run-basic-ssl-cli-tests/action.yml
vendored
Normal file
50
.github/actions/ssl/windows/run-basic-ssl-cli-tests/action.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: 'Run Basic SSL CLI Tests - Windows'
|
||||
description: 'Run basic SSL CLI tests on Windows'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Run CLI tests
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# navigate to basic SSL test collection directory
|
||||
Set-Location tests\ssl\basic-ssl\collections\badssl
|
||||
|
||||
Write-Host "basic ssl success"
|
||||
# should pass
|
||||
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit1.xml --insecure --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
|
||||
[xml]$xml1 = Get-Content junit1.xml
|
||||
$testsuites1 = if ($xml1.testsuites) { $xml1.testsuites.testsuite } else { $xml1.testsuite }
|
||||
$errorCount1 = ($testsuites1 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
|
||||
if ($errorCount1 -ne 1) { exit 1 }
|
||||
|
||||
Write-Host "with default/system ca certs"
|
||||
# should pass
|
||||
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit2.xml --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
|
||||
[xml]$xml2 = Get-Content junit2.xml
|
||||
$testsuites2 = if ($xml2.testsuites) { $xml2.testsuites.testsuite } else { $xml2.testsuite }
|
||||
$errorCount2 = ($testsuites2 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
|
||||
if ($errorCount2 -ne 1) { exit 1 }
|
||||
|
||||
# navigate to self-signed SSL test collection directory
|
||||
Set-Location ..\self-signed-badssl
|
||||
|
||||
Write-Host "self-signed ssl with validation disabled"
|
||||
# should pass
|
||||
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit3.xml --insecure --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
|
||||
[xml]$xml3 = Get-Content junit3.xml
|
||||
$testsuites3 = if ($xml3.testsuites) { $xml3.testsuites.testsuite } else { $xml3.testsuite }
|
||||
$errorCount3 = ($testsuites3 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
|
||||
if ($errorCount3 -ne 1) { exit 1 }
|
||||
|
||||
Write-Host "self-signed ssl with default/system ca certs"
|
||||
Write-Host "request will error"
|
||||
# should fail
|
||||
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit4.xml --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
|
||||
# Ignore the exit code - we expect this to fail
|
||||
[xml]$xml4 = Get-Content junit4.xml
|
||||
$testsuites4 = if ($xml4.testsuites) { $xml4.testsuites.testsuite } else { $xml4.testsuite }
|
||||
$errorCount4 = ($testsuites4 | Where-Object { $_.errors -eq "1" } | Measure-Object).Count
|
||||
if ($errorCount4 -ne 1) { exit 1 }
|
||||
47
.github/actions/ssl/windows/run-custom-ca-certs-cli-tests/action.yml
vendored
Normal file
47
.github/actions/ssl/windows/run-custom-ca-certs-cli-tests/action.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: 'Run Custom CA Certs CLI Tests - Windows'
|
||||
description: 'Run custom CA certs CLI tests on Windows'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Run CLI tests
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# navigate to CA certificates test collection directory
|
||||
Set-Location tests\ssl\custom-ca-certs\collection
|
||||
|
||||
Write-Host "custom valid ca cert"
|
||||
# should pass
|
||||
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit1.xml --cacert ..\server\certs\ca-cert.pem --ignore-truststore --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
|
||||
[xml]$xml1 = Get-Content junit1.xml
|
||||
$testsuites1 = if ($xml1.testsuites) { $xml1.testsuites.testsuite } else { $xml1.testsuite }
|
||||
$errorCount1 = ($testsuites1 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
|
||||
if ($errorCount1 -ne 1) { exit 1 }
|
||||
|
||||
Write-Host "custom valid ca cert with defaults"
|
||||
# should pass
|
||||
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit2.xml --cacert ..\server\certs\ca-cert.pem --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
|
||||
[xml]$xml2 = Get-Content junit2.xml
|
||||
$testsuites2 = if ($xml2.testsuites) { $xml2.testsuites.testsuite } else { $xml2.testsuite }
|
||||
$errorCount2 = ($testsuites2 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
|
||||
if ($errorCount2 -ne 1) { exit 1 }
|
||||
|
||||
Write-Host "custom invalid ca cert"
|
||||
Write-Host "request will error"
|
||||
# should fail
|
||||
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit3.xml --cacert ..\server\certs\ca-key.pem --ignore-truststore --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
|
||||
# Ignore the exit code - we expect this to fail
|
||||
[xml]$xml3 = Get-Content junit3.xml
|
||||
$testsuites3 = if ($xml3.testsuites) { $xml3.testsuites.testsuite } else { $xml3.testsuite }
|
||||
$errorCount3 = ($testsuites3 | Where-Object { $_.errors -eq "1" } | Measure-Object).Count
|
||||
if ($errorCount3 -ne 1) { exit 1 }
|
||||
|
||||
Write-Host "custom invalid ca cert with defaults"
|
||||
# should pass
|
||||
$process = Start-Process -FilePath "node" -ArgumentList "..\..\..\..\packages\bruno-cli\bin\bru.js run .\request.bru --output junit4.xml --cacert ..\server\certs\ca-key.pem --format junit" -NoNewWindow -Wait -PassThru -RedirectStandardError "nul"
|
||||
[xml]$xml4 = Get-Content junit4.xml
|
||||
$testsuites4 = if ($xml4.testsuites) { $xml4.testsuites.testsuite } else { $xml4.testsuite }
|
||||
$errorCount4 = ($testsuites4 | Where-Object { $_.errors -eq "0" } | Measure-Object).Count
|
||||
if ($errorCount4 -ne 1) { exit 1 }
|
||||
17
.github/actions/ssl/windows/run-ssl-e2e-tests/action.yml
vendored
Normal file
17
.github/actions/ssl/windows/run-ssl-e2e-tests/action.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: 'Run SSL E2E Tests - Windows'
|
||||
description: 'Run SSL E2E tests on Windows'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Run E2E tests
|
||||
shell: pwsh
|
||||
run: |
|
||||
npm run test:e2e:ssl
|
||||
|
||||
- name: Upload Playwright Report
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report-windows
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
25
.github/actions/ssl/windows/setup-ca-certs/action.yml
vendored
Normal file
25
.github/actions/ssl/windows/setup-ca-certs/action.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: 'Setup CA Certificates - Windows'
|
||||
description: 'Setup CA certificates and start test server for custom CA certs tests on Windows'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup CA certificates
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Set-Location tests\ssl\custom-ca-certs\server
|
||||
|
||||
Write-Host "running certificate setup"
|
||||
node scripts/generate-certs.js
|
||||
|
||||
- name: Start test server
|
||||
shell: pwsh
|
||||
run: |
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
Set-Location tests\ssl\custom-ca-certs\server
|
||||
|
||||
Write-Host "starting server in background"
|
||||
Start-Process -FilePath "node" -ArgumentList "index.js" -PassThru -WindowStyle Hidden
|
||||
4
.github/workflows/npm-bru-cli.yml
vendored
4
.github/workflows/npm-bru-cli.yml
vendored
@@ -25,8 +25,8 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
|
||||
91
.github/workflows/ssl-tests.yml
vendored
Normal file
91
.github/workflows/ssl-tests.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
name: SSL Tests
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
tests-for-linux:
|
||||
name: SSL Tests - Linux
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
|
||||
- name: Setup Feature Dependencies
|
||||
uses: ./.github/actions/ssl/linux/setup-feature-specific-deps
|
||||
|
||||
- name: Setup CA Certificates
|
||||
uses: ./.github/actions/ssl/linux/setup-ca-certs
|
||||
|
||||
- name: Run Basic SSL CLI Tests
|
||||
uses: ./.github/actions/ssl/linux/run-basic-ssl-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs CLI Tests
|
||||
uses: ./.github/actions/ssl/linux/run-custom-ca-certs-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs E2E Tests
|
||||
uses: ./.github/actions/ssl/linux/run-ssl-e2e-tests
|
||||
|
||||
tests-for-macos:
|
||||
name: SSL Tests - macOS
|
||||
timeout-minutes: 60
|
||||
runs-on: macos-latest
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
|
||||
- name: Setup Feature Dependencies
|
||||
uses: ./.github/actions/ssl/macos/setup-feature-specific-deps
|
||||
|
||||
- name: Setup CA Certificates
|
||||
uses: ./.github/actions/ssl/macos/setup-ca-certs
|
||||
|
||||
- name: Run Basic SSL CLI Tests
|
||||
uses: ./.github/actions/ssl/macos/run-basic-ssl-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs CLI Tests
|
||||
uses: ./.github/actions/ssl/macos/run-custom-ca-certs-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs E2E Tests
|
||||
uses: ./.github/actions/ssl/macos/run-ssl-e2e-tests
|
||||
|
||||
tests-for-windows:
|
||||
name: SSL Tests - Windows
|
||||
timeout-minutes: 60
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
|
||||
- name: Setup CA Certificates
|
||||
uses: ./.github/actions/ssl/windows/setup-ca-certs
|
||||
|
||||
- name: Run Basic SSL CLI Tests
|
||||
uses: ./.github/actions/ssl/windows/run-basic-ssl-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs CLI Tests
|
||||
uses: ./.github/actions/ssl/windows/run-custom-ca-certs-cli-tests
|
||||
|
||||
- name: Run Custom CA Certs E2E Tests
|
||||
uses: ./.github/actions/ssl/windows/run-ssl-e2e-tests
|
||||
19
.github/workflows/tests.yml
vendored
19
.github/workflows/tests.yml
vendored
@@ -13,8 +13,8 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
@@ -34,6 +34,8 @@ jobs:
|
||||
|
||||
- name: Lint Check
|
||||
run: npm run lint
|
||||
env:
|
||||
ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || 'main' }}
|
||||
|
||||
# tests
|
||||
- name: Test Package bruno-js
|
||||
@@ -64,8 +66,8 @@ jobs:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
@@ -83,6 +85,11 @@ jobs:
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
npm run build --workspace=packages/bruno-filestore
|
||||
|
||||
- name: Run Local Testbench
|
||||
run: |
|
||||
npm start --workspace=packages/bruno-tests &
|
||||
sleep 5
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd packages/bruno-tests/collection
|
||||
@@ -101,8 +108,8 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: v22.11.x
|
||||
- name: Install dependencies
|
||||
|
||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
npx nano-staged
|
||||
44
CODING_STANDARDS.md
Normal file
44
CODING_STANDARDS.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Bruno Coding Standards
|
||||
|
||||
- No diffs unless an actual change is made, the code changes need to be as minimal as possible, avoid making un-necessary whitespace diffs. This is already handled by eslint but make sure you check your code changes before commiting and raising a PR.
|
||||
|
||||
## General Style Rules
|
||||
|
||||
- Use 2 spaces for indentation. No tabs, just spaces – keeps everything neat and uniform.
|
||||
|
||||
- Stick to single quotes for strings. Double quotes are cool elsewhere, but here we go single.
|
||||
|
||||
- Always add semicolons at the end of statements. It's like putting a period at the end of a sentence – clarity matters.
|
||||
|
||||
- JSX is enabled, so feel free to use it where it makes sense.
|
||||
|
||||
## Punctuation and Spacing
|
||||
|
||||
- No trailing commas. Keep it clean, no extra commas hanging around.
|
||||
|
||||
- Always use parentheses around parameters in arrow functions. Even for single params – consistency is key.
|
||||
|
||||
- For multiline constructs, put opening braces on the same line, and ensure consistency. Minimum 2 elements for multiline.
|
||||
|
||||
- No newlines inside function parentheses. Keep 'em tight.
|
||||
|
||||
- Space before and after the arrow in arrow functions. `() => {}` is good.
|
||||
|
||||
- No space between function name and parentheses. `func()` not `func ()`.
|
||||
|
||||
- Semicolons go at the end of the line, not on a new line.
|
||||
|
||||
- No strict max length – write readable code, not cramped lines.
|
||||
|
||||
- Multiple expressions per line in JSX are fine – flexibility is nice.
|
||||
|
||||
Remember, these rules are here to make our codebase harmonious. If something doesn't fit perfectly, let's chat about it. Happy coding! 🚀
|
||||
|
||||
## Readability and Abstractions
|
||||
|
||||
- Avoid abstractions unless the exact same code is being used in more than 3 places.
|
||||
- Names for functions need to be concise and descriptive.
|
||||
- Add in JSDoc comments to add more details to the abstractions if needed.
|
||||
- Follow functional programming but just enough to be readable, we don't need to go as deep as ADTs and Monads, we want to keep the code pipeline obvious and easy for everyone to read and contribute to.
|
||||
- Avoid single line abstractions where all that's being done is increasing the call stack with one additional function.
|
||||
- Add in meaningful comments instead of obvious ones where complex code flow is explained properly.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 537 KiB After Width: | Height: | Size: 813 KiB |
BIN
assets/images/vscode-demo.png
Normal file
BIN
assets/images/vscode-demo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 409 KiB |
@@ -16,6 +16,7 @@
|
||||
| [日本語](docs/contributing/contributing_ja.md)
|
||||
| [हिंदी](docs/contributing/contributing_hi.md)
|
||||
| [Dutch](docs/contributing/contributing_nl.md)
|
||||
| [فارسی](docs/contributing/contributing_fa.md)
|
||||
|
||||
## Let's make Bruno better, together!!
|
||||
|
||||
@@ -69,10 +70,12 @@ npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
npm run build:bruno-filestore
|
||||
|
||||
# bundle js sandbox libraries
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
```
|
||||
|
||||
##### Option 2
|
||||
|
||||
```bash
|
||||
@@ -93,18 +96,22 @@ npm run dev:electron
|
||||
```
|
||||
|
||||
##### Option 2
|
||||
|
||||
```bash
|
||||
# run electron and react app concurrently
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### Customize Electron `userData` path
|
||||
|
||||
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
|
||||
|
||||
e.g.
|
||||
|
||||
```sh
|
||||
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
|
||||
```
|
||||
|
||||
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
92
docs/contributing/contributing_fa.md
Normal file
92
docs/contributing/contributing_fa.md
Normal file
@@ -0,0 +1,92 @@
|
||||
[English](../../contributing.md)
|
||||
|
||||
## با هم، Bruno را بهتر میکنیم!
|
||||
|
||||
خوشحالم که قصد دارید Bruno را بهبود ببخشید. در ادامه قوانین و راهنماها برای راهاندازی Bruno روی سیستم شما آورده شده است.
|
||||
|
||||
### فناوریهای استفادهشده
|
||||
|
||||
به فارسی برونو Bruno با استفاده از Next.js و React ساخته شده است. همچنین از Electron برای بستهبندی نسخه دسکتاپ (که امکان مجموعههای محلی را فراهم میکند) استفاده میکنیم.
|
||||
|
||||
کتابخانههایی که استفاده میکنیم:
|
||||
|
||||
- CSS - Tailwind استایل
|
||||
- Codemirror - ویرایشگر کد
|
||||
- Redux - مدیریت وضعیت
|
||||
- Tabler Icons - آیکونها
|
||||
- formik - فرمها
|
||||
- Yup اعتبارسنجی اسکیمـا
|
||||
- axios - کلاینت درخواست
|
||||
- chokidar - پایشگر سیستم فایل
|
||||
|
||||
### پیشنیازها
|
||||
|
||||
شما به [نود v20.x یا اخرین نسخه پایدار](https://nodejs.org/en/) و npm 8.x نیاز دارید. در این پروژه از فضای کاری npm (npm workspaces) استفاده میکنیم.
|
||||
|
||||
### شروع به کدنویسی
|
||||
|
||||
برای راهاندازی محیط توسعه محلی به فایل [مستندات توسعه](docs/development_fa.md) مراجعه کنید:
|
||||
|
||||
### ارسال Pull Request
|
||||
|
||||
1 - لطفاً Pull Requestها (PR) را کوتاه و متمرکز نگه دارید و تنها یک هدف مشخص را دنبال کنند. </br>
|
||||
2 - لطفاً از فرمت نامگذاری شاخهها استفاده کنید:
|
||||
|
||||
- feature/[name]: این شاخه باید شامل یک قابلیت مشخص باشد.
|
||||
- feature/dark-mode : مثال
|
||||
- bugfix/[name]: این شاخه باید تنها شامل رفع یک باگ مشخص باشد.
|
||||
- bugfix/bug-1 : مثال
|
||||
|
||||
## توسعه
|
||||
|
||||
به فارسی برونو یا Bruno بهصورت یک اپلیکیشن «سنگین» توسعه داده میشود. برای اجرا باید ابتدا Next.js را در یک پنجره ترمینال اجرا کنید و سپس اپلیکیشن Electron را در پنجره ترمینال دیگری راهاندازی نمایید.
|
||||
|
||||
### نیازمندی توسعه
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### اجرای محلی
|
||||
|
||||
```bash
|
||||
# از ورژن NodeJS 18 استفاده کنید
|
||||
nvm use
|
||||
|
||||
# نصب وابستگیها
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# ساخت مستندات GraphQL
|
||||
npm run build:graphql-docs
|
||||
|
||||
# ساخت bruno-query
|
||||
npm run build:bruno-query
|
||||
|
||||
# اجرای اپ Next (ترمینال 1)
|
||||
npm run dev:web
|
||||
|
||||
# اجرای اپ Electron (ترمینال 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### عیبیابی
|
||||
|
||||
ممکن است هنگام اجرای `npm install` خطای `Unsupported platform` ببینید. برای رفع این مشکل، پوشه `node_modules` و فایل `package-lock.json` را حذف کرده و سپس دوباره `npm install` را اجرا کنید. این کار معمولاً همه پکیجهای لازم را نصب میکند.
|
||||
|
||||
```shell
|
||||
# حذف پوشه node_modules در زیردایرکتوریها
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# حذف فایل package-lock.json در زیردایرکتوریها
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### تستها
|
||||
|
||||
```bash
|
||||
# اجرای تستهای schema مربوط به bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# اجرای تستها در همه فضاهای کاری (در صورت وجود)
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
470
docs/playwright-testing-guide.md
Normal file
470
docs/playwright-testing-guide.md
Normal file
@@ -0,0 +1,470 @@
|
||||
# Playwright Testing Guide for Bruno
|
||||
|
||||
This guide explains how to create and run Playwright test cases for the Bruno application using the UI.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Creating Tests Using Codegen](#creating-tests-using-codegen)
|
||||
- [Manual Test Creation](#manual-test-creation)
|
||||
- [Test Structure and Organization](#test-structure-and-organization)
|
||||
- [Available Test Fixtures](#available-test-fixtures)
|
||||
- [Running Tests](#running-tests)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Examples](#examples)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Overview
|
||||
|
||||
Bruno uses Playwright for end-to-end testing of its Electron application. The testing setup includes custom fixtures for Electron app testing and utilities for managing test data.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js installed
|
||||
- All dependencies installed (`npm install`)
|
||||
- Electron app can be built and run
|
||||
|
||||
## Creating Tests Using Codegen
|
||||
|
||||
The easiest way to create tests is using Playwright's codegen feature, which records your UI interactions and generates test code.
|
||||
|
||||
### Using the Built-in Codegen Script
|
||||
|
||||
```bash
|
||||
# Generate a test with a specific name
|
||||
npm run test:codegen my-new-test
|
||||
|
||||
# Generate a test without specifying a name (will prompt for input)
|
||||
npm run test:codegen
|
||||
```
|
||||
|
||||
### What Happens During Codegen
|
||||
|
||||
1. The Electron app launches automatically
|
||||
2. Playwright Inspector opens in a separate window
|
||||
3. You interact with the Bruno UI
|
||||
4. Actions are recorded and converted to test code
|
||||
5. The generated test file is saved in `e2e-tests/`
|
||||
|
||||
### Codegen Workflow
|
||||
|
||||
1. **Start Recording**: Run the codegen command
|
||||
2. **Interact with UI**: Perform the actions you want to test
|
||||
3. **Add Assertions**: Use the inspector to add assertions
|
||||
4. **Save Test**: The test file is automatically generated
|
||||
5. **Review and Refine**: Edit the generated test as needed
|
||||
|
||||
## Manual Test Creation
|
||||
|
||||
You can also create tests manually by following the established patterns.
|
||||
|
||||
### Basic Test Structure
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Test description', async ({ page }) => {
|
||||
// Test steps here
|
||||
await page.getByLabel('Some Label').click();
|
||||
|
||||
// Assertions
|
||||
await expect(page.getByText('Expected Text')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### Test with Temporary Data
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Test with temporary data', async ({ page, createTmpDir }) => {
|
||||
// Create temporary directory for test data
|
||||
const testDir = await createTmpDir('test-collection');
|
||||
|
||||
// Test steps
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').fill('test-collection');
|
||||
await page.getByLabel('Location').fill(testDir);
|
||||
|
||||
// Assertions
|
||||
await expect(page.getByText('test-collection')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
## Test Structure and Organization
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
e2e-tests/
|
||||
├── 001-sanity-tests/ # Basic functionality tests
|
||||
│ ├── 001-home-screen.spec.ts
|
||||
│ └── 002-create-new-collection-and-new-request.spec.ts
|
||||
├── 002-feature-tests/ # Specific feature tests
|
||||
├── 003-integration-tests/ # Complex workflow tests
|
||||
└── bruno-testbench/ # Test utilities and helpers
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- **Files**: Use descriptive names with `.spec.ts` extension
|
||||
- **Tests**: Use clear, descriptive test names
|
||||
- **Folders**: Use numbered prefixes for ordering
|
||||
|
||||
### Test File Template
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test.describe('Feature Name', () => {
|
||||
test('should perform specific action', async ({ page }) => {
|
||||
// Arrange
|
||||
// Act
|
||||
// Assert
|
||||
});
|
||||
|
||||
test('should handle error case', async ({ page }) => {
|
||||
// Test error scenarios
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Available Test Fixtures
|
||||
|
||||
The Bruno Playwright setup provides several custom fixtures:
|
||||
|
||||
### Core Fixtures
|
||||
|
||||
- `page`: Main page for testing
|
||||
- `context`: Browser context
|
||||
- `electronApp`: Electron application instance
|
||||
|
||||
### Utility Fixtures
|
||||
|
||||
- `createTmpDir`: Creates temporary directories for test data
|
||||
- `newPage`: Creates a new page instance
|
||||
- `pageWithUserData`: Page with custom user data
|
||||
- `launchElectronApp`: Launches a new Electron app instance
|
||||
- `reuseOrLaunchElectronApp`: Reuses existing app or launches new one
|
||||
|
||||
### Using Fixtures
|
||||
|
||||
```typescript
|
||||
test('Test with multiple fixtures', async ({ page, createTmpDir, electronApp }) => {
|
||||
const testDir = await createTmpDir('test-data');
|
||||
|
||||
// Your test logic here
|
||||
});
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Basic Commands
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm run test:e2e
|
||||
|
||||
# Run specific test file
|
||||
npx playwright test e2e-tests/001-sanity-tests/001-home-screen.spec.ts
|
||||
|
||||
# Run tests in a specific folder
|
||||
npx playwright test e2e-tests/001-sanity-tests/
|
||||
```
|
||||
|
||||
### Advanced Options
|
||||
|
||||
```bash
|
||||
# Run with UI mode (for debugging)
|
||||
npx playwright test --ui
|
||||
|
||||
# Run in headed mode (see browser)
|
||||
npx playwright test --headed
|
||||
|
||||
# Run with specific browser
|
||||
npx playwright test --project="Bruno Electron App"
|
||||
|
||||
# Run with debugging
|
||||
npx playwright test --debug
|
||||
|
||||
# Run with trace recording
|
||||
npx playwright test --trace on
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
```bash
|
||||
# Install browsers for CI
|
||||
npx playwright install
|
||||
|
||||
# Run tests in CI mode
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Semantic Selectors
|
||||
|
||||
**Preferred:**
|
||||
|
||||
```typescript
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.getByLabel('Collection Name').fill('test');
|
||||
await page.getByText('Success message').toBeVisible();
|
||||
```
|
||||
|
||||
**Avoid:**
|
||||
|
||||
```typescript
|
||||
await page.locator('.btn-primary').click();
|
||||
await page.locator('#collection-name').fill('test');
|
||||
```
|
||||
|
||||
### 2. Create Isolated Tests
|
||||
|
||||
Each test should be independent and not rely on other tests:
|
||||
|
||||
```typescript
|
||||
test('should create collection', async ({ page, createTmpDir }) => {
|
||||
const testDir = await createTmpDir('collection-test');
|
||||
|
||||
// Test creates its own data
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').fill('test-collection');
|
||||
await page.getByLabel('Location').fill(testDir);
|
||||
|
||||
// Clean up happens automatically via createTmpDir
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Add Meaningful Assertions
|
||||
|
||||
Always verify the expected outcomes:
|
||||
|
||||
```typescript
|
||||
test('should save request successfully', async ({ page }) => {
|
||||
// Arrange
|
||||
await page.getByLabel('Create Collection').click();
|
||||
|
||||
// Act
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Assert
|
||||
await expect(page.getByText('Request saved successfully')).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: 'GET request' })).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Handle Async Operations
|
||||
|
||||
```typescript
|
||||
test('should wait for network requests', async ({ page }) => {
|
||||
// Wait for specific network request
|
||||
await page.waitForResponse((response) => response.url().includes('/api/endpoint'));
|
||||
|
||||
// Or wait for element to be stable
|
||||
await page.waitForSelector('[data-testid="loading"]', { state: 'hidden' });
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Use Test Data Management
|
||||
|
||||
```typescript
|
||||
test('should work with test data', async ({ page, createTmpDir }) => {
|
||||
const testDir = await createTmpDir('test-data');
|
||||
|
||||
// Create test files
|
||||
await fs.writeFile(path.join(testDir, 'test.bru'), testContent);
|
||||
|
||||
// Use in test
|
||||
await page.getByLabel('Open Collection').click();
|
||||
await page.getByText(testDir).click();
|
||||
});
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Basic Collection Creation
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('should create a new collection', async ({ page, createTmpDir }) => {
|
||||
const testDir = await createTmpDir('new-collection');
|
||||
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').fill('My Test Collection');
|
||||
await page.getByLabel('Location').fill(testDir);
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
await expect(page.getByText('My Test Collection')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
### Example 2: Request Creation and Execution
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('should create and execute HTTP request', async ({ page, createTmpDir }) => {
|
||||
const testDir = await createTmpDir('request-test');
|
||||
|
||||
// Create collection
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').fill('Request Test');
|
||||
await page.getByLabel('Location').fill(testDir);
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Create request
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
await page.getByPlaceholder('Request Name').fill('Test Request');
|
||||
await page.locator('#new-request-url .CodeMirror').click();
|
||||
await page.locator('textarea').fill('http://localhost:8081/ping');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Execute request
|
||||
await page.locator('#send-request').getByRole('img').nth(2).click();
|
||||
|
||||
// Verify response
|
||||
await expect(page.getByRole('main')).toContainText('200 OK');
|
||||
});
|
||||
```
|
||||
|
||||
### Example 3: Environment Management
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('should create and use environment variables', async ({ page, createTmpDir }) => {
|
||||
const testDir = await createTmpDir('env-test');
|
||||
|
||||
// Setup collection
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').fill('Environment Test');
|
||||
await page.getByLabel('Location').fill(testDir);
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Create environment
|
||||
await page.getByRole('button', { name: 'Environments' }).click();
|
||||
await page.getByRole('button', { name: 'Add Environment' }).click();
|
||||
await page.getByLabel('Environment Name').fill('Development');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Add variable
|
||||
await page.getByRole('button', { name: 'Add Variable' }).click();
|
||||
await page.getByLabel('Variable Name').fill('API_URL');
|
||||
await page.getByLabel('Variable Value').fill('http://localhost:3000');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
await expect(page.getByText('API_URL')).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Electron App Not Starting**
|
||||
|
||||
```bash
|
||||
# Ensure dependencies are installed
|
||||
npm install
|
||||
|
||||
# Try running the app manually first
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
2. **Tests Timing Out**
|
||||
|
||||
```typescript
|
||||
// Increase timeout for specific test
|
||||
test('slow test', async ({ page }) => {
|
||||
test.setTimeout(60000); // 60 seconds
|
||||
// Test steps
|
||||
});
|
||||
```
|
||||
|
||||
3. **Element Not Found**
|
||||
|
||||
```typescript
|
||||
// Wait for element to be present
|
||||
await page.waitForSelector('[data-testid="element"]');
|
||||
|
||||
// Or use more specific selectors
|
||||
await page.getByRole('button', { name: 'Exact Button Text' }).click();
|
||||
```
|
||||
|
||||
4. **Flaky Tests**
|
||||
|
||||
```typescript
|
||||
// Use stable selectors
|
||||
await page.getByTestId('stable-id').click();
|
||||
|
||||
// Wait for state changes
|
||||
await page.waitForLoadState('networkidle');
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
```bash
|
||||
# Run with debug mode
|
||||
npx playwright test --debug
|
||||
|
||||
# Run specific test in debug mode
|
||||
npx playwright test --debug e2e-tests/001-sanity-tests/001-home-screen.spec.ts
|
||||
```
|
||||
|
||||
### Trace Analysis
|
||||
|
||||
```bash
|
||||
# Run with trace recording
|
||||
npx playwright test --trace on
|
||||
|
||||
# View trace in browser
|
||||
npx playwright show-trace test-results/trace-*.zip
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The Playwright configuration is in `playwright.config.ts`:
|
||||
|
||||
```typescript
|
||||
export default defineConfig({
|
||||
testDir: './e2e-tests',
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
workers: process.env.CI ? undefined : 1,
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'Bruno Electron App'
|
||||
}
|
||||
],
|
||||
|
||||
webServer: [
|
||||
{
|
||||
command: 'npm run dev:web',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI
|
||||
},
|
||||
{
|
||||
command: 'npm start --workspace=packages/bruno-tests',
|
||||
url: 'http://localhost:8081/ping',
|
||||
reuseExistingServer: !process.env.CI
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Playwright Documentation](https://playwright.dev/)
|
||||
- [Playwright Test API](https://playwright.dev/docs/api/class-test)
|
||||
- [Electron Testing with Playwright](https://playwright.dev/docs/api/class-electronapplication)
|
||||
- [Bruno Project Structure](../readme.md)
|
||||
|
||||
---
|
||||
|
||||
For questions or issues with testing, please refer to the project's contributing guidelines or create an issue in the repository.
|
||||
8
docs/publishing/publishing_fa.md
Normal file
8
docs/publishing/publishing_fa.md
Normal file
@@ -0,0 +1,8 @@
|
||||
[English](../../publishing.md)
|
||||
|
||||
### انتشار Bruno در یک پکیج منیجر جدید
|
||||
|
||||
اگرچه کد ما متنباز است و همه میتوانند از آن استفاده کنند، لطفاً قبل از انتشار Bruno در مدیر بستههای جدید با ما تماس بگیرید. به عنوان سازنده Bruno، علامت تجاری `Bruno` را برای این پروژه دارم و مایلم توزیع آن را مدیریت کنم. اگر دوست دارید Bruno را در یک مدیر بسته جدید ببینید، لطفاً یک issue در گیتهاب ثبت کنید.
|
||||
|
||||
اگرچه بیشتر قابلیتهای ما رایگان و متنباز هستند (شامل REST و GraphQL Apis)،
|
||||
ما تلاش میکنیم بین اصول متنباز و توسعه پایدار تعادل مناسبی برقرار کنیم - https://github.com/usebruno/bruno/discussions/269
|
||||
@@ -41,13 +41,6 @@
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### الطبعة الذهبية ✨
|
||||
|
||||
غالبية ميزاتنا مجانية ومفتوحة المصدر.
|
||||
نحن نسعى لتحقيق توازن متناغم بين [مبادئ الشفافية والاستدامة](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
طلبات الشراء لـ [الطبعة الذهبية](https://www.usebruno.com/pricing) ستطلق قريبًا بسعر ~~$19~~ **$9** ! <br/>
|
||||
[اشترك هنا](https://usebruno.ck.page/4c65576bd4) لتصلك إشعارات عند الإطلاق.
|
||||
|
||||
### التثبيت
|
||||
|
||||
@@ -74,10 +67,13 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# على نظام Linux عبر Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
|
||||
@@ -59,10 +59,13 @@ snap install bruno
|
||||
|
||||
# Apt এর মাধ্যমে লিনাক্সে
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
|
||||
@@ -37,13 +37,37 @@ Bruno 直接在您的电脑文件夹中存储您的 API 信息。我们使用纯
|
||||
|
||||
Bruno 仅限离线使用。我们计划永不向 Bruno 添加云同步功能。我们重视您的数据隐私,并认为它应该留在您的设备上。阅读我们的长期愿景 [点击查看](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
[下载 Bruno](https://www.usebruno.com/downloads)
|
||||
|
||||
📢 观看我们在印度 FOSS 3.0 会议上的最新演讲 [点击查看](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### 安装
|
||||
## 商业版本 ✨
|
||||
|
||||
Bruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) Mac、Windows 和 Linux 的可执行文件。
|
||||
我们的大多数功能都是免费且开源的。
|
||||
我们致力于在 [开源与可持续性发展](https://github.com/usebruno/bruno/discussions/269) 之间取得和谐的平衡
|
||||
|
||||
欢迎使用我们的 [付费版本](https://www.usebruno.com/pricing) ,看看附加的功能是否对您或团队有所帮助! <br/>
|
||||
|
||||
## 目录
|
||||
- [安装](#安装)
|
||||
- [特性](#特性)
|
||||
- [跨平台使用 🖥️](#跨平台使用-)
|
||||
- [通过Git协作 👩💻🧑💻](#通过git协作-)
|
||||
- [重要链接 📌](#重要链接-)
|
||||
- [展示 🎥](#展示-)
|
||||
- [分享评价 📣](#分享评价-)
|
||||
- [发布到新的包管理器](#发布到新的包管理器)
|
||||
- [联系方式 🌐](#联系方式-)
|
||||
- [商标](#商标)
|
||||
- [贡献 👩💻🧑💻](#贡献-)
|
||||
- [作者](#作者)
|
||||
- [许可证 📄](#许可证-)
|
||||
|
||||
## 安装
|
||||
|
||||
Bruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) 适用于Mac、Windows 和 Linux 的可执行文件。
|
||||
|
||||
您也可以通过包管理器如 Homebrew、Chocolatey、Scoop、Snap 和 Apt 安装 Bruno。
|
||||
|
||||
@@ -58,79 +82,71 @@ choco install bruno
|
||||
scoop bucket add extras
|
||||
scoop install bruno
|
||||
|
||||
# 在 Windows 上用 winget 安装
|
||||
winget install Bruno.Bruno
|
||||
|
||||
# 在 Linux 上用 Snap 安装
|
||||
snap install bruno
|
||||
|
||||
# 在 Linux 上用 Flatpak 安装
|
||||
flatpak install com.usebruno.Bruno
|
||||
|
||||
# 在 Linux 上用 Apt 安装
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
### 在 Mac 上通过 Homebrew 安装 🖥️
|
||||
## 特性
|
||||
|
||||
### 跨平台使用 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Collaborate 安装 👩💻🧑💻
|
||||
### 通过Git协作 👩💻🧑💻
|
||||
|
||||
或者任何您选择的版本控制系统
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### 重要链接 📌
|
||||
## 重要链接 📌
|
||||
|
||||
- [我们的愿景](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [路线图](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [路线图](https://www.usebruno.com/roadmap)
|
||||
- [文档](https://docs.usebruno.com)
|
||||
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
|
||||
- [网站](https://www.usebruno.com)
|
||||
- [价格](https://www.usebruno.com/pricing)
|
||||
- [下载](https://www.usebruno.com/downloads)
|
||||
- [GitHub 赞助](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### 展示 🎥
|
||||
## 展示 🎥
|
||||
|
||||
- [Testimonials](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### 支持 ❤️
|
||||
|
||||
如果您喜欢 Bruno 并想支持我们的开源工作,请考虑通过 [GitHub Sponsors](https://github.com/sponsors/helloanoop) 来赞助我们。
|
||||
|
||||
### 分享评价 📣
|
||||
## 分享评价 📣
|
||||
|
||||
如果 Bruno 在您的工作和团队中帮助了您,请不要忘记在我们的 GitHub 讨论上分享您的 [评价](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
### 发布到新的包管理器
|
||||
## 发布到新的包管理器
|
||||
|
||||
有关更多信息,请参见 [此处](../publishing/publishing_cn.md) 。
|
||||
如需了解更多信息,请参见 [此处](../publishing/publishing_cn.md) 。
|
||||
|
||||
### 贡献 👩💻🧑💻
|
||||
|
||||
我很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。
|
||||
|
||||
即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。
|
||||
|
||||
### 作者
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### 联系方式 🌐
|
||||
## 联系方式 🌐
|
||||
|
||||
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
|
||||
[Website](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[LinkedIn](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### 商标
|
||||
## 商标
|
||||
|
||||
**名称**
|
||||
|
||||
@@ -140,6 +156,20 @@ sudo apt update && sudo apt install bruno
|
||||
|
||||
Logo 源自 [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### 许可证 📄
|
||||
## 贡献 👩💻🧑💻
|
||||
|
||||
很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。
|
||||
|
||||
即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。
|
||||
|
||||
## 作者
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## 许可证 📄
|
||||
|
||||
[MIT](../../license.md)
|
||||
|
||||
@@ -43,13 +43,6 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Syn
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Golden Edition ✨
|
||||
|
||||
Die meisten unserer Funktionen sind kostenlos und quelloffen.
|
||||
Wir bemühen uns um ein Gleichgewicht zwischen [Open-Source-Prinzipien und Nachhaltigkeit](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
Du kannst die [Golden Edition](https://www.usebruno.com/pricing) bestellen **$19**! <br/>
|
||||
|
||||
### Installation
|
||||
|
||||
Bruno ist als Download [auf unserer Website](https://www.usebruno.com/downloads) für Mac, Windows und Linux verfügbar.
|
||||
@@ -78,10 +71,13 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# Auf Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
|
||||
@@ -43,13 +43,6 @@ Bruno funciona sin conexión a internet. No tenemos intenciones de añadir sincr
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Golden Edition ✨
|
||||
|
||||
La mayoría de nuestras funcionalidades son gratis y de código abierto.
|
||||
Queremos alcanzar un equilibrio en armonía entre los [principios open-source y la sostenibilidad](https://github.com/usebruno/bruno/discussions/269).
|
||||
|
||||
¡Puedes reservar la [Golden Edition](https://www.usebruno.com/pricing) por ~~$19~~ **$9**! <br/>
|
||||
|
||||
### Instalación
|
||||
|
||||
Bruno está disponible para su descarga [en nuestro sitio web](https://www.usebruno.com/downloads) para Mac, Windows y Linux.
|
||||
@@ -75,10 +68,13 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# En Linux con Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
|
||||
143
docs/readme/readme_fa.md
Normal file
143
docs/readme/readme_fa.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### برونو یا Bruno - محیط توسعه متن باز برای تست و توسعه API ها
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
[English](../../readme.md)
|
||||
| [Українська](./readme_ua.md)
|
||||
| [Русский](./readme_ru.md)
|
||||
| [Türkçe](./readme_tr.md)
|
||||
| [Deutsch](./readme_de.md)
|
||||
| [Français](./readme_fr.md)
|
||||
| [Português (BR)](./readme_pt_br.md)
|
||||
| [한국어](./readme_kr.md)
|
||||
| [বাংলা](./readme_bn.md)
|
||||
| [Español](./readme_es.md)
|
||||
| **فارسی**
|
||||
| [Română](./readme_ro.md)
|
||||
| [Polski](./readme_pl.md)
|
||||
| [简体中文](./readme_cn.md)
|
||||
| [正體中文](./readme_zhtw.md)
|
||||
| [العربية](./readme_ar.md)
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
|
||||
برونو یک کلاینت API جدید و نوآورانه است که هدفش تغییر وضعیت فعلی ابزارهایی مانند Postman و سایر ابزارهای مشابه است.
|
||||
|
||||
برونو مجموعههای شما را مستقیماً در یک پوشه روی فایلسیستم شما ذخیره میکند. ما از یک زبان نشانهگذاری ساده به نام Bru برای ذخیره اطلاعات درخواستهای API استفاده میکنیم.
|
||||
|
||||
شما میتوانید برای همکاری روی مجموعههای API خود، از Git یا هر سیستم کنترل نسخه دلخواهتان استفاده کنید.
|
||||
|
||||
برونو فقط به صورت آفلاین کار میکند. هیچ برنامهای برای اضافه کردن همگامسازی ابری به برونو در آینده وجود ندارد. ما به حریم خصوصی دادههای شما اهمیت میدهیم و معتقدیم که باید روی دستگاه خودتان باقی بمانند. میتوانید چشمانداز بلندمدت ما را مطالعه کنید. [اینجا (به انگلیسی)](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
📢 جدیدترین ارائه ما را در کنفرانس India FOSS 3.0 تماشا کنید.
|
||||
[اینجا](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### نصب
|
||||
|
||||
برونو به صورت یک فایل باینری برای دانلود در دسترس است. [بر روی وبسایت ما](https://www.usebruno.com/downloads) برای مک لینکوس و ویندوز.
|
||||
|
||||
همچنین میتوانید برونو را از طریق مدیر بستههایی مانند Homebrew، Chocolatey، Snap و Apt نصب کنید.
|
||||
|
||||
```sh
|
||||
# بر روی مک از طریق brew
|
||||
brew install bruno
|
||||
|
||||
# بر روی ویندوز از طریق Chocolatey
|
||||
choco install bruno
|
||||
|
||||
# بر روی لینوکس از طریق Snap
|
||||
snap install bruno
|
||||
|
||||
# بر روی لینوکس از طریق Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
### روی پلتفرمهای مختلف کار میکند 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### همکاری از طریق گیت 👩💻🧑💻
|
||||
|
||||
یا هر سیستم کنترل نسخهای که ترجیح میدهید
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### لینکهای مهم 📌
|
||||
|
||||
- [آخرین نسخه پایدار ما](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [نقشه راه](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [مستندات](https://docs.usebruno.com)
|
||||
- [وبسایت](https://www.usebruno.com)
|
||||
- [اشتراک ها](https://www.usebruno.com/pricing)
|
||||
- [دانلود](https://www.usebruno.com/downloads)
|
||||
|
||||
### ویدیوها 🎥
|
||||
|
||||
- [تجربه ها](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [مرکز دانش](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [اسکریپ مانیا](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### حمایت ❤️
|
||||
|
||||
جوون! اگر این پروژه را دوست دارید، روی دکمه ⭐ کلیک کنید!
|
||||
|
||||
### تجربههای به اشتراک گذاشتهشده 📣
|
||||
|
||||
اگر برونو به شما یا تیمتان کمک کرده است، لطفاً فراموش نکنید تجربههای خود را به اشتراک بگذارید. [تجربههای خود را در بحث گیتهاب ما به اشتراک بگذارید](https://github.com/usebruno/bruno/discussions/343).
|
||||
|
||||
### انتشار برونو در یک پکیچ منیجر جدید
|
||||
|
||||
لطفا چک بکنید [اینجارو](../../publishing.md) برای اطلاعات بیشتر.
|
||||
|
||||
### مشارکت 👩💻🧑💻
|
||||
|
||||
خوشحالم که میخواهید برونو را بهتر کنید. لطفا [راهنمای مشارکت را بررسی کنید](../contributing/contributing_fa.md).
|
||||
|
||||
حتی اگر نمیتوانید از طریق کدنویسی مشارکت کنید، در گزارش باگها و درخواست قابلیتهای جدید که به حل نیازهای شما کمک میکند تردید نکنید.
|
||||
|
||||
### نویسنده ها
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### در ارتباط باشید 🌐
|
||||
|
||||
[𝕏 (تویتر)](https://twitter.com/use_bruno) <br />
|
||||
[وبسایت](https://www.usebruno.com) <br />
|
||||
[دیسکورد](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[لینکدین](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### برند
|
||||
|
||||
**نام**
|
||||
|
||||
به فارسی برونو - `Bruno` یک علامت تجاری ثبتشده متعلق به [Anoop M D](https://www.helloanoop.com/)
|
||||
|
||||
**لوگو**
|
||||
|
||||
لوگو توسط [OpenMoji](https://openmoji.org/library/emoji-1F436/) ساخته شده است. مجوز: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### مجوز 📄
|
||||
|
||||
[MIT](../../license.md)
|
||||
@@ -63,10 +63,13 @@ snap install bruno
|
||||
|
||||
# Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
|
||||
@@ -75,12 +75,14 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# Linux पर Apt के माध्यम से
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
|
||||
कई प्लेटफार्मों पर चलाएं 🖥️
|
||||
<br /><br />
|
||||
@@ -148,4 +150,3 @@ Scriptmania
|
||||
|
||||
लाइसेंस 📄
|
||||
MIT
|
||||
|
||||
|
||||
@@ -59,10 +59,13 @@ snap install bruno
|
||||
|
||||
# Su Linux tramite Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
|
||||
@@ -43,13 +43,6 @@ Bruno はオフラインのみで利用できます。Bruno にクラウド同
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### ゴールデンエディション ✨
|
||||
|
||||
機能のほとんどが無料で使用でき、オープンソースとなっています。
|
||||
私たちは[オープンソースの原則と長期的な維持](https://github.com/usebruno/bruno/discussions/269)の間でうまくバランスを取ろうと努力しています。
|
||||
|
||||
[ゴールデンエディション](https://www.usebruno.com/pricing)を **19 ドル** (買い切り)で購入できます!
|
||||
|
||||
### インストール方法
|
||||
|
||||
Bruno は[私たちのウェブサイト](https://www.usebruno.com/downloads)からバイナリをダウンロードできます。Mac, Windows, Linux に対応しています。
|
||||
@@ -78,10 +71,13 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# LinuxでAptを使ってインストール
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
|
||||
@@ -43,12 +43,6 @@
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### ოქროს გამოცემა ✨
|
||||
|
||||
მთავარი ფუნქციების უმეტესობა უფასოა და ღია წყაროა. ჩვენ ვცდილობთ ჰარმონიული ბალანსის დაცვას [ღია წყაროების პრინციპებსა და მდგრადობას შორის](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
თქვენ შეგიძლიათ შეიძინოთ [ოქროს გამოცემა](https://www.usebruno.com/pricing) ერთჯერადი გადახდით **19 დოლარად**! <br/>
|
||||
|
||||
### ინსტალაცია
|
||||
|
||||
ბრუნო ხელმისაწვდომია როგორც ბინარული ჩამოტვირთვა [ჩვენ的网站上](https://www.usebruno.com/downloads) Mac-ის, Windows-ისა და Linux-ისთვის.
|
||||
@@ -77,12 +71,14 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# Linux-ზე Apt-ის საშუალებით
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
### პლატფორმებს შორის მუშაობა 🖥️
|
||||
|
||||
@@ -59,10 +59,13 @@ snap install bruno
|
||||
|
||||
# On Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
|
||||
@@ -26,13 +26,6 @@ Bruno is uitsluitend offline. Er zijn geen plannen om ooit cloud-synchronisatie
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Golden Edition ✨
|
||||
|
||||
De meeste van onze functies zijn gratis en open source.
|
||||
We streven naar een harmonieuze balans tussen [open-source principes en duurzaamheid](https://github.com/usebruno/bruno/discussions/269).
|
||||
|
||||
Je kunt de [Golden Edition](https://www.usebruno.com/pricing) kopen voor een eenmalige betaling van **$19**! <br/>
|
||||
|
||||
### Installatie
|
||||
|
||||
Bruno is beschikbaar als binaire download [op onze website](https://www.usebruno.com/downloads) voor Mac, Windows en Linux.
|
||||
@@ -61,12 +54,14 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# Op Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
|
||||
sudo apt update
|
||||
sudo apt install bruno
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
### Draai op meerdere platformen 🖥️
|
||||
|
||||
@@ -69,10 +69,13 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# On Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
|
||||
@@ -41,13 +41,6 @@ Bruno é totalmente offline. Não há planos de adicionar sincronização em nuv
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Golden Edition ✨
|
||||
|
||||
A grande maioria dos nossos recursos são gratuitos e de código aberto.
|
||||
Nós nos esforçamos para encontrar um equilíbrio harmônico entre [princípios de código aberto e sustentabilidade](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
Você pode pré encomendar o plano [Golden Edition](https://www.usebruno.com/pricing) por ~~USD $19~~ **USD $9**! <br/>
|
||||
|
||||
### Instalação
|
||||
|
||||
Bruno está disponível para download como binário [em nosso site](https://www.usebruno.com/downloads) para Mac, Windows e Linux.
|
||||
@@ -76,10 +69,13 @@ flatpak install com.usebruno.Bruno
|
||||
|
||||
# No Linux via Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
|
||||
@@ -59,10 +59,13 @@ snap install bruno
|
||||
|
||||
# Pe Linux cu Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
|
||||
@@ -63,10 +63,13 @@ snap install bruno
|
||||
|
||||
# Apt aracılığıyla Linux'ta
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
|
||||
@@ -63,10 +63,13 @@ snap install bruno
|
||||
|
||||
# 在 Linux 上使用 Apt 安裝
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg
|
||||
sudo gpg --list-keys
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" | sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Create new collection and add a simple HTTP request', async ({ page, createTmpDir }) => {
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').click();
|
||||
await page.getByLabel('Name').fill('test-collection');
|
||||
await page.getByLabel('Name').press('Tab');
|
||||
await page.getByLabel('Location').fill(await createTmpDir('test-collection'));
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
await page.getByText('test-collection').click();
|
||||
await page.getByLabel('Safe Mode').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
await page.getByPlaceholder('Request Name').fill('r1');
|
||||
await page.locator('#new-request-url .CodeMirror').click();
|
||||
await page.locator('textarea').fill('http://localhost:8081');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.locator('#request-url .CodeMirror').click();
|
||||
await page.locator('textarea').fill('/ping');
|
||||
await page.locator('#send-request').getByRole('img').nth(2).click();
|
||||
|
||||
await expect(page.getByRole('main')).toContainText('200 OK');
|
||||
|
||||
await page.getByRole('tab', { name: 'GET r1' }).locator('circle').click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await page.getByText('GETr1').click();
|
||||
await page.getByRole('button', { name: 'Clear response' }).click();
|
||||
await page.locator('body').press('ControlOrMeta+Enter');
|
||||
|
||||
await expect(page.getByRole('main')).toContainText('200 OK');
|
||||
});
|
||||
@@ -1,49 +0,0 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test.describe.parallel('Run Testbench Requests', () => {
|
||||
test('Run bruno-testbench in Developer Mode', async ({ pageWithUserData: page }) => {
|
||||
test.setTimeout(2 * 60 * 1000);
|
||||
|
||||
await page.getByText('bruno-testbench').click();
|
||||
await page.getByLabel('Developer Mode(use only if').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('.environment-selector').nth(1).click();
|
||||
await page.locator('.dropdown-item').getByText('Prod').click();
|
||||
await page.locator('.collection-actions').hover();
|
||||
await page.locator('.collection-actions .icon').click();
|
||||
await page.getByText('Run', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Run Collection' }).click();
|
||||
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
|
||||
|
||||
const result = await page.getByText('Total Requests: ').innerText();
|
||||
const [totalRequests, passed, failed, skipped] = result
|
||||
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
|
||||
.slice(1);
|
||||
|
||||
await expect(parseInt(failed)).toBe(0);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed));
|
||||
});
|
||||
|
||||
test.fixme('Run bruno-testbench in Safe Mode', async ({ pageWithUserData: page }) => {
|
||||
test.setTimeout(2 * 60 * 1000);
|
||||
|
||||
await page.getByText('bruno-testbench').click();
|
||||
await page.getByLabel('Safe Mode').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('.environment-selector').nth(1).click();
|
||||
await page.locator('.dropdown-item').getByText('Prod').click();
|
||||
await page.locator('.collection-actions').hover();
|
||||
await page.locator('.collection-actions .icon').click();
|
||||
await page.getByText('Run', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Run Collection' }).click();
|
||||
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
|
||||
|
||||
const result = await page.getByText('Total Requests: ').innerText();
|
||||
const [totalRequests, passed, failed, skipped] = result
|
||||
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
|
||||
.slice(1);
|
||||
|
||||
await expect(parseInt(failed)).toBe(0);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - parseInt(failed));
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test.describe.serial('Persistent Environment Test', () => {
|
||||
test.setTimeout(2 * 10 * 1000);
|
||||
|
||||
test('add env using script', async ({ pageWithUserData: page, restartApp }) => {
|
||||
await page.locator('#sidebar-collection-name').click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByText('ping', { exact: true }).click();
|
||||
await page.getByText('No Environment').click();
|
||||
await page.getByRole('tooltip').locator('div').filter({ hasText: 'Env' }).nth(3).click();
|
||||
await page.locator('#send-request').getByRole('img').nth(2).click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.locator('div').filter({ hasText: /^Env$/ }).nth(3).click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
await expect(page.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible();
|
||||
await page.getByText('×').click();
|
||||
|
||||
const newApp = await restartApp();
|
||||
const newPage = await newApp.firstWindow();
|
||||
await newPage.locator('#sidebar-collection-name').click();
|
||||
await newPage.getByRole('button', { name: 'Save' }).click();
|
||||
await newPage.getByText('ping', { exact: true }).click();
|
||||
await newPage.getByText('No Environment').click();
|
||||
await newPage.getByRole('tooltip').locator('div').filter({ hasText: 'Env' }).nth(3).click();
|
||||
await newPage.locator('div').filter({ hasText: /^Env$/ }).nth(3).click();
|
||||
await newPage.getByText('Configure', { exact: true }).click();
|
||||
await expect(newPage.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible();
|
||||
await newPage.getByText('×').click();
|
||||
await newPage.waitForTimeout(1000);
|
||||
await newPage.close();
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test.describe.serial('Persistent Environment Test', () => {
|
||||
test.setTimeout(2 * 10 * 1000);
|
||||
|
||||
test('add env using script', async ({ pageWithUserData: page, restartApp }) => {
|
||||
await page.locator('#sidebar-collection-name').click();
|
||||
await page.getByText('ping2', { exact: true }).click();
|
||||
await page.getByText('Env', { exact: true }).click();
|
||||
await page.getByText('Stage', { exact: true }).click();
|
||||
await page.locator('#send-request').getByRole('img').nth(2).click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Stage$/ })
|
||||
.nth(3)
|
||||
.click();
|
||||
await page.getByText('Configure', { exact: true }).click();
|
||||
await expect(page.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).toBeVisible();
|
||||
await page.getByText('×').click();
|
||||
|
||||
const newApp = await restartApp();
|
||||
const newPage = await newApp.firstWindow();
|
||||
await newPage.locator('#sidebar-collection-name').click();
|
||||
await newPage.getByRole('button', { name: 'Save' }).click();
|
||||
await newPage.getByText('ping2', { exact: true }).click();
|
||||
await newPage.getByText('No Environment').click();
|
||||
await newPage.getByText('Stage').click();
|
||||
await newPage
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Stage$/ })
|
||||
.nth(3)
|
||||
.click();
|
||||
await newPage.getByText('Configure', { exact: true }).click();
|
||||
await expect(newPage.getByRole('row', { name: 'persistent-env-test' }).getByRole('cell').nth(3)).not.toBeVisible();
|
||||
await newPage.getByText('×').click();
|
||||
await newPage.waitForTimeout(1000);
|
||||
await newPage.close();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +0,0 @@
|
||||
vars {
|
||||
host: https://testbench-sanity.usebruno.com
|
||||
persistent-env-test: persistent-env-test-value
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
vars {
|
||||
host: https://testbench-sanity.usebruno.com
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/e2e-tests/persistent-env-tests/collection"
|
||||
]
|
||||
}
|
||||
222
eslint.config.js
222
eslint.config.js
@@ -1,11 +1,75 @@
|
||||
// eslint.config.js
|
||||
const { defineConfig } = require("eslint/config");
|
||||
const globals = require("globals");
|
||||
const { fixupPluginRules } = require('@eslint/compat');
|
||||
const eslintPluginDiff = require('eslint-plugin-diff');
|
||||
|
||||
module.exports = defineConfig([
|
||||
let stylistic;
|
||||
|
||||
const runESMImports = async () => {
|
||||
stylistic = await import('@stylistic/eslint-plugin').then((d) => d.default);
|
||||
};
|
||||
|
||||
module.exports = runESMImports().then(() => defineConfig([
|
||||
{
|
||||
plugins: {
|
||||
'diff': fixupPluginRules(eslintPluginDiff),
|
||||
'@stylistic': stylistic
|
||||
},
|
||||
languageOptions: {
|
||||
parser: require('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
files: [
|
||||
'./eslint.config.js',
|
||||
'tests/**/*.{ts,js}',
|
||||
'playwright/**/*.{js,ts}',
|
||||
'packages/bruno-app/**/*.{js,jsx,ts}',
|
||||
'packages/bruno-app/src/test-utils/mocks/codemirror.js',
|
||||
'packages/bruno-cli/**/*.js',
|
||||
'packages/bruno-common/**/*.ts',
|
||||
'packages/bruno-converters/**/*.js',
|
||||
'packages/bruno-electron/**/*.js',
|
||||
'packages/bruno-filestore/**/*.ts',
|
||||
'packages/bruno-js/**/*.js',
|
||||
'packages/bruno-lang/**/*.js',
|
||||
'packages/bruno-requests/**/*.ts',
|
||||
'packages/bruno-requests/**/*.js',
|
||||
'packages/bruno-tests/**/*.{js,ts}'
|
||||
],
|
||||
processor: 'diff/diff',
|
||||
rules: {
|
||||
...stylistic.configs.customize({
|
||||
indent: 2,
|
||||
quotes: 'single',
|
||||
semi: true,
|
||||
jsx: true
|
||||
}).rules,
|
||||
'@stylistic/comma-dangle': ['error', 'never'],
|
||||
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],
|
||||
'@stylistic/arrow-parens': ['error', 'always'],
|
||||
'@stylistic/curly-newline': ['error', {
|
||||
multiline: true,
|
||||
minElements: 2,
|
||||
consistent: true
|
||||
}],
|
||||
'@stylistic/function-paren-newline': ['error', 'never'],
|
||||
'@stylistic/array-bracket-spacing': ['error', 'never'],
|
||||
'@stylistic/arrow-spacing': ['error', { before: true, after: true }],
|
||||
'@stylistic/function-call-spacing': ['error', 'never'],
|
||||
'@stylistic/multiline-ternary': ['off'],
|
||||
'@stylistic/padding-line-between-statements': ['off'],
|
||||
'@stylistic/semi-style': ['error', 'last'],
|
||||
'@stylistic/max-len': ['off'],
|
||||
'@stylistic/jsx-one-expression-per-line': ['off']
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
|
||||
ignores: ["**/*.config.js"],
|
||||
ignores: ["**/*.config.js", "**/public/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
@@ -13,7 +77,8 @@ module.exports = defineConfig([
|
||||
global: false,
|
||||
require: false,
|
||||
Buffer: false,
|
||||
process: false
|
||||
process: false,
|
||||
ipcRenderer: false
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
@@ -39,8 +104,60 @@ module.exports = defineConfig([
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-electron/**/*.{js}"],
|
||||
files: ["packages/bruno-cli/**/*.js"],
|
||||
ignores: ["**/*.config.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest"
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-common/**/*.ts"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parser: require("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./packages/bruno-common/tsconfig.json",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-converters/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-electron/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/web/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
@@ -50,5 +167,98 @@ module.exports = defineConfig([
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
}
|
||||
]);
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-filestore/**/*.ts"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parser: require("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./packages/bruno-filestore/tsconfig.json",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-js/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
window: false,
|
||||
self: false,
|
||||
HTMLElement: false,
|
||||
typeDetectGlobalObject: false
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-lang/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-requests/**/*.ts"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parser: require("@typescript-eslint/parser"),
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./packages/bruno-requests/tsconfig.json",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-requests/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
]));
|
||||
|
||||
3924
package-lock.json
generated
3924
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -19,19 +19,25 @@
|
||||
],
|
||||
"homepage": "https://usebruno.com",
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@stylistic/eslint-plugin": "^5.3.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.14.1",
|
||||
"@typescript-eslint/parser": "^8.39.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-plugin-diff": "^2.0.3",
|
||||
"fs-extra": "^11.1.1",
|
||||
"globals": "^16.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nano-staged": "^0.8.0",
|
||||
"playwright": "^1.51.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
@@ -64,9 +70,17 @@
|
||||
"build:electron:snap": "./scripts/build-electron.sh snap",
|
||||
"watch:common": "npm run watch --workspace=packages/bruno-common",
|
||||
"test:codegen": "node playwright/codegen.ts",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e": "playwright test --project=default",
|
||||
"test:e2e:ssl": "playwright test --project=ssl",
|
||||
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||
"lint": "node --max_old_space_size=4096 $(npx which eslint)"
|
||||
"lint": "node --max_old_space_size=4096 $(npx which eslint)",
|
||||
"lint:fix": "node --max_old_space_size=4096 $(npx which eslint) --fix",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"nano-staged": {
|
||||
"*.{js,ts,jsx}": [
|
||||
"npm run lint:fix"
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.29.5",
|
||||
@@ -76,4 +90,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@prantlf/jsonlint": "^16.0.0",
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"@tabler/icons": "^1.46.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
@@ -26,6 +27,8 @@
|
||||
"cookie": "0.7.1",
|
||||
"dompurify": "^3.2.4",
|
||||
"escape-html": "^1.0.3",
|
||||
"fast-fuzzy": "^1.12.0",
|
||||
"fast-json-format": "~0.4.0",
|
||||
"file": "^0.2.2",
|
||||
"file-dialog": "^0.0.8",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -34,9 +37,9 @@
|
||||
"graphiql": "3.7.1",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"hexy": "^0.3.5",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"i18next": "24.1.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"jsesc": "^3.0.2",
|
||||
@@ -45,6 +48,7 @@
|
||||
"jsonc-parser": "^3.2.1",
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.2",
|
||||
"markdown-it-replace-link": "^1.2.0",
|
||||
@@ -92,7 +96,7 @@
|
||||
"@rsbuild/plugin-react": "^1.0.7",
|
||||
"@rsbuild/plugin-sass": "^1.1.0",
|
||||
"@rsbuild/plugin-styled-components": "1.1.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrappe
|
||||
|
||||
const AccordionContext = createContext();
|
||||
|
||||
const Accordion = ({ children, defaultIndex }) => {
|
||||
const Accordion = ({ children, defaultIndex, dataTestId }) => {
|
||||
const [openIndex, setOpenIndex] = useState(defaultIndex);
|
||||
|
||||
const toggleItem = (index) => {
|
||||
@@ -13,7 +13,7 @@ const Accordion = ({ children, defaultIndex }) => {
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={{ openIndex, toggleItem }}>
|
||||
<div>{children}</div>
|
||||
<div data-testid={dataTestId}>{children}</div>
|
||||
</AccordionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
83
packages/bruno-app/src/components/BodyModeSelector/index.js
Normal file
83
packages/bruno-app/src/components/BodyModeSelector/index.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { humanizeRequestBodyMode } from 'utils/collections';
|
||||
|
||||
const DEFAULT_MODES = [
|
||||
{ key: 'multipartForm', label: 'Multipart Form', category: 'Form' },
|
||||
{ key: 'formUrlEncoded', label: 'Form URL Encoded', category: 'Form' },
|
||||
{ key: 'json', label: 'JSON', category: 'Raw' },
|
||||
{ key: 'xml', label: 'XML', category: 'Raw' },
|
||||
{ key: 'text', label: 'TEXT', category: 'Raw' },
|
||||
{ key: 'sparql', label: 'SPARQL', category: 'Raw' },
|
||||
{ key: 'file', label: 'File / Binary', category: 'Other' },
|
||||
{ key: 'none', label: 'None', category: 'Other' }
|
||||
];
|
||||
|
||||
const BodyModeSelector = ({
|
||||
currentMode,
|
||||
onModeChange,
|
||||
modes = DEFAULT_MODES,
|
||||
disabled = false,
|
||||
className = '',
|
||||
wrapperClassName = '',
|
||||
showCategories = true,
|
||||
placement = 'bottom-end'
|
||||
}) => {
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none selected-body-mode">
|
||||
{humanizeRequestBodyMode(currentMode)}
|
||||
{' '}
|
||||
<IconCaretDown className="caret ml-2" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onModeSelect = (mode) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange(mode);
|
||||
};
|
||||
|
||||
// Group modes by category for rendering
|
||||
const groupedModes = modes.reduce((acc, mode) => {
|
||||
const category = mode.category || 'Other';
|
||||
if (!acc[category]) {
|
||||
acc[category] = [];
|
||||
}
|
||||
acc[category].push(mode);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<div className={`inline-flex items-center body-mode-selector ${disabled ? 'cursor-default' : 'cursor-pointer'} ${wrapperClassName}`}>
|
||||
<Dropdown
|
||||
onCreate={onDropdownCreate}
|
||||
icon={<Icon />}
|
||||
placement={placement}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
>
|
||||
{Object.entries(groupedModes).map(([category, categoryModes]) => (
|
||||
<React.Fragment key={category}>
|
||||
{showCategories && <div className="label-item font-medium">{category}</div>}
|
||||
{categoryModes.map((mode) => (
|
||||
<div
|
||||
key={mode.key}
|
||||
className="dropdown-item"
|
||||
onClick={() => onModeSelect(mode.key)}
|
||||
>
|
||||
{mode.label}
|
||||
</div>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BodyModeSelector;
|
||||
79
packages/bruno-app/src/components/Checkbox/StyledWrapper.js
Normal file
79
packages/bruno-app/src/components/Checkbox/StyledWrapper.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.checkbox-container {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-checkmark {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
visibility: ${(props) => props.checked ? 'visible' : 'hidden'};
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.checkbox-input {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid ${(props) => {
|
||||
if (props.checked && props.disabled) {
|
||||
return props.theme.colors.text.muted;
|
||||
}
|
||||
|
||||
if (props.checked && !props.disabled) {
|
||||
return props.theme.colors.text.yellow;
|
||||
}
|
||||
|
||||
return props.theme.colors.text.muted;
|
||||
}};
|
||||
border-radius: 4px;
|
||||
background-color: ${(props) => {
|
||||
if (props.checked && !props.disabled) {
|
||||
return props.theme.colors.text.yellow;
|
||||
}
|
||||
|
||||
if (props.checked && props.disabled) {
|
||||
return props.theme.colors.text.muted;
|
||||
}
|
||||
|
||||
return 'transparent';
|
||||
}};
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px ${(props) => props.theme.colors.text.yellow}40;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
45
packages/bruno-app/src/components/Checkbox/index.js
Normal file
45
packages/bruno-app/src/components/Checkbox/index.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import IconCheckMark from 'components/Icons/IconCheckMark';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const Checkbox = ({
|
||||
checked = false,
|
||||
disabled = false,
|
||||
onChange,
|
||||
className = '',
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
dataTestId = 'checkbox'
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const handleChange = (e) => {
|
||||
if (!disabled && onChange) {
|
||||
onChange(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper checked={checked} disabled={disabled} className={className}>
|
||||
<div className="checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
name={name}
|
||||
value={value}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
className="checkbox-input"
|
||||
data-testid={dataTestId}
|
||||
/>
|
||||
<IconCheckMark className="checkbox-checkmark" color={theme.examples.checkbox.color} size={14} />
|
||||
</div>
|
||||
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkbox;
|
||||
@@ -1,6 +1,12 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
&.read-only {
|
||||
div.CodeMirror .CodeMirror-cursor {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
div.CodeMirror {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
@@ -109,6 +115,17 @@ const StyledWrapper = styled.div`
|
||||
text-decoration:unset;
|
||||
}
|
||||
|
||||
.cm-search-line-highlight {
|
||||
background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent};
|
||||
}
|
||||
|
||||
.cm-search-match {
|
||||
background: rgba(255, 193, 7, 0.25);
|
||||
}
|
||||
|
||||
.cm-search-current {
|
||||
background: rgba(255, 193, 7, 0.4);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -14,6 +14,8 @@ import * as jsonlint from '@prantlf/jsonlint';
|
||||
import { JSHINT } from 'jshint';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { setupLinkAware } from 'utils/codemirror/linkAware';
|
||||
import CodeMirrorSearch from 'components/CodeMirrorSearch';
|
||||
|
||||
const CodeMirror = require('codemirror');
|
||||
window.jsonlint = jsonlint;
|
||||
@@ -37,6 +39,10 @@ export default class CodeEditor extends React.Component {
|
||||
expr: true,
|
||||
asi: true
|
||||
};
|
||||
|
||||
this.state = {
|
||||
searchBarVisible: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -45,12 +51,14 @@ export default class CodeEditor extends React.Component {
|
||||
const editor = (this.editor = CodeMirror(this._node, {
|
||||
value: this.props.value || '',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
lineWrapping: this.props.enableLineWrapping ?? true,
|
||||
tabSize: TAB_SIZE,
|
||||
mode: this.props.mode || 'application/ld+json',
|
||||
brunoVarInfo: {
|
||||
variables
|
||||
},
|
||||
brunoVarInfo: this.props.enableBrunoVarInfo !== false ? {
|
||||
variables,
|
||||
collection: this.props.collection,
|
||||
item: this.props.item
|
||||
} : false,
|
||||
keyMap: 'sublime',
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
@@ -83,24 +91,14 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
},
|
||||
'Cmd-F': (cm) => {
|
||||
if (this._isSearchOpen()) {
|
||||
// replace the older search component with the new one
|
||||
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
search && search.remove();
|
||||
if (!this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: true });
|
||||
}
|
||||
cm.execCommand('findPersistent');
|
||||
this._bindSearchHandler();
|
||||
this._appendSearchResultsCount();
|
||||
},
|
||||
'Ctrl-F': (cm) => {
|
||||
if (this._isSearchOpen()) {
|
||||
// replace the older search component with the new one
|
||||
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
search && search.remove();
|
||||
if (!this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: true });
|
||||
}
|
||||
cm.execCommand('findPersistent');
|
||||
this._bindSearchHandler();
|
||||
this._appendSearchResultsCount();
|
||||
},
|
||||
'Cmd-H': 'replace',
|
||||
'Ctrl-H': 'replace',
|
||||
@@ -129,6 +127,11 @@ export default class CodeEditor extends React.Component {
|
||||
} else {
|
||||
this.editor.toggleComment();
|
||||
}
|
||||
},
|
||||
'Esc': () => {
|
||||
if (this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: false });
|
||||
}
|
||||
}
|
||||
},
|
||||
foldOptions: {
|
||||
@@ -186,6 +189,8 @@ export default class CodeEditor extends React.Component {
|
||||
if (editor) {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
editor.on('change', this._onEdit);
|
||||
editor.on('scroll', this.onScroll);
|
||||
editor.scrollTo(null, this.props.initialScroll);
|
||||
this.addOverlay();
|
||||
|
||||
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
|
||||
@@ -200,6 +205,8 @@ export default class CodeEditor extends React.Component {
|
||||
editor,
|
||||
autoCompleteOptions
|
||||
);
|
||||
|
||||
setupLinkAware(editor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,24 +232,48 @@ export default class CodeEditor extends React.Component {
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.addOverlay();
|
||||
}
|
||||
|
||||
// Update collection and item when they change
|
||||
if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {
|
||||
if (!isEqual(this.props.collection, this.editor.options.brunoVarInfo.collection)) {
|
||||
this.editor.options.brunoVarInfo.collection = this.props.collection;
|
||||
}
|
||||
if (!isEqual(this.props.item, this.editor.options.brunoVarInfo.item)) {
|
||||
this.editor.options.brunoVarInfo.item = this.props.item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
|
||||
if (this.props.initialScroll !== prevProps.initialScroll) {
|
||||
this.editor.scrollTo(null, this.props.initialScroll);
|
||||
}
|
||||
|
||||
if (this.props.enableLineWrapping !== prevProps.enableLineWrapping) {
|
||||
this.editor.setOption('lineWrapping', this.props.enableLineWrapping);
|
||||
}
|
||||
|
||||
if (this.props.mode !== prevProps.mode) {
|
||||
this.editor.setOption('mode', this.props.mode);
|
||||
}
|
||||
|
||||
if (this.props.readOnly !== prevProps.readOnly && this.editor) {
|
||||
this.editor.setOption('readOnly', this.props.readOnly);
|
||||
}
|
||||
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor?._destroyLinkAware?.();
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor.off('scroll', this.onScroll);
|
||||
this.editor = null;
|
||||
}
|
||||
|
||||
this._unbindSearchHandler();
|
||||
if (this.brunoAutoCompleteCleanup) {
|
||||
this.brunoAutoCompleteCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -251,14 +282,22 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full w-full flex flex-col relative graphiql-container"
|
||||
className={`h-full w-full flex flex-col relative graphiql-container ${this.props.readOnly ? 'read-only' : ''}`}
|
||||
aria-label="Code Editor"
|
||||
font={this.props.font}
|
||||
fontSize={this.props.fontSize}
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<CodeMirrorSearch
|
||||
visible={this.state.searchBarVisible}
|
||||
editor={this.editor}
|
||||
onClose={() => this.setState({ searchBarVisible: false })}
|
||||
/>
|
||||
<div
|
||||
className={`editor-container${this.state.searchBarVisible ? ' search-bar-visible' : ''}`}
|
||||
ref={(node) => { this._node = node; }}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -267,10 +306,17 @@ export default class CodeEditor extends React.Component {
|
||||
let variables = getAllVariables(this.props.collection, this.props.item);
|
||||
this.variables = variables;
|
||||
|
||||
// Update brunoVarInfo with latest variables
|
||||
if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {
|
||||
this.editor.options.brunoVarInfo.variables = variables;
|
||||
}
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
onScroll = (event) => this.props.onScroll?.(event);
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
@@ -280,67 +326,4 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_isSearchOpen = () => {
|
||||
return document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
};
|
||||
|
||||
/**
|
||||
* Bind handler to search input to count number of search results
|
||||
*/
|
||||
_bindSearchHandler = () => {
|
||||
const searchInput = document.querySelector('.CodeMirror-search-field');
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', this._countSearchResults);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Unbind handler to search input to count number of search results
|
||||
*/
|
||||
_unbindSearchHandler = () => {
|
||||
const searchInput = document.querySelector('.CodeMirror-search-field');
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.removeEventListener('input', this._countSearchResults);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Append search results count to search dialog
|
||||
*/
|
||||
_appendSearchResultsCount = () => {
|
||||
const dialog = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
|
||||
if (dialog) {
|
||||
const searchResultsCount = document.createElement('span');
|
||||
searchResultsCount.id = this.searchResultsCountElementId;
|
||||
dialog.appendChild(searchResultsCount);
|
||||
|
||||
this._countSearchResults();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Count search results and update state
|
||||
*/
|
||||
_countSearchResults = () => {
|
||||
let count = 0;
|
||||
|
||||
const searchInput = document.querySelector('.CodeMirror-search-field');
|
||||
|
||||
if (searchInput && searchInput.value.length > 0) {
|
||||
// Escape special characters in search input to prevent RegExp crashes. Fixes #3051
|
||||
const text = new RegExp(escapeRegExp(searchInput.value), 'gi');
|
||||
const matches = this.editor.getValue().match(text);
|
||||
count = matches ? matches.length : 0;
|
||||
}
|
||||
|
||||
const searchResultsCountElement = document.querySelector(`#${this.searchResultsCountElementId}`);
|
||||
|
||||
if (searchResultsCountElement) {
|
||||
searchResultsCountElement.innerText = `${count} results`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.bruno-search-bar {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
padding: 0 2px;
|
||||
min-height: 36px;
|
||||
background: ${(props) => props.theme.sidebar.search.bg} !important;
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${(props) => props.theme.sidebar.search.bg} !important;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
width: auto;
|
||||
min-width: 180px;
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.bruno-search-bar input {
|
||||
min-width: 80px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 1px 2px;
|
||||
font-size: 13px;
|
||||
margin: 0 1px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.searchbar-icon-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0 1px;
|
||||
margin: 0 1px;
|
||||
cursor: pointer;
|
||||
color: #aaa;
|
||||
border-radius: 3px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.searchbar-result-count {
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: #aaa;
|
||||
margin: 0 8px 0 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bruno-search-bar.compact {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
color: ${(props) => props.theme.codemirror.text || props.theme.text};
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
border-radius: 4px;
|
||||
padding: 1px 3px;
|
||||
min-height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.bruno-search-bar input {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
padding: 1px 2px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.searchbar-icon-btn:focus {
|
||||
outline: 1px solid ${(props) => props.theme.codemirror.border};
|
||||
}
|
||||
|
||||
.bruno-search-bar, .bruno-search-bar input {
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
||||
}
|
||||
|
||||
.cm-search-line-highlight {
|
||||
background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent};
|
||||
}
|
||||
|
||||
.searchbar-icon-btn.active {
|
||||
color: #f39c12 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
201
packages/bruno-app/src/components/CodeMirrorSearch/index.js
Normal file
201
packages/bruno-app/src/components/CodeMirrorSearch/index.js
Normal file
@@ -0,0 +1,201 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { IconRegex, IconArrowUp, IconArrowDown, IconX, IconLetterCase, IconLetterW } from '@tabler/icons';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');
|
||||
}
|
||||
|
||||
const CodeMirrorSearch = ({ visible, editor, onClose }) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [regex, setRegex] = useState(false);
|
||||
const [caseSensitive, setCaseSensitive] = useState(false);
|
||||
const [wholeWord, setWholeWord] = useState(false);
|
||||
const [matchIndex, setMatchIndex] = useState(0);
|
||||
const [matchCount, setMatchCount] = useState(0);
|
||||
|
||||
const searchMarks = useRef([]);
|
||||
const searchLineHighlight = useRef(null);
|
||||
const searchMatches = useRef([]);
|
||||
|
||||
const debouncedSearchText = useDebounce(searchText, 150);
|
||||
|
||||
const memoizedMatches = useMemo(() => {
|
||||
if (!editor || !visible) return [];
|
||||
if (!debouncedSearchText) return [];
|
||||
|
||||
try {
|
||||
let query, options = {};
|
||||
if (regex) {
|
||||
try {
|
||||
query = new RegExp(debouncedSearchText, caseSensitive ? 'g' : 'gi');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
} else if (wholeWord) {
|
||||
const escaped = escapeRegExp(debouncedSearchText);
|
||||
query = new RegExp(`\\b${escaped}\\b`, caseSensitive ? 'g' : 'gi');
|
||||
} else {
|
||||
query = debouncedSearchText;
|
||||
options = { caseFold: !caseSensitive };
|
||||
}
|
||||
|
||||
const cursor = editor.getSearchCursor(query, { line: 0, ch: 0 }, options);
|
||||
const out = [];
|
||||
while (cursor.findNext()) {
|
||||
out.push({ from: cursor.from(), to: cursor.to() });
|
||||
}
|
||||
return out;
|
||||
} catch (e) {
|
||||
console.error('Search error:', e);
|
||||
return [];
|
||||
}
|
||||
}, [editor, visible, debouncedSearchText, regex, caseSensitive, wholeWord]);
|
||||
|
||||
const doSearch = useCallback((newIndex = 0) => {
|
||||
if (!editor) return;
|
||||
|
||||
// Clear previous marks
|
||||
searchMarks.current.forEach((mark) => mark.clear());
|
||||
searchMarks.current = [];
|
||||
// Clear previous line highlight
|
||||
if (searchLineHighlight.current !== null) {
|
||||
editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight');
|
||||
searchLineHighlight.current = null;
|
||||
}
|
||||
|
||||
if (!debouncedSearchText) {
|
||||
setMatchCount(0);
|
||||
setMatchIndex(0);
|
||||
searchMatches.current = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const matches = memoizedMatches;
|
||||
let matchIndex = matches.length ? Math.max(0, Math.min(newIndex, matches.length - 1)) : 0;
|
||||
matches.forEach((m, i) => {
|
||||
const mark = editor.markText(m.from, m.to, {
|
||||
className: i === matchIndex ? 'cm-search-current' : 'cm-search-match',
|
||||
clearOnEnter: true
|
||||
});
|
||||
searchMarks.current.push(mark);
|
||||
});
|
||||
|
||||
if (matches.length) {
|
||||
const currentLine = matches[matchIndex].from.line;
|
||||
editor.addLineClass(currentLine, 'wrap', 'cm-search-line-highlight');
|
||||
searchLineHighlight.current = currentLine;
|
||||
|
||||
editor.scrollIntoView(matches[matchIndex].from, 100);
|
||||
editor.setSelection(matches[matchIndex].from, matches[matchIndex].to);
|
||||
} else {
|
||||
searchLineHighlight.current = null;
|
||||
}
|
||||
|
||||
setMatchCount(matches.length);
|
||||
setMatchIndex(matchIndex);
|
||||
searchMatches.current = matches;
|
||||
} catch (e) {
|
||||
console.error('Search error:', e);
|
||||
setMatchCount(0);
|
||||
setMatchIndex(0);
|
||||
searchMatches.current = [];
|
||||
}
|
||||
}, [debouncedSearchText, regex, caseSensitive, wholeWord, editor, memoizedMatches]);
|
||||
|
||||
useEffect(() => {
|
||||
doSearch(0, debouncedSearchText);
|
||||
}, [debouncedSearchText, doSearch]);
|
||||
|
||||
const handleSearchBarClose = useCallback(() => {
|
||||
searchMarks.current.forEach((mark) => mark.clear());
|
||||
searchMarks.current = [];
|
||||
if (searchLineHighlight.current !== null && editor) {
|
||||
editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight');
|
||||
searchLineHighlight.current = null;
|
||||
}
|
||||
searchMatches.current = [];
|
||||
if (onClose) onClose();
|
||||
// Focus the editor after closing the search bar
|
||||
if (editor) {
|
||||
setTimeout(() => editor.focus(), 0);
|
||||
}
|
||||
}, [editor, onClose]);
|
||||
|
||||
const handleSearchTextChange = (text) => {
|
||||
setSearchText(text);
|
||||
setMatchIndex(0);
|
||||
};
|
||||
|
||||
const handleToggleRegex = () => {
|
||||
setRegex((prev) => !prev);
|
||||
setMatchIndex(0);
|
||||
doSearch(0);
|
||||
};
|
||||
|
||||
const handleToggleCase = () => {
|
||||
setCaseSensitive((prev) => !prev);
|
||||
setMatchIndex(0);
|
||||
doSearch(0);
|
||||
};
|
||||
|
||||
const handleToggleWholeWord = () => {
|
||||
setWholeWord((prev) => !prev);
|
||||
setMatchIndex(0);
|
||||
doSearch(0);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (!searchMatches.current || !searchMatches.current.length) return;
|
||||
let next = (matchIndex + 1) % searchMatches.current.length;
|
||||
setMatchIndex(next);
|
||||
doSearch(next);
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (!searchMatches.current || !searchMatches.current.length) return;
|
||||
let prev = (matchIndex - 1 + searchMatches.current.length) % searchMatches.current.length;
|
||||
setMatchIndex(prev);
|
||||
doSearch(prev);
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="bruno-search-bar compact">
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
value={searchText}
|
||||
onChange={(e) => handleSearchTextChange(e.target.value)}
|
||||
placeholder="Search..."
|
||||
spellCheck={false}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) handleNext();
|
||||
if (e.key === 'Enter' && e.shiftKey) handlePrev();
|
||||
if (e.key === 'Escape') handleSearchBarClose();
|
||||
}}
|
||||
/>
|
||||
<span className="searchbar-result-count">{matchCount > 0 ? `${matchIndex + 1} / ${matchCount}` : '0 results'}</span>
|
||||
<ToolHint text="Regex search" toolhintId="searchbar-regex-toolhint" place="top">
|
||||
<button className={`searchbar-icon-btn ${regex ? 'active' : ''}`} onClick={handleToggleRegex}><IconRegex size={16} /></button>
|
||||
</ToolHint>
|
||||
<ToolHint text="Case sensitive" toolhintId="searchbar-case-toolhint" place="top">
|
||||
<button className={`searchbar-icon-btn ${caseSensitive ? 'active' : ''}`} onClick={handleToggleCase}><IconLetterCase size={14} /></button>
|
||||
</ToolHint>
|
||||
<ToolHint text="Whole word" toolhintId="searchbar-wholeword-toolhint" place="top">
|
||||
<button className={`searchbar-icon-btn ${wholeWord ? 'active' : ''}`} onClick={handleToggleWholeWord}><IconLetterW size={14} /></button>
|
||||
</ToolHint>
|
||||
<button className="searchbar-icon-btn" title="Previous" onClick={handlePrev}><IconArrowUp size={14} /></button>
|
||||
<button className="searchbar-icon-btn" title="Next" onClick={handleNext}><IconArrowDown size={14} /></button>
|
||||
<button className="searchbar-icon-btn" title="Close" onClick={handleSearchBarClose}><IconX size={14} /></button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeMirrorSearch;
|
||||
@@ -6,7 +6,7 @@ import Dropdown from 'components/Dropdown';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAPIKeyPlacement } from 'utils/collections';
|
||||
|
||||
@@ -16,9 +16,9 @@ const ApiKeyAuth = ({ collection }) => {
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const apikeyAuth = get(collection, 'root.request.auth.apikey', {});
|
||||
const apikeyAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.apikey', {}) : get(collection, 'root.request.auth.apikey', {});
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
|
||||
@@ -11,7 +11,7 @@ const AuthMode = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const authMode = get(collection, 'root.request.auth.mode');
|
||||
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const AwsV4Auth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = get(collection, 'root.request.auth.awsv4', {});
|
||||
const awsv4Auth = collection.draft?.root ? get(collection, 'draft.root.request.auth.awsv4', {}) : get(collection, 'root.request.auth.awsv4', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleAccessKeyIdChange = (accessKeyId) => {
|
||||
dispatch(
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BasicAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const basicAuth = get(collection, 'root.request.auth.basic', {});
|
||||
const basicAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.basic', {}) : get(collection, 'root.request.auth.basic', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(basicAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BearerAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const bearerToken = get(collection, 'root.request.auth.bearer.token', '');
|
||||
const bearerToken = collection.draft?.root ? get(collection, 'draft.root.request.auth.bearer.token', '') : get(collection, 'root.request.auth.bearer.token', '');
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(bearerToken);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleTokenChange = (token) => {
|
||||
dispatch(
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DigestAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const digestAuth = get(collection, 'root.request.auth.digest', {});
|
||||
const digestAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.digest', {}) : get(collection, 'root.request.auth.digest', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(digestAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ const NTLMAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const ntlmAuth = get(collection, 'root.request.auth.ntlm', {});
|
||||
const ntlmAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.ntlm', {}) : get(collection, 'root.request.auth.ntlm', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -14,10 +14,10 @@ const GrantTypeComponentMap = ({collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const save = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
dispatch(saveCollectionSettings(collection.uid));
|
||||
};
|
||||
|
||||
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
|
||||
let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', {});
|
||||
|
||||
switch (grantType) {
|
||||
@@ -40,7 +40,7 @@ const GrantTypeComponentMap = ({collection }) => {
|
||||
};
|
||||
|
||||
const OAuth2 = ({ collection }) => {
|
||||
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
|
||||
let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {});
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WsseAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const wsseAuth = get(collection, 'root.request.auth.wsse', {});
|
||||
const wsseAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.wsse', {}) : get(collection, 'root.request.auth.wsse', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(wsseAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleUserChange = (username) => {
|
||||
dispatch(
|
||||
|
||||
@@ -8,17 +8,17 @@ import BasicAuth from './BasicAuth';
|
||||
import DigestAuth from './DigestAuth';
|
||||
import WsseAuth from './WsseAuth';
|
||||
import ApiKeyAuth from './ApiKeyAuth/';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import OAuth2 from './OAuth2';
|
||||
import NTLMAuth from './NTLMAuth';
|
||||
|
||||
|
||||
const Auth = ({ collection }) => {
|
||||
const authMode = get(collection, 'root.request.auth.mode');
|
||||
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import React from 'react';
|
||||
import { IconCertificate, IconTrash, IconWorld } from '@tabler/icons';
|
||||
import { useFormik } from 'formik';
|
||||
import { uuid } from 'utils/common';
|
||||
import * as Yup from 'yup';
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons';
|
||||
import { useState } from 'react';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useRef } from 'react';
|
||||
import path from 'utils/common/path';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning/index';
|
||||
import SingleLineEditor from 'components/SingleLineEditor/index';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField/index';
|
||||
import { useTheme } from 'styled-components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateCollectionClientCertificates } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
const ClientCertSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Get client certs from draft if exists, otherwise from brunoConfig
|
||||
const clientCertConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.clientCertificates.certs', [])
|
||||
: get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const certFilePathInputRef = useRef();
|
||||
const keyFilePathInputRef = useRef();
|
||||
const pfxFilePathInputRef = useRef();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
@@ -62,28 +73,47 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
passphrase: values.passphrase
|
||||
};
|
||||
}
|
||||
onUpdate(relevantValues);
|
||||
|
||||
// Add the new cert to the existing certs in draft
|
||||
const updatedCerts = [...clientCertConfig, relevantValues];
|
||||
const clientCertificates = {
|
||||
enabled: true,
|
||||
certs: updatedCerts
|
||||
};
|
||||
|
||||
dispatch(updateCollectionClientCertificates({
|
||||
collectionUid: collection.uid,
|
||||
clientCertificates
|
||||
}));
|
||||
|
||||
formik.resetForm();
|
||||
resetFileInputFields();
|
||||
}
|
||||
});
|
||||
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(formik.values.passphrase);
|
||||
|
||||
const getFile = (e) => {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]);
|
||||
if (filePath) {
|
||||
let relativePath = path.relative(root, filePath);
|
||||
let relativePath = path.relative(collection.pathname, filePath);
|
||||
formik.setFieldValue(e.name, relativePath);
|
||||
}
|
||||
};
|
||||
|
||||
const resetFileInputFields = () => {
|
||||
certFilePathInputRef.current.value = '';
|
||||
keyFilePathInputRef.current.value = '';
|
||||
pfxFilePathInputRef.current.value = '';
|
||||
if (certFilePathInputRef.current) {
|
||||
certFilePathInputRef.current.value = '';
|
||||
}
|
||||
if (keyFilePathInputRef.current) {
|
||||
keyFilePathInputRef.current.value = '';
|
||||
}
|
||||
if (pfxFilePathInputRef.current) {
|
||||
pfxFilePathInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||
|
||||
const handleTypeChange = (e) => {
|
||||
formik.setFieldValue('type', e.target.value);
|
||||
if (e.target.value === 'cert') {
|
||||
@@ -97,6 +127,21 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (indexToRemove) => {
|
||||
const updatedCerts = clientCertConfig.filter((cert, index) => index !== indexToRemove);
|
||||
const clientCertificates = {
|
||||
enabled: true,
|
||||
certs: updatedCerts
|
||||
};
|
||||
|
||||
dispatch(updateCollectionClientCertificates({
|
||||
collectionUid: collection.uid,
|
||||
clientCertificates
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full h-full">
|
||||
<div className="text-xs mb-4 text-muted">Add client certificates to be used for specific domains.</div>
|
||||
@@ -116,9 +161,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
|
||||
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
|
||||
</div>
|
||||
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
|
||||
<button onClick={() => handleRemove(index)} className="remove-certificate ml-2">
|
||||
<IconTrash size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
@@ -314,30 +359,27 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
Passphrase
|
||||
</label>
|
||||
<div className="textbox flex flex-row items-center w-[300px] h-[1.70rem] relative">
|
||||
<input
|
||||
id="passphrase"
|
||||
type={passwordVisible ? 'text' : 'password'}
|
||||
name="passphrase"
|
||||
className="outline-none w-64 bg-transparent"
|
||||
onChange={formik.handleChange}
|
||||
<SingleLineEditor
|
||||
value={formik.values.passphrase || ''}
|
||||
theme={storedTheme}
|
||||
onChange={(val) => formik.setFieldValue('passphrase', val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm absolute right-0 l"
|
||||
onClick={() => setPasswordVisible(!passwordVisible)}
|
||||
>
|
||||
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
|
||||
</button>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
{formik.touched.passphrase && formik.errors.passphrase ? (
|
||||
<div className="ml-1 text-red-500">{formik.errors.passphrase}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<div className="mt-6 flex flex-row gap-2 items-center">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Add
|
||||
</button>
|
||||
<div className="h-4 border-l border-gray-600"></div>
|
||||
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'github-markdown-css/github-markdown.css';
|
||||
import get from 'lodash/get';
|
||||
import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections';
|
||||
import { updateCollectionDocs, deleteCollectionDraft } from 'providers/ReduxStore/slices/collections';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Markdown from 'components/MarkDown';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -14,7 +14,7 @@ const Docs = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme } = useTheme();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const docs = get(collection, 'root.docs', '');
|
||||
const docs = collection.draft?.root ? get(collection, 'draft.root.docs', '') : get(collection, 'root.docs', '');
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const toggleViewMode = () => {
|
||||
@@ -31,17 +31,17 @@ const Docs = ({ collection }) => {
|
||||
};
|
||||
|
||||
const handleDiscardChanges = () => {
|
||||
dispatch(
|
||||
dispatch((
|
||||
updateCollectionDocs({
|
||||
collectionUid: collection.uid,
|
||||
docs: docs
|
||||
})
|
||||
}))
|
||||
);
|
||||
toggleViewMode();
|
||||
}
|
||||
|
||||
const onSave = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
dispatch(saveCollectionSettings(collection.uid));
|
||||
toggleViewMode();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash, IconFile, IconFileImport, IconAlertCircle } from '@tabler/icons';
|
||||
import { getRelativePath, getBasename, getDirPath } from 'utils/common/path';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { existsSync, resolvePath } from '../../../utils/filesystem';
|
||||
|
||||
const GrpcSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
brunoConfig: { grpc: grpcConfig = {} }
|
||||
} = collection;
|
||||
|
||||
const fileInputRef = useRef(null);
|
||||
const [protoFileValidity, setProtoFileValidity] = useState({});
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
protoFiles: grpcConfig.protoFiles || []
|
||||
},
|
||||
onSubmit: (newGrpcConfig) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.grpc = newGrpcConfig;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
|
||||
toast.success('gRPC settings updated');
|
||||
}
|
||||
});
|
||||
|
||||
// Get file path using the ipcRenderer
|
||||
const getProtoFile = (event) => {
|
||||
const files = event?.files;
|
||||
if (files && files.length > 0) {
|
||||
const newProtoFiles = [...formik.values.protoFiles];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(files[i]);
|
||||
if (filePath) {
|
||||
const relativePath = getRelativePath(filePath, collection.pathname);
|
||||
const protoFileObj = {
|
||||
path: relativePath,
|
||||
type: 'file'
|
||||
};
|
||||
|
||||
// Check if this path already exists
|
||||
const exists = newProtoFiles.some(pf => pf.path === protoFileObj.path);
|
||||
if (!exists) {
|
||||
newProtoFiles.push(protoFileObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formik.setFieldValue('protoFiles', newProtoFiles);
|
||||
// Reset the file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for removing a proto file
|
||||
const handleRemoveProtoFile = (index) => {
|
||||
const updatedProtoFiles = [...formik.values.protoFiles];
|
||||
updatedProtoFiles.splice(index, 1);
|
||||
formik.setFieldValue('protoFiles', updatedProtoFiles);
|
||||
};
|
||||
|
||||
// Handle the browse button click
|
||||
const handleBrowseClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
// Check if a proto file path is valid
|
||||
const isProtoFileValid = async (protoFile) => {
|
||||
try {
|
||||
const absolutePath = await resolvePath(protoFile.path, collection.pathname);
|
||||
return await existsSync(absolutePath);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Validate all proto files and update state
|
||||
useEffect(() => {
|
||||
const validateProtoFiles = async () => {
|
||||
const validityMap = {};
|
||||
for (const file of formik.values.protoFiles) {
|
||||
validityMap[file.path] = await isProtoFileValid(file);
|
||||
}
|
||||
setProtoFileValidity(validityMap);
|
||||
};
|
||||
|
||||
validateProtoFiles();
|
||||
}, [formik.values.protoFiles, collection.pathname]);
|
||||
|
||||
// Handle replacing an invalid proto file
|
||||
const handleReplaceProtoFile = (index) => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
// Store the index to replace after file selection
|
||||
fileInputRef.current.dataset.replaceIndex = index;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle file input change
|
||||
const handleFileInputChange = (e) => {
|
||||
const replaceIndex = e.target.dataset.replaceIndex;
|
||||
if (replaceIndex !== undefined) {
|
||||
// Handle replacement
|
||||
const files = e.target.files;
|
||||
if (files && files.length > 0) {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(files[0]);
|
||||
if (filePath) {
|
||||
const relativePath = getRelativePath(filePath, collection.pathname);
|
||||
const updatedProtoFiles = [...formik.values.protoFiles];
|
||||
updatedProtoFiles[replaceIndex] = {
|
||||
path: relativePath,
|
||||
type: 'file'
|
||||
};
|
||||
formik.setFieldValue('protoFiles', updatedProtoFiles);
|
||||
}
|
||||
}
|
||||
delete e.target.dataset.replaceIndex;
|
||||
} else {
|
||||
getProtoFile(e.target);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label className="font-semibold text-sm mb-3 flex items-center" htmlFor="protoFiles">
|
||||
Add Proto Files
|
||||
<span id="proto-files-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="text-gray-500 cursor-pointer" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="proto-files-tooltip"
|
||||
className="tooltip-mod font-normal"
|
||||
html="Keep your proto files within the collection folder or the corresponding git repository to ensure paths remain valid when sharing the collection."
|
||||
/>
|
||||
</label>
|
||||
<div className="flex flex-col">
|
||||
{/* Hidden file input for file selection */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
accept=".proto"
|
||||
multiple
|
||||
onChange={handleFileInputChange}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* File selection options */}
|
||||
<div className="flex flex-col space-y-3">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary flex items-center"
|
||||
onClick={handleBrowseClick}
|
||||
>
|
||||
<IconFileImport size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Browse for proto files
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-t border-neutral-600 my-2"></div>
|
||||
|
||||
{/* List of added proto files */}
|
||||
<div>
|
||||
<div className="text-sm font-semibold mb-2 flex items-center">
|
||||
<IconFile size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Added Proto Files ({formik.values.protoFiles.length})
|
||||
</div>
|
||||
|
||||
{formik.values.protoFiles.length === 0 ? (
|
||||
<div className="text-neutral-500 text-sm italic">No proto files added yet</div>
|
||||
) : (
|
||||
<>
|
||||
{formik.values.protoFiles.some(file => !protoFileValidity[file.path]) && (
|
||||
<div className="text-xs text-red-500 mb-2 flex items-center bg-red-50 dark:bg-red-900/20 p-2 rounded">
|
||||
<IconAlertCircle size={14} className="mr-1" />
|
||||
Some proto files cannot be found at their specified paths. Use the "Replace" option to update their locations.
|
||||
</div>
|
||||
)}
|
||||
<ul className="mt-4">
|
||||
{formik.values.protoFiles.map((file, index) => {
|
||||
const isValid = protoFileValidity[file.path];
|
||||
return (
|
||||
<li key={index} className="flex items-center available-certificates p-2 rounded-lg mb-2">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<div className="flex w-full items-center">
|
||||
<IconFile className="mr-2" size={18} strokeWidth={1.5} />
|
||||
<div
|
||||
className="overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px] text-sm"
|
||||
title={file.path}
|
||||
>
|
||||
{getBasename(file.path)}
|
||||
<span className="text-xs text-neutral-500 ml-2">
|
||||
{getDirPath(file.path)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-end">
|
||||
{!isValid && (
|
||||
<div className="flex items-center mr-2">
|
||||
<IconAlertCircle
|
||||
size={16}
|
||||
className="text-red-500"
|
||||
title="Proto file not found. Click to replace."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-red-500 ml-1 hover:underline"
|
||||
onClick={() => handleReplaceProtoFile(index)}
|
||||
>
|
||||
Replace
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="remove-certificate ml-2"
|
||||
onClick={() => handleRemoveProtoFile(index)}
|
||||
title="Remove file"
|
||||
>
|
||||
<IconTrash size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcSettings;
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
deleteCollectionHeader,
|
||||
setCollectionHeaders
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
@@ -21,7 +21,7 @@ const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
const Headers = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = get(collection, 'root.request.headers', []);
|
||||
const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
|
||||
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
|
||||
|
||||
const toggleBulkEditMode = () => {
|
||||
@@ -40,12 +40,13 @@ const Headers = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
const handleHeaderValueChange = (e, _header, type) => {
|
||||
const header = cloneDeep(_header);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
header.name = e.target.value;
|
||||
// Strip newlines from header keys
|
||||
header.name = e.target.value.replace(/[\r\n]/g, '');
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
|
||||
@@ -1,44 +1,46 @@
|
||||
import React from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
|
||||
import { updateCollectionPresets } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { get } from 'lodash';
|
||||
import DeprecationWarning from 'components/DeprecationWarning';
|
||||
|
||||
const PresetsSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isGrpcEnabled = useBetaFeature(BETA_FEATURES.GRPC);
|
||||
const {
|
||||
brunoConfig: { presets: presets = {} }
|
||||
} = collection;
|
||||
const initialPresets = { requestType: 'http', requestUrl: '' };
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
requestType: presets.requestType === 'grpc' && !isGrpcEnabled ? 'http' : presets.requestType || 'http',
|
||||
requestUrl: presets.requestUrl || ''
|
||||
},
|
||||
onSubmit: (newPresets) => {
|
||||
// If gRPC is disabled but the preset is set to grpc, change it to http
|
||||
if (!isGrpcEnabled && newPresets.requestType === 'grpc') {
|
||||
newPresets.requestType = 'http';
|
||||
}
|
||||
// Get presets from draft.brunoConfig if it exists, otherwise from brunoConfig
|
||||
const currentPresets = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.presets', initialPresets)
|
||||
: get(collection, 'brunoConfig.presets', initialPresets);
|
||||
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.presets = newPresets;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
|
||||
toast.success('Collection presets updated');
|
||||
}
|
||||
});
|
||||
// Helper to update presets config
|
||||
const updatePresets = (updates) => {
|
||||
const updatedPresets = { ...currentPresets, ...updates };
|
||||
dispatch(updateCollectionPresets({
|
||||
collectionUid: collection.uid,
|
||||
presets: updatedPresets
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleRequestTypeChange = (e) => {
|
||||
updatePresets({ requestType: e.target.value });
|
||||
};
|
||||
|
||||
const handleRequestUrlChange = (e) => {
|
||||
updatePresets({ requestUrl: e.target.value });
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
<DeprecationWarning featureName="Presets" learnMoreUrl="https://github.com/usebruno/bruno/discussions/6234" />
|
||||
<div className="text-xs mb-4 mt-4 text-muted">
|
||||
These presets will be used as the default values for new requests in this collection.
|
||||
</div>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="bruno-form">
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
Request Type
|
||||
@@ -49,9 +51,9 @@ const PresetsSettings = ({ collection }) => {
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
onChange={handleRequestTypeChange}
|
||||
value="http"
|
||||
checked={formik.values.requestType === 'http'}
|
||||
checked={(currentPresets.requestType || 'http') === 'http'}
|
||||
/>
|
||||
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
|
||||
HTTP
|
||||
@@ -62,30 +64,26 @@ const PresetsSettings = ({ collection }) => {
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
onChange={handleRequestTypeChange}
|
||||
value="graphql"
|
||||
checked={formik.values.requestType === 'graphql'}
|
||||
checked={(currentPresets.requestType || 'http') === 'graphql'}
|
||||
/>
|
||||
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
|
||||
GraphQL
|
||||
</label>
|
||||
|
||||
{isGrpcEnabled && (
|
||||
<>
|
||||
<input
|
||||
id="grpc"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value="grpc"
|
||||
checked={formik.values.requestType === 'grpc'}
|
||||
/>
|
||||
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
|
||||
gRPC
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
<input
|
||||
id="grpc"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={handleRequestTypeChange}
|
||||
value="grpc"
|
||||
checked={(currentPresets.requestType || 'http') === 'grpc'}
|
||||
/>
|
||||
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
|
||||
gRPC
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
@@ -104,8 +102,8 @@ const PresetsSettings = ({ collection }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.requestUrl || ''}
|
||||
onChange={handleRequestUrlChange}
|
||||
value={currentPresets.requestUrl || ''}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
@@ -113,11 +111,11 @@ const PresetsSettings = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,4 +10,4 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,347 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import {
|
||||
IconTrash,
|
||||
IconFile,
|
||||
IconFileImport,
|
||||
IconAlertCircle,
|
||||
IconFolder
|
||||
} from '@tabler/icons';
|
||||
import { getBasename } from 'utils/common/path';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import useProtoFileManagement from '../../../hooks/useProtoFileManagement';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
const ProtobufSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
protoFiles,
|
||||
importPaths,
|
||||
addProtoFileToCollection,
|
||||
addImportPathToCollection,
|
||||
toggleImportPath,
|
||||
browseForProtoFile,
|
||||
browseForImportDirectory,
|
||||
removeProtoFileFromCollection,
|
||||
removeImportPathFromCollection,
|
||||
replaceImportPathInCollection,
|
||||
replaceProtoFileInCollection
|
||||
} = useProtoFileManagement(collection);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
// Get file path using the ipcRenderer
|
||||
const getProtoFile = async (event) => {
|
||||
const files = event?.files;
|
||||
if (files && files.length > 0) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(files[i]);
|
||||
if (filePath) {
|
||||
await addProtoFileToCollection(filePath);
|
||||
}
|
||||
}
|
||||
// Reset the file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveProtoFile = async (index) => {
|
||||
await removeProtoFileFromCollection(index);
|
||||
};
|
||||
|
||||
const handleBrowseClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleReplaceProtoFile = async (index) => {
|
||||
const result = await browseForProtoFile();
|
||||
if (result.success) {
|
||||
await replaceProtoFileInCollection(index, result.filePath);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReplaceImportPath = async (index) => {
|
||||
const result = await browseForImportDirectory();
|
||||
if (result.success) {
|
||||
await replaceImportPathInCollection(index, result.directoryPath);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileInputChange = (e) => {
|
||||
getProtoFile(e.target);
|
||||
};
|
||||
|
||||
const getImportPath = async () => {
|
||||
const result = await browseForImportDirectory();
|
||||
if (result.success) {
|
||||
await addImportPathToCollection(result.directoryPath);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveImportPath = async (index) => {
|
||||
await removeImportPathFromCollection(index);
|
||||
};
|
||||
|
||||
const handleToggleImportPath = async (index) => {
|
||||
await toggleImportPath(index);
|
||||
};
|
||||
|
||||
const handleBrowseImportPathClick = () => {
|
||||
getImportPath();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
{/* Hidden file input for file selection */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
accept=".proto"
|
||||
multiple
|
||||
onChange={handleFileInputChange}
|
||||
/>
|
||||
|
||||
{/* Proto Files Section */}
|
||||
<div className="mb-6" data-testid="protobuf-proto-files-section">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center">
|
||||
<label className="font-semibold text-sm flex items-center" htmlFor="protoFiles">
|
||||
Proto Files (
|
||||
{protoFiles.length}
|
||||
)
|
||||
<span id="proto-files-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="text-gray-500 cursor-pointer" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="proto-files-tooltip"
|
||||
className="tooltip-mod font-normal"
|
||||
html="Keep your proto files within the collection folder or the corresponding git repository to ensure paths remain valid when sharing the collection."
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{protoFiles.some((file) => !file.exists) && (
|
||||
<div className="text-xs text-red-600 dark:text-red-400 mb-2 flex items-center p-2 rounded" data-testid="protobuf-invalid-files-message">
|
||||
<IconAlertCircle size={14} className="mr-1" />
|
||||
Some proto files cannot be found. Use the replace option to update their locations.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<table className="w-full border-collapse" data-testid="protobuf-proto-files-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
File
|
||||
</th>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Path
|
||||
</th>
|
||||
<th className="text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{protoFiles.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="3" className="border border-gray-200 dark:border-gray-700 px-3 py-8 text-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<IconFile size={24} className="text-gray-400 mb-2" />
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">No proto files added</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
protoFiles.map((file, index) => {
|
||||
const isValid = file.exists;
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="flex items-center">
|
||||
<IconFile size={16} className="text-gray-500 dark:text-gray-400 mr-2" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100" data-testid="protobuf-proto-file-name">
|
||||
{getBasename(collection.pathname, file.path)}
|
||||
</span>
|
||||
{!isValid && <IconAlertCircle size={12} className="text-red-600 dark:text-red-400 ml-2" />}
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 font-mono">
|
||||
{file.path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{!isValid && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReplaceProtoFile(index)}
|
||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 p-1 rounded"
|
||||
title="Replace file"
|
||||
>
|
||||
<IconFileImport size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveProtoFile(index)}
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 p-1 rounded"
|
||||
title="Remove file"
|
||||
data-testid="protobuf-remove-file-button"
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleBrowseClick} data-testid="protobuf-add-file-button">
|
||||
+ Add Proto File
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Import Paths Section */}
|
||||
<div className="mb-6" data-testid="protobuf-import-paths-section">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center">
|
||||
<label className="font-semibold text-sm flex items-center" htmlFor="importPaths">
|
||||
Import Paths (
|
||||
{importPaths.length}
|
||||
)
|
||||
<span id="import-paths-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="text-gray-500 cursor-pointer" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="import-paths-tooltip"
|
||||
className="tooltip-mod font-normal"
|
||||
html="Add directories that contain proto files to be imported. These paths help resolve import statements in your proto files."
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{importPaths.some((path) => !path.exists) && (
|
||||
<div className="text-xs text-red-600 dark:text-red-400 mb-2 flex items-center p-2 rounded" data-testid="protobuf-invalid-import-paths-message">
|
||||
<IconAlertCircle size={14} className="mr-1" />
|
||||
Some import paths cannot be found at their specified locations.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<table className="w-full border-collapse" data-testid="protobuf-import-paths-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
</th>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Directory
|
||||
</th>
|
||||
<th className="text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Path
|
||||
</th>
|
||||
<th className="text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{importPaths.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="4" className="border border-gray-200 dark:border-gray-700 px-3 py-8 text-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<IconFolder size={24} className="text-gray-400 mb-2" />
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">No import paths added</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
importPaths.map((importPath, index) => {
|
||||
const isValid = importPath.exists;
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={importPath.enabled}
|
||||
onChange={() => handleToggleImportPath(index)}
|
||||
className="h-4 w-4 text-gray-600 focus:ring-gray-500 border-gray-300 dark:border-gray-600 rounded"
|
||||
title={importPath.enabled ? 'Disable this import path' : 'Enable this import path'}
|
||||
data-testid="protobuf-import-path-checkbox"
|
||||
/>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="flex items-center">
|
||||
<IconFolder size={16} className="text-gray-500 dark:text-gray-400 mr-2" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{getBasename(collection.pathname, importPath.path)}
|
||||
</span>
|
||||
{!isValid && <IconAlertCircle size={12} className="text-red-600 dark:text-red-400 ml-2" />}
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400 font-mono">
|
||||
{importPath.path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="border border-gray-200 dark:border-gray-700 px-3 py-2 text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{!isValid && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReplaceImportPath(index)}
|
||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 p-1 rounded"
|
||||
title="Replace directory"
|
||||
>
|
||||
<IconFileImport size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveImportPath(index)}
|
||||
className="text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-300 p-1 rounded"
|
||||
title="Remove import path"
|
||||
data-testid="protobuf-remove-import-path-button"
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleBrowseImportPathClick} data-testid="protobuf-add-import-path-button">
|
||||
+ Add Import Path
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="button" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProtobufSettings;
|
||||
@@ -1,106 +1,155 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import React from 'react';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons';
|
||||
import { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateCollectionProxy } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { get } from 'lodash';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
const proxySchema = Yup.object({
|
||||
enabled: Yup.string().oneOf(['global', 'true', 'false']),
|
||||
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
|
||||
hostname: Yup.string()
|
||||
.when('enabled', {
|
||||
is: 'true',
|
||||
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
|
||||
otherwise: (hostname) => hostname.nullable()
|
||||
})
|
||||
.max(1024),
|
||||
port: Yup.number()
|
||||
.min(1)
|
||||
.max(65535)
|
||||
.typeError('Specify port between 1 and 65535')
|
||||
.nullable()
|
||||
.transform((_, val) => (val ? Number(val) : null)),
|
||||
auth: Yup.object()
|
||||
.when('enabled', {
|
||||
is: 'true',
|
||||
then: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
username: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (username) => username.required('Specify username for proxy authentication.')
|
||||
})
|
||||
.max(1024),
|
||||
password: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (password) => password.required('Specify password for proxy authentication.')
|
||||
})
|
||||
.max(1024)
|
||||
})
|
||||
})
|
||||
.optional(),
|
||||
bypassProxy: Yup.string().optional().max(1024)
|
||||
});
|
||||
const ProxySettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const initialProxyConfig = { enabled: 'global', protocol: 'http', hostname: '', port: '', auth: { enabled: false, username: '', password: '' }, bypassProxy: '' };
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
enabled: proxyConfig.enabled || 'global',
|
||||
protocol: proxyConfig.protocol || 'http',
|
||||
hostname: proxyConfig.hostname || '',
|
||||
port: proxyConfig.port || '',
|
||||
auth: {
|
||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||
},
|
||||
bypassProxy: proxyConfig.bypassProxy || ''
|
||||
},
|
||||
validationSchema: proxySchema,
|
||||
onSubmit: (values) => {
|
||||
proxySchema
|
||||
.validate(values, { abortEarly: true })
|
||||
.then((validatedProxy) => {
|
||||
// serialize 'enabled' to boolean
|
||||
if (validatedProxy.enabled === 'true') {
|
||||
validatedProxy.enabled = true;
|
||||
} else if (validatedProxy.enabled === 'false') {
|
||||
validatedProxy.enabled = false;
|
||||
}
|
||||
// Get proxy from draft.brunoConfig if it exists, otherwise from brunoConfig
|
||||
const currentProxyConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.proxy', initialProxyConfig)
|
||||
: get(collection, 'brunoConfig.proxy', initialProxyConfig);
|
||||
|
||||
onUpdate(validatedProxy);
|
||||
})
|
||||
.catch((error) => {
|
||||
let errMsg = error.message || 'Preferences validation error';
|
||||
toast.error(errMsg);
|
||||
});
|
||||
}
|
||||
});
|
||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
formik.setValues({
|
||||
enabled: proxyConfig.enabled === true ? 'true' : proxyConfig.enabled === false ? 'false' : 'global',
|
||||
protocol: proxyConfig.protocol || 'http',
|
||||
hostname: proxyConfig.hostname || '',
|
||||
port: proxyConfig.port || '',
|
||||
const validateHostnameOnChange = (hostname) => {
|
||||
if (hostname && hostname.length > 1024) {
|
||||
toast.error('Hostname must be less than 1024 characters');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validatePortOnChange = (port) => {
|
||||
if (!port || port === '') {
|
||||
return true; // Allow empty port during typing
|
||||
}
|
||||
const portNum = Number(port);
|
||||
if (isNaN(portNum)) {
|
||||
toast.error('Port must be a valid number');
|
||||
return false;
|
||||
}
|
||||
if (portNum < 1 || portNum > 65535) {
|
||||
toast.error('Port must be between 1 and 65535');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateAuthUsernameOnChange = (username) => {
|
||||
if (username && username.length > 1024) {
|
||||
toast.error('Username must be less than 1024 characters');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateAuthPasswordOnChange = (password) => {
|
||||
if (password && password.length > 1024) {
|
||||
toast.error('Password must be less than 1024 characters');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateBypassProxyOnChange = (bypassProxy) => {
|
||||
if (bypassProxy && bypassProxy.length > 1024) {
|
||||
toast.error('Bypass proxy must be less than 1024 characters');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Helper to update proxy config
|
||||
const updateProxy = (updates) => {
|
||||
const updatedProxy = { ...currentProxyConfig, ...updates };
|
||||
dispatch(updateCollectionProxy({
|
||||
collectionUid: collection.uid,
|
||||
proxy: updatedProxy
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleEnabledChange = (e) => {
|
||||
const value = e.target.value;
|
||||
// Convert string to boolean or keep as 'global'
|
||||
const enabled = value === 'true' ? true : value === 'false' ? false : 'global';
|
||||
updateProxy({ enabled });
|
||||
};
|
||||
|
||||
const handleProtocolChange = (e) => {
|
||||
updateProxy({ protocol: e.target.value });
|
||||
};
|
||||
|
||||
const handleHostnameChange = (e) => {
|
||||
const hostname = e.target.value;
|
||||
if (validateHostnameOnChange(hostname)) {
|
||||
updateProxy({ hostname });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePortChange = (e) => {
|
||||
const port = e.target.value ? Number(e.target.value) : '';
|
||||
if (validatePortOnChange(port)) {
|
||||
updateProxy({ port });
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthEnabledChange = (e) => {
|
||||
updateProxy({
|
||||
auth: {
|
||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||
},
|
||||
bypassProxy: proxyConfig.bypassProxy || ''
|
||||
...currentProxyConfig.auth,
|
||||
enabled: e.target.checked
|
||||
}
|
||||
});
|
||||
}, [proxyConfig]);
|
||||
};
|
||||
|
||||
const handleAuthUsernameChange = (e) => {
|
||||
const username = e.target.value;
|
||||
if (validateAuthUsernameOnChange(username)) {
|
||||
updateProxy({
|
||||
auth: {
|
||||
...currentProxyConfig.auth,
|
||||
username
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthPasswordChange = (e) => {
|
||||
const password = e.target.value;
|
||||
if (validateAuthPasswordOnChange(password)) {
|
||||
updateProxy({
|
||||
auth: {
|
||||
...currentProxyConfig.auth,
|
||||
password
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleBypassProxyChange = (e) => {
|
||||
const bypassProxy = e.target.value;
|
||||
if (validateBypassProxyOnChange(bypassProxy)) {
|
||||
updateProxy({ bypassProxy });
|
||||
}
|
||||
};
|
||||
|
||||
const enabledValue = currentProxyConfig.enabled === true ? 'true' : currentProxyConfig.enabled === false ? 'false' : 'global';
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">Configure proxy settings for this collection.</div>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="bruno-form">
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
Config
|
||||
@@ -120,8 +169,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
type="radio"
|
||||
name="enabled"
|
||||
value="global"
|
||||
checked={formik.values.enabled === 'global'}
|
||||
onChange={formik.handleChange}
|
||||
checked={enabledValue === 'global'}
|
||||
onChange={handleEnabledChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
global
|
||||
@@ -130,9 +179,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
<input
|
||||
type="radio"
|
||||
name="enabled"
|
||||
value={'true'}
|
||||
checked={formik.values.enabled === 'true'}
|
||||
onChange={formik.handleChange}
|
||||
value="true"
|
||||
checked={enabledValue === 'true'}
|
||||
onChange={handleEnabledChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
enabled
|
||||
@@ -141,9 +190,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
<input
|
||||
type="radio"
|
||||
name="enabled"
|
||||
value={'false'}
|
||||
checked={formik.values.enabled === 'false'}
|
||||
onChange={formik.handleChange}
|
||||
value="false"
|
||||
checked={enabledValue === 'false'}
|
||||
onChange={handleEnabledChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
disabled
|
||||
@@ -160,8 +209,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="http"
|
||||
checked={formik.values.protocol === 'http'}
|
||||
onChange={formik.handleChange}
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'http'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTP
|
||||
@@ -171,8 +220,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="https"
|
||||
checked={formik.values.protocol === 'https'}
|
||||
onChange={formik.handleChange}
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'https'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTPS
|
||||
@@ -182,8 +231,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks4"
|
||||
checked={formik.values.protocol === 'socks4'}
|
||||
onChange={formik.handleChange}
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'socks4'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS4
|
||||
@@ -193,8 +242,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks5"
|
||||
checked={formik.values.protocol === 'socks5'}
|
||||
onChange={formik.handleChange}
|
||||
checked={(currentProxyConfig.protocol || 'http') === 'socks5'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS5
|
||||
@@ -214,12 +263,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.hostname || ''}
|
||||
onChange={handleHostnameChange}
|
||||
value={currentProxyConfig.hostname || ''}
|
||||
/>
|
||||
{formik.touched.hostname && formik.errors.hostname ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="port">
|
||||
@@ -234,12 +280,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.port}
|
||||
onChange={handlePortChange}
|
||||
value={currentProxyConfig.port || ''}
|
||||
/>
|
||||
{formik.touched.port && formik.errors.port ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.port}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.enabled">
|
||||
@@ -248,8 +291,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
name="auth.enabled"
|
||||
checked={formik.values.auth.enabled}
|
||||
onChange={formik.handleChange}
|
||||
checked={currentProxyConfig.auth?.enabled || false}
|
||||
onChange={handleAuthEnabledChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -266,12 +309,9 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.username}
|
||||
onChange={formik.handleChange}
|
||||
value={currentProxyConfig.auth?.username || ''}
|
||||
onChange={handleAuthUsernameChange}
|
||||
/>
|
||||
{formik.touched.auth?.username && formik.errors.auth?.username ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.password">
|
||||
@@ -287,8 +327,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.password}
|
||||
onChange={formik.handleChange}
|
||||
value={currentProxyConfig.auth?.password || ''}
|
||||
onChange={handleAuthPasswordChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -298,9 +338,6 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
|
||||
</button>
|
||||
</div>
|
||||
{formik.touched.auth?.password && formik.errors.auth?.password ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
@@ -316,19 +353,16 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.bypassProxy || ''}
|
||||
onChange={handleBypassProxyChange}
|
||||
value={currentProxyConfig.bypassProxy || ''}
|
||||
/>
|
||||
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,37 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateCollectionRequestScript, updateCollectionResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Script = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestScript = get(collection, 'root.request.script.req', '');
|
||||
const responseScript = get(collection, 'root.request.script.res', '');
|
||||
const [activeTab, setActiveTab] = useState('pre-request');
|
||||
const preRequestEditorRef = useRef(null);
|
||||
const postResponseEditorRef = useRef(null);
|
||||
const requestScript = collection.draft?.root ? get(collection, 'draft.root.request.script.req', '') : get(collection, 'root.request.script.req', '');
|
||||
const responseScript = collection.draft?.root ? get(collection, 'draft.root.request.script.res', '') : get(collection, 'root.request.script.res', '');
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
// Refresh CodeMirror when tab becomes visible
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (activeTab === 'pre-request' && preRequestEditorRef.current?.editor) {
|
||||
preRequestEditorRef.current.editor.refresh();
|
||||
} else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) {
|
||||
postResponseEditorRef.current.editor.refresh();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [activeTab]);
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateCollectionRequestScript({
|
||||
@@ -34,42 +51,51 @@ const Script = ({ collection }) => {
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
dispatch(saveCollectionSettings(collection.uid));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<StyledWrapper className="w-full flex flex-col h-full pt-4">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Write pre and post-request scripts that will run before and after any request in this collection is sent.
|
||||
</div>
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 mt-6">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="pre-request">Pre Request</TabsTrigger>
|
||||
<TabsTrigger value="post-response">Post Response</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="pre-request" className="mt-2">
|
||||
<CodeEditor
|
||||
ref={preRequestEditorRef}
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="post-response" className="mt-2">
|
||||
<CodeEditor
|
||||
ref={postResponseEditorRef}
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-12">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
|
||||
@@ -3,13 +3,13 @@ import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateCollectionTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Tests = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tests = get(collection, 'root.request.tests', '');
|
||||
const tests = collection.draft?.root ? get(collection, 'draft.root.request.tests', '') : get(collection, 'root.request.tests', '');
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
@@ -23,7 +23,7 @@ const Tests = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
|
||||
@@ -3,8 +3,8 @@ import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import MultiLineEditor from 'components/MultiLineEditor';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -28,7 +28,7 @@ const VarsTable = ({ collection, vars, varType }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const onSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
const handleVarChange = (e, v, type) => {
|
||||
const _var = cloneDeep(v);
|
||||
switch (type) {
|
||||
@@ -114,7 +114,7 @@ const VarsTable = ({ collection, vars, varType }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
<MultiLineEditor
|
||||
value={_var.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
|
||||
@@ -2,14 +2,16 @@ import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import DeprecationWarning from 'components/DeprecationWarning';
|
||||
|
||||
const Vars = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = get(collection, 'root.request.vars.res', []);
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []);
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
@@ -18,6 +20,7 @@ const Vars = ({ collection }) => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<DeprecationWarning featureName="Post Response Variables" learnMoreUrl="https://github.com/usebruno/bruno/discussions/6231" />
|
||||
<VarsTable collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import ProxySettings from './ProxySettings';
|
||||
@@ -13,16 +10,14 @@ import Auth from './Auth';
|
||||
import Script from './Script';
|
||||
import Test from './Tests';
|
||||
import Presets from './Presets';
|
||||
import Grpc from './Grpc';
|
||||
import Protobuf from './Protobuf';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars/index';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import Overview from './Overview/index';
|
||||
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
|
||||
|
||||
const CollectionSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isGrpcEnabled = useBetaFeature(BETA_FEATURES.GRPC);
|
||||
const tab = collection.settingsSelectedTab;
|
||||
const setTab = (tab) => {
|
||||
dispatch(
|
||||
@@ -33,61 +28,26 @@ const CollectionSettings = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const root = collection?.root;
|
||||
const root = collection?.draft?.root || collection?.root;
|
||||
const hasScripts = root?.request?.script?.res || root?.request?.script?.req;
|
||||
const hasTests = root?.request?.tests;
|
||||
const hasDocs = root?.docs;
|
||||
|
||||
const headers = get(collection, 'root.request.headers', []);
|
||||
const headers = collection.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
|
||||
const activeHeadersCount = headers.filter((header) => header.enabled).length;
|
||||
|
||||
const requestVars = get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = get(collection, 'root.request.vars.res', []);
|
||||
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []);
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
|
||||
const authMode = (collection.draft?.root ? get(collection, 'draft.root.request.auth', {}) : get(collection, 'root.request.auth', {})).mode || 'none';
|
||||
|
||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const grpcConfig = get(collection, 'brunoConfig.grpc', {});
|
||||
const presets = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.presets', []) : get(collection, 'brunoConfig.presets', []);
|
||||
const hasPresets = presets && presets.requestUrl !== '';
|
||||
|
||||
const onProxySettingsUpdate = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.proxy = config;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection settings updated successfully.');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
|
||||
const onClientCertSettingsUpdate = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
if (!brunoConfig.clientCertificates) {
|
||||
brunoConfig.clientCertificates = {
|
||||
enabled: true,
|
||||
certs: [config]
|
||||
};
|
||||
} else {
|
||||
brunoConfig.clientCertificates.certs.push(config);
|
||||
}
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection settings updated successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
|
||||
const onClientCertSettingsRemove = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.clientCertificates.certs = brunoConfig.clientCertificates.certs.filter(
|
||||
(item) => item.domain != config.domain
|
||||
);
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection settings updated successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
const proxyConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.proxy', {}) : get(collection, 'brunoConfig.proxy', {});
|
||||
const proxyEnabled = proxyConfig.hostname ? true : false;
|
||||
const clientCertConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.clientCertificates.certs', []) : get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const protobufConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.protobuf', {}) : get(collection, 'brunoConfig.protobuf', {});
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
@@ -113,20 +73,17 @@ const CollectionSettings = ({ collection }) => {
|
||||
return <Presets collection={collection} />;
|
||||
}
|
||||
case 'proxy': {
|
||||
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
|
||||
return <ProxySettings collection={collection} />;
|
||||
}
|
||||
case 'clientCert': {
|
||||
return (
|
||||
<ClientCertSettings
|
||||
root={collection.pathname}
|
||||
clientCertConfig={clientCertConfig}
|
||||
onUpdate={onClientCertSettingsUpdate}
|
||||
onRemove={onClientCertSettingsRemove}
|
||||
collection={collection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'grpc': {
|
||||
return <Grpc collection={collection} />;
|
||||
case 'protobuf': {
|
||||
return <Protobuf collection={collection} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -165,21 +122,20 @@ const CollectionSettings = ({ collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('presets')} role="tab" onClick={() => setTab('presets')}>
|
||||
Presets
|
||||
{hasPresets && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||
Proxy
|
||||
{Object.keys(proxyConfig).length > 0 && <StatusDot />}
|
||||
{Object.keys(proxyConfig).length > 0 && proxyEnabled && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('clientCert')} role="tab" onClick={() => setTab('clientCert')}>
|
||||
Client Certificates
|
||||
{clientCertConfig.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
{isGrpcEnabled && (
|
||||
<div className={getTabClassname('grpc')} role="tab" onClick={() => setTab('grpc')}>
|
||||
gRPC
|
||||
{grpcConfig.protoFiles && grpcConfig.protoFiles.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
)}
|
||||
<div className={getTabClassname('protobuf')} role="tab" onClick={() => setTab('protobuf')}>
|
||||
Protobuf
|
||||
{protobufConfig.protoFiles && protobufConfig.protoFiles.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
</div>
|
||||
<section className="mt-4 h-full overflow-auto">{getTabPanel(tab)}</section>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.deprecation-warning {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
background: ${(props) => props.theme.deprecationWarning.bg};
|
||||
border: 1px solid ${(props) => props.theme.deprecationWarning.border};
|
||||
border-radius: 6px;
|
||||
|
||||
.warning-icon {
|
||||
color: ${(props) => props.theme.deprecationWarning.icon};
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-style: normal;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
color: ${(props) => props.theme.deprecationWarning.text};
|
||||
|
||||
a {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import IconAlertTriangleFilled from '../Icons/IconAlertTriangleFilled';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DeprecationWarning = ({ featureName, learnMoreUrl }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="deprecation-warning">
|
||||
<IconAlertTriangleFilled className="warning-icon" size={16} />
|
||||
<span className="warning-text">
|
||||
{featureName} will be removed in <strong>v3.0.0</strong>. They are deprecated and will no longer be supported. Learn more in{' '}
|
||||
<a href={learnMoreUrl} target="_blank" rel="noreferrer">this post</a> or contact us at{' '}
|
||||
<a href="mailto:support@usebruno.com">support@usebruno.com</a> with questions.
|
||||
</span>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeprecationWarning;
|
||||
@@ -67,10 +67,10 @@ const RequestTab = ({ request, response }) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{request?.body && (
|
||||
{request?.data && (
|
||||
<div className="section">
|
||||
<h4>Request Body</h4>
|
||||
<pre className="code-block">{formatBody(request.body)}</pre>
|
||||
<pre className="code-block">{formatBody(request.data)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -239,4 +239,4 @@ const RequestDetailsPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestDetailsPanel;
|
||||
export default RequestDetailsPanel;
|
||||
|
||||
@@ -12,7 +12,8 @@ import {
|
||||
IconCode,
|
||||
IconChevronDown,
|
||||
IconTerminal2,
|
||||
IconNetwork
|
||||
IconNetwork,
|
||||
IconDashboard
|
||||
} from '@tabler/icons';
|
||||
import {
|
||||
closeConsole,
|
||||
@@ -24,10 +25,12 @@ import {
|
||||
updateNetworkFilter,
|
||||
toggleAllNetworkFilters
|
||||
} from 'providers/ReduxStore/slices/logs';
|
||||
|
||||
import NetworkTab from './NetworkTab';
|
||||
import RequestDetailsPanel from './RequestDetailsPanel';
|
||||
// import DebugTab from './DebugTab';
|
||||
import ErrorDetailsPanel from './ErrorDetailsPanel';
|
||||
import Performance from '../Performance';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const LogIcon = ({ type }) => {
|
||||
@@ -384,6 +387,8 @@ const Console = () => {
|
||||
);
|
||||
case 'network':
|
||||
return <NetworkTab />;
|
||||
case 'performance':
|
||||
return <Performance />;
|
||||
// case 'debug':
|
||||
// return <DebugTab />;
|
||||
default:
|
||||
@@ -484,6 +489,14 @@ const Console = () => {
|
||||
<span>Network</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`console-tab ${activeTab === 'performance' ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange('performance')}
|
||||
>
|
||||
<IconDashboard size={16} strokeWidth={1.5} />
|
||||
<span>Performance</span>
|
||||
</button>
|
||||
|
||||
{/* <button
|
||||
className={`console-tab ${activeTab === 'debug' ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange('debug')}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.tab-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: ${(props) => props.theme.console.bg};
|
||||
}
|
||||
|
||||
.tab-content-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.overview-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.overview-section {
|
||||
margin-bottom: 32px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid ${(props) => props.theme.console.border};
|
||||
|
||||
h3 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: ${(props) => props.theme.console.textMuted};
|
||||
}
|
||||
}
|
||||
|
||||
.system-resources {
|
||||
margin-bottom: 16px;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
}
|
||||
}
|
||||
|
||||
.resource-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.resource-card {
|
||||
background: ${(props) => props.theme.console.headerBg};
|
||||
border: 1px solid ${(props) => props.theme.console.border};
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.resource-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
}
|
||||
|
||||
.resource-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.resource-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.console.titleColor};
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.resource-subtitle {
|
||||
font-size: 11px;
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
}
|
||||
|
||||
.resource-trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
margin-top: 8px;
|
||||
|
||||
&.up {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: #e81123;
|
||||
}
|
||||
|
||||
&.stable {
|
||||
color: ${(props) => props.theme.console.buttonColor};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
131
packages/bruno-app/src/components/Devtools/Performance/index.js
Normal file
131
packages/bruno-app/src/components/Devtools/Performance/index.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import {
|
||||
IconCpu,
|
||||
IconDatabase,
|
||||
IconClock,
|
||||
IconServer,
|
||||
IconChartLine
|
||||
} from '@tabler/icons';
|
||||
|
||||
const Performance = () => {
|
||||
const { systemResources } = useSelector((state) => state.performance);
|
||||
|
||||
useEffect(() => {
|
||||
const { ipcRenderer } = window;
|
||||
|
||||
if (!ipcRenderer) {
|
||||
console.warn('IPC Renderer not available');
|
||||
return;
|
||||
}
|
||||
|
||||
const startMonitoring = async () => {
|
||||
try {
|
||||
await ipcRenderer.invoke('renderer:start-system-monitoring', 2000);
|
||||
} catch (error) {
|
||||
console.error('Failed to start system monitoring:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const stopMonitoring = async () => {
|
||||
try {
|
||||
await ipcRenderer.invoke('renderer:stop-system-monitoring');
|
||||
} catch (error) {
|
||||
console.error('Failed to stop system monitoring:', error);
|
||||
}
|
||||
};
|
||||
|
||||
startMonitoring();
|
||||
|
||||
return () => {
|
||||
stopMonitoring();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const formatBytes = (bytes) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatUptime = (seconds) => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
if (hours > 0) return `${hours}h ${minutes}m ${secs}s`;
|
||||
if (minutes > 0) return `${minutes}m ${secs}s`;
|
||||
return `${secs}s`;
|
||||
};
|
||||
|
||||
const SystemResourceCard = ({ icon: Icon, title, value, subtitle, color = 'default', trend }) => (
|
||||
<div className={`resource-card ${color}`}>
|
||||
<div className="resource-header">
|
||||
<Icon size={20} strokeWidth={1.5} />
|
||||
<span className="resource-title">{title}</span>
|
||||
</div>
|
||||
<div className="resource-value">{value}</div>
|
||||
{subtitle && <div className="resource-subtitle">{subtitle}</div>}
|
||||
{trend && (
|
||||
<div className={`resource-trend ${trend > 0 ? 'up' : trend < 0 ? 'down' : 'stable'}`}>
|
||||
<IconChartLine size={12} strokeWidth={1.5} />
|
||||
<span>
|
||||
{trend > 0 ? '+' : ''}
|
||||
{trend.toFixed(1)}
|
||||
%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="tab-content">
|
||||
<div className="tab-content-area">
|
||||
<div className="system-resources">
|
||||
<h2>System Resources</h2>
|
||||
<div className="resource-cards">
|
||||
<SystemResourceCard
|
||||
icon={IconCpu}
|
||||
title="CPU Usage"
|
||||
value={`${systemResources.cpu.toFixed(1)}%`}
|
||||
subtitle="Current process"
|
||||
color={systemResources.cpu > 80 ? 'danger' : systemResources.cpu > 60 ? 'warning' : 'success'}
|
||||
/>
|
||||
|
||||
<SystemResourceCard
|
||||
icon={IconDatabase}
|
||||
title="Memory Usage"
|
||||
value={formatBytes(systemResources.memory)}
|
||||
subtitle="Current process"
|
||||
color={systemResources.memory > 500 * 1024 * 1024 ? 'danger' : 'default'}
|
||||
/>
|
||||
|
||||
<SystemResourceCard
|
||||
icon={IconClock}
|
||||
title="Uptime"
|
||||
value={formatUptime(systemResources.uptime)}
|
||||
subtitle="Process runtime"
|
||||
color="info"
|
||||
/>
|
||||
|
||||
<SystemResourceCard
|
||||
icon={IconServer}
|
||||
title="Process ID"
|
||||
value={systemResources.pid || 'N/A'}
|
||||
subtitle="Current PID"
|
||||
color="default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Performance;
|
||||
@@ -2,7 +2,12 @@ import React from 'react';
|
||||
import Tippy from '@tippyjs/react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Dropdown = ({ icon, children, onCreate, placement, transparent, ...props }) => {
|
||||
const Dropdown = ({ icon, children, onCreate, placement, transparent, visible, ...props }) => {
|
||||
// When in controlled mode (visible prop is provided), don't use trigger prop
|
||||
const tippyProps = visible !== undefined
|
||||
? { ...props, visible, interactive: true, appendTo: 'parent' }
|
||||
: { ...props, trigger: 'click', interactive: true, appendTo: 'parent' };
|
||||
|
||||
return (
|
||||
<StyledWrapper className="dropdown" transparent={transparent}>
|
||||
<Tippy
|
||||
@@ -11,10 +16,7 @@ const Dropdown = ({ icon, children, onCreate, placement, transparent, ...props }
|
||||
animation={false}
|
||||
arrow={false}
|
||||
onCreate={onCreate}
|
||||
interactive={true}
|
||||
trigger="click"
|
||||
appendTo="parent"
|
||||
{...props}
|
||||
{...tippyProps}
|
||||
>
|
||||
{icon}
|
||||
</Tippy>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
/* Environment item styling */
|
||||
.environment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
.environment-name {
|
||||
color: ${(props) => props.theme.text};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,269 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import Portal from 'components/Portal/index';
|
||||
import Modal from 'components/Modal';
|
||||
import { exportBrunoEnvironment } from 'utils/exporters/bruno-environment';
|
||||
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ExportEnvironmentModal = ({ onClose, environments = [], environmentType }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Helper function to truncate environment names
|
||||
const truncateEnvName = (name) => {
|
||||
if (name.length > 40) {
|
||||
return name.substring(0, 40) + '...';
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [filePath, setFilePath] = useState('');
|
||||
const [selectedEnvironments, setSelectedEnvironments] = useState({});
|
||||
const [exportFormat, setExportFormat] = useState(environments.length > 1 ? 'single-file' : 'single-object');
|
||||
|
||||
// Initialize selected environments
|
||||
useEffect(() => {
|
||||
const initialSelection = {};
|
||||
|
||||
// Add all environments and select them by default
|
||||
environments.forEach((env) => {
|
||||
initialSelection[env.uid] = true;
|
||||
});
|
||||
|
||||
setSelectedEnvironments(initialSelection);
|
||||
}, [environments]);
|
||||
|
||||
useEffect(() => {
|
||||
const selectedCount = Object.values(selectedEnvironments).filter(Boolean).length;
|
||||
if (selectedCount <= 1) {
|
||||
setExportFormat('single-object');
|
||||
}
|
||||
if (exportFormat === 'single-object' && selectedCount > 1) {
|
||||
setExportFormat('single-file');
|
||||
}
|
||||
}, [selectedEnvironments]);
|
||||
|
||||
const browse = () => {
|
||||
dispatch(browseDirectory())
|
||||
.then((dirPath) => {
|
||||
if (typeof dirPath === 'string') {
|
||||
setFilePath(dirPath);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setFilePath('');
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleEnvironmentToggle = (envUid) => {
|
||||
setSelectedEnvironments((prev) => {
|
||||
const newSelection = {
|
||||
...prev,
|
||||
[envUid]: !prev[envUid]
|
||||
};
|
||||
return newSelection;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectAll = () => {
|
||||
const allSelected = environments.every((env) => selectedEnvironments[env.uid]) || false;
|
||||
|
||||
const newSelection = environments.reduce((acc, env) => ({
|
||||
...acc,
|
||||
[env.uid]: !allSelected
|
||||
}), {}) || {};
|
||||
|
||||
setSelectedEnvironments(newSelection);
|
||||
};
|
||||
|
||||
// Memoized selected environments and count
|
||||
const selectedEnvs = useMemo(() => {
|
||||
return environments.filter((env) => selectedEnvironments[env.uid]) || [];
|
||||
}, [environments, selectedEnvironments]);
|
||||
|
||||
const selectedCount = selectedEnvs.length;
|
||||
|
||||
const exportFormatOptions = useMemo(() => {
|
||||
const isMultiple = selectedCount > 1;
|
||||
|
||||
if (isMultiple) {
|
||||
return [
|
||||
{ value: 'single-file', label: 'Single JSON file', description: 'All environments in one JSON array' },
|
||||
{ value: 'folder', label: 'Separate files in folder', description: 'Each environment as a separate JSON file', disabled: false }
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
{ value: 'single-object', label: 'Single JSON file', description: 'Export as a single environment JSON object' },
|
||||
{ value: 'folder', label: 'Separate files in folder', description: 'Each environment as a separate JSON file', disabled: true }
|
||||
];
|
||||
}, [selectedCount, exportFormat]);
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
|
||||
if (!filePath) {
|
||||
toast.error('Please select a location to save the files');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedCount === 0) {
|
||||
toast.error('Please select at least one environment to export');
|
||||
return;
|
||||
}
|
||||
|
||||
await exportBrunoEnvironment({ environments: selectedEnvs, environmentType, filePath, exportFormat });
|
||||
|
||||
const successMessage = exportFormat === 'folder'
|
||||
? `Environments exported successfully to bruno-${environmentType}-environments folder`
|
||||
: 'Environment(s) exported successfully';
|
||||
toast.success(successMessage);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Export error:', error);
|
||||
toast.error(error.message || 'Failed to export environments');
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<StyledWrapper>
|
||||
<Modal
|
||||
size="md"
|
||||
title="Export Environments"
|
||||
hideFooter={true}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<div className="py-2">
|
||||
{/* Environments Section */}
|
||||
<div className="mb-4">
|
||||
{environments && environments.length > 0 ? (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex justify-between items-center mb-2 pb-1">
|
||||
<h3 className="font-semibold text-sm text-theme">
|
||||
{environmentType === 'global' ? 'Global Environments' : 'Collection Environments'}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSelectAll}
|
||||
className="text-xs text-link px-1 py-0.5 rounded transition-colors"
|
||||
>
|
||||
{environments.every((env) => selectedEnvironments[env.uid]) ? 'Deselect All' : 'Select All'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 flex-1 overflow-y-auto">
|
||||
{environments.map((env) => (
|
||||
<label key={env.uid} className="environment-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedEnvironments[env.uid] || false}
|
||||
onChange={() => handleEnvironmentToggle(env.uid)}
|
||||
disabled={isExporting}
|
||||
className="w-3.5 h-3.5 flex-shrink-0"
|
||||
/>
|
||||
<span className="environment-name">{truncateEnvName(env.name)}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex justify-between items-center mb-2 pb-1">
|
||||
<h3 className="font-semibold text-sm text-theme">
|
||||
{environmentType === 'global' ? 'Global Environments' : 'Collection Environments'}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center justify-center flex-1 p-4 text-center">
|
||||
<span className="text-xs text-muted">
|
||||
No {environmentType === 'global' ? 'global' : 'collection'} environments
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Export Format Section */}
|
||||
{selectedCount > 0 && (
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium mb-2 text-theme">
|
||||
Export Format
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{exportFormatOptions.map((option) => (
|
||||
<label key={option.value} className={`flex items-start p-2 rounded transition-colors ${option.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}>
|
||||
<input
|
||||
type="radio"
|
||||
name="exportFormat"
|
||||
value={option.value}
|
||||
checked={exportFormat === option.value}
|
||||
onChange={(e) => setExportFormat(e.target.value)}
|
||||
disabled={isExporting || option.disabled}
|
||||
className={`mt-0.5 mr-3 w-4 h-4 ${option.disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
/>
|
||||
<div>
|
||||
<div className={`text-sm font-medium ${option.disabled ? 'text-muted' : 'text-theme'}`}>{option.label}</div>
|
||||
<div className="text-xs text-muted">{option.description}</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Location Input Section */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="export-location" className="block text-sm font-medium mb-2 text-theme">
|
||||
Location
|
||||
</label>
|
||||
<div className="flex flex-col relative items-center">
|
||||
<input
|
||||
id="export-location"
|
||||
type="text"
|
||||
className={`flex-1 textbox w-full ${isExporting || selectedCount <= 0 ? '' : 'cursor-pointer'}`}
|
||||
title={filePath}
|
||||
value={filePath}
|
||||
onClick={browse}
|
||||
onChange={(e) => setFilePath(e.target.value)}
|
||||
disabled={isExporting || selectedCount <= 0}
|
||||
placeholder="Select a target location"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Export Actions */}
|
||||
<div className="flex justify-end gap-2 mt-4 pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-cancel mt-2 flex items-center"
|
||||
onClick={onClose}
|
||||
disabled={isExporting}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary mt-2 flex items-center"
|
||||
onClick={handleExport}
|
||||
disabled={isExporting || selectedCount === 0}
|
||||
>
|
||||
{isExporting ? 'Exporting...' : `Export ${selectedCount || ''} Environment${selectedCount !== 1 ? 's' : ''}`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExportEnvironmentModal;
|
||||
@@ -0,0 +1,166 @@
|
||||
import React, { useState } from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import importPostmanEnvironment from 'utils/importers/postman-environment';
|
||||
import importBrunoEnvironment from 'utils/importers/bruno-environment';
|
||||
import { readMultipleFiles } from 'utils/importers/file-reader';
|
||||
import { importEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import { IconFileImport } from '@tabler/icons';
|
||||
|
||||
const ImportEnvironmentModal = ({ type = 'collection', collection, onClose, onEnvironmentCreated }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
||||
const isGlobal = type === 'global';
|
||||
|
||||
// Validate required props
|
||||
if (!isGlobal && !collection) {
|
||||
console.error('ImportEnvironmentModal: collection prop is required when type is "collection"');
|
||||
return null;
|
||||
}
|
||||
const modalTitle = isGlobal ? 'Import Global Environment' : 'Import Environment';
|
||||
const modalTestId = isGlobal ? 'import-global-environment-modal' : 'import-environment-modal';
|
||||
const importTestId = isGlobal ? 'import-global-environment' : 'import-environment';
|
||||
|
||||
const processEnvironments = async (environments, successMessage) => {
|
||||
const validEnvironments = environments.filter((env) => {
|
||||
if (env.name && env.name !== 'undefined') {
|
||||
return true;
|
||||
} else {
|
||||
toast.error('Failed to import environment: env has no name');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (validEnvironments.length === 0) {
|
||||
toast.error('No valid environments found to import');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Process environments sequentially to ensure unique name checking considers previously imported environments
|
||||
let importedCount = 0;
|
||||
for (const environment of validEnvironments) {
|
||||
const action = isGlobal
|
||||
? addGlobalEnvironment({ name: environment.name, variables: environment.variables })
|
||||
: importEnvironment({ name: environment.name, variables: environment.variables, collectionUid: collection?.uid });
|
||||
|
||||
await dispatch(action);
|
||||
importedCount++;
|
||||
}
|
||||
|
||||
toast.success(`${importedCount > 1 ? `${importedCount} environments` : 'Environment'} imported successfully`);
|
||||
} catch (error) {
|
||||
toast.error('An error occurred while importing the environment(s)');
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const detectEnvironmentFormat = (data) => {
|
||||
// bruno environment `single-object` export type
|
||||
if (data.info && data.info.type === 'bruno-environment') {
|
||||
return 'bruno';
|
||||
} else if (Array.isArray(data)) {
|
||||
// bruno environment`single-file` export type
|
||||
return data.some((env) => env.info && env.info.type === 'bruno-environment') ? 'bruno' : 'postman';
|
||||
} else if (data.id && data.values) {
|
||||
// postman environment
|
||||
return 'postman';
|
||||
}
|
||||
return 'bruno';
|
||||
};
|
||||
|
||||
const handleImportEnvironment = async (files) => {
|
||||
try {
|
||||
// Read and parse all files
|
||||
const parsedFiles = await readMultipleFiles(Array.from(files));
|
||||
|
||||
// Detect format from first file's content
|
||||
const format = detectEnvironmentFormat(parsedFiles[0].content);
|
||||
let environments;
|
||||
|
||||
if (format === 'postman') {
|
||||
environments = await importPostmanEnvironment(parsedFiles);
|
||||
} else {
|
||||
environments = await importBrunoEnvironment(parsedFiles);
|
||||
}
|
||||
|
||||
await processEnvironments(environments);
|
||||
onClose();
|
||||
if (onEnvironmentCreated) {
|
||||
onEnvironmentCreated();
|
||||
}
|
||||
} catch (err) {
|
||||
toastError(err, 'Import environment failed');
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelect = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = true;
|
||||
input.accept = '.json';
|
||||
input.onchange = (e) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
handleImportEnvironment(e.target.files);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
setIsDragOver(false);
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
if (files.length > 0) {
|
||||
handleImportEnvironment(files);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="md" title={modalTitle} hideFooter={true} handleConfirm={onClose} handleCancel={onClose} dataTestId={modalTestId}>
|
||||
<div className="py-2">
|
||||
<div
|
||||
className={`flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed p-12 text-center cursor-pointer transition-colors focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2 ${
|
||||
isDragOver
|
||||
? 'border-amber-400 bg-amber-50 dark:bg-amber-900/20'
|
||||
: 'border-zinc-300 dark:border-zinc-400 hover:border-zinc-400'
|
||||
}`}
|
||||
onClick={handleFileSelect}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
data-testid={importTestId}
|
||||
>
|
||||
<IconFileImport size={64} />
|
||||
<span className="mt-2 block text-sm font-semibold">
|
||||
{isDragOver ? 'Drop your environment files here' : 'Import your environments'}
|
||||
</span>
|
||||
<span className="mt-1 block text-xs text-muted">
|
||||
Drag & drop JSON files/folders or click to browse. Supports both Bruno and Postman formats.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportEnvironmentModal;
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { IconPlus, IconDownload, IconSettings } from '@tabler/icons';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
|
||||
const EnvironmentListContent = ({
|
||||
environments,
|
||||
activeEnvironmentUid,
|
||||
description,
|
||||
onEnvironmentSelect,
|
||||
onSettingsClick,
|
||||
onCreateClick,
|
||||
onImportClick
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
{environments && environments.length > 0 ? (
|
||||
<>
|
||||
<div className="environment-list">
|
||||
<div className="dropdown-item no-environment" onClick={() => onEnvironmentSelect(null)}>
|
||||
<span>No Environment</span>
|
||||
</div>
|
||||
<ToolHint
|
||||
anchorSelect="[data-tooltip-content]"
|
||||
place="right"
|
||||
positionStrategy="fixed"
|
||||
tooltipStyle={{
|
||||
maxWidth: '200px',
|
||||
wordWrap: 'break-word'
|
||||
}}
|
||||
delayShow={1000}
|
||||
>
|
||||
<div>
|
||||
{environments.map((env) => (
|
||||
<div
|
||||
key={env.uid}
|
||||
className={`dropdown-item ${env.uid === activeEnvironmentUid ? 'active' : ''}`}
|
||||
onClick={() => onEnvironmentSelect(env)}
|
||||
data-tooltip-content={env.name}
|
||||
data-tooltip-hidden={env.name?.length < 90}
|
||||
>
|
||||
<span className="max-w-100% truncate no-wrap">{env.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ToolHint>
|
||||
<div className="dropdown-item configure-button">
|
||||
<button onClick={onSettingsClick} id="configure-env">
|
||||
<IconSettings size={16} strokeWidth={1.5} />
|
||||
<span>Configure</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
<h3>Ready to get started?</h3>
|
||||
<p>{description}</p>
|
||||
<div className="space-y-2">
|
||||
<button onClick={onCreateClick} id="create-env">
|
||||
<IconPlus size={16} strokeWidth={1.5} />
|
||||
Create
|
||||
</button>
|
||||
<button onClick={onImportClick} id="import-env">
|
||||
<IconDownload size={16} strokeWidth={1.5} />
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentListContent;
|
||||
@@ -2,14 +2,230 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.current-environment {
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border-radius: 15px;
|
||||
border-radius: 0.9375rem;
|
||||
padding: 0.25rem 0.5rem 0.25rem 0.75rem;
|
||||
user-select: none;
|
||||
background-color: transparent;
|
||||
border: 1px solid ${(props) => props.theme.dropdown.selectedColor};
|
||||
line-height: 1rem;
|
||||
|
||||
.caret {
|
||||
margin-left: 0.25rem;
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140, 140, 140);
|
||||
}
|
||||
|
||||
.env-icon {
|
||||
margin-right: 0.25rem;
|
||||
color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
}
|
||||
|
||||
.env-text {
|
||||
color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
font-size: 0.875rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.env-separator {
|
||||
color: #8c8c8c;
|
||||
margin: 0 0.25rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.env-text-inactive {
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.no-environments {
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border: 1px solid transparent;
|
||||
color: ${(props) => props.theme.dropdown.secondaryText};
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-box {
|
||||
width: ${(props) => props.width}px;
|
||||
min-width: 12rem;
|
||||
max-width: 650px !important;
|
||||
min-height: 15.5rem;
|
||||
max-height: 75vh;
|
||||
font-size: 0.8125rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tippy-box .tippy-content {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.35rem 0.6rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.8125rem;
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: ${(props) => props.theme.dropdown.selectedBg};
|
||||
color: ${(props) => props.theme.dropdown.selectedColor};
|
||||
}
|
||||
|
||||
&.no-environment {
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.configure-button {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: ${(props) => props.theme.dropdown.bg};
|
||||
border-top: 0.0625rem solid ${(props) => props.theme.dropdown.separator};
|
||||
z-index: 10;
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.dropdown.bg + ' !important'};
|
||||
}
|
||||
|
||||
button {
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
color: var(--color-tab-inactive);
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.tab-content-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color};
|
||||
border-bottom-color: ${(props) => props.theme.tabs.active.border};
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.environment-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: calc(75vh - 8rem);
|
||||
padding-bottom: 2.625rem;
|
||||
}
|
||||
|
||||
.dropdown-item-list {
|
||||
max-height: 75vh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
max-width: 20rem;
|
||||
margin: 0 auto;
|
||||
padding: 0.35rem 0.6rem;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 12.5rem;
|
||||
|
||||
h3 {
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
p {
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
opacity: 0.75;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1rem;
|
||||
max-width: 11.875rem;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.space-y-2 {
|
||||
width: 100%;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.space-y-2 > button {
|
||||
border: 0.0625rem solid ${(props) => props.theme.dropdown.primaryText};
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-collection-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
opacity: 0.75;
|
||||
|
||||
svg {
|
||||
margin: 0 auto 1rem auto;
|
||||
color: ${(props) => props.theme.dropdown.primaryText};
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,95 +1,275 @@
|
||||
import React, { useRef, forwardRef, useState } from 'react';
|
||||
import React, { useMemo, useState, useRef, forwardRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconWorld, IconDatabase, IconCaretDown, IconSettings, IconPlus, IconDownload } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateEnvironmentSettingsModalVisibility } from 'providers/ReduxStore/slices/app';
|
||||
import { IconSettings, IconCaretDown, IconDatabase, IconDatabaseOff } from '@tabler/icons';
|
||||
import EnvironmentSettings from '../EnvironmentSettings';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { selectGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import EnvironmentListContent from './EnvironmentListContent/index';
|
||||
import EnvironmentSettings from '../EnvironmentSettings';
|
||||
import GlobalEnvironmentSettings from 'components/GlobalEnvironments/EnvironmentSettings';
|
||||
import CreateEnvironment from '../EnvironmentSettings/CreateEnvironment';
|
||||
import ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';
|
||||
import CreateGlobalEnvironment from 'components/GlobalEnvironments/EnvironmentSettings/CreateEnvironment';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const EnvironmentSelector = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const [openSettingsModal, setOpenSettingsModal] = useState(false);
|
||||
const { environments, activeEnvironmentUid } = collection;
|
||||
const activeEnvironment = activeEnvironmentUid ? find(environments, (e) => e.uid === activeEnvironmentUid) : null;
|
||||
const [activeTab, setActiveTab] = useState('collection');
|
||||
const [showGlobalSettings, setShowGlobalSettings] = useState(false);
|
||||
const [showCollectionSettings, setShowCollectionSettings] = useState(false);
|
||||
const [showCreateGlobalModal, setShowCreateGlobalModal] = useState(false);
|
||||
const [showImportGlobalModal, setShowImportGlobalModal] = useState(false);
|
||||
const [showCreateCollectionModal, setShowCreateCollectionModal] = useState(false);
|
||||
const [showImportCollectionModal, setShowImportCollectionModal] = useState(false);
|
||||
|
||||
const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
|
||||
const activeGlobalEnvironmentUid = useSelector((state) => state.globalEnvironments.activeGlobalEnvironmentUid);
|
||||
const activeGlobalEnvironment = activeGlobalEnvironmentUid
|
||||
? find(globalEnvironments, (e) => e.uid === activeGlobalEnvironmentUid)
|
||||
: null;
|
||||
|
||||
const environments = collection?.environments || [];
|
||||
const activeEnvironmentUid = collection?.activeEnvironmentUid;
|
||||
const activeCollectionEnvironment = activeEnvironmentUid
|
||||
? find(environments, (e) => e.uid === activeEnvironmentUid)
|
||||
: null;
|
||||
|
||||
const tabs = [
|
||||
{ id: 'collection', label: 'Collection', icon: <IconDatabase size={16} strokeWidth={1.5} /> },
|
||||
{ id: 'global', label: 'Global', icon: <IconWorld size={16} strokeWidth={1.5} /> }
|
||||
];
|
||||
|
||||
const onDropdownCreate = (ref) => {
|
||||
dropdownTippyRef.current = ref;
|
||||
};
|
||||
|
||||
// Get description based on active tab
|
||||
const description =
|
||||
activeTab === 'collection'
|
||||
? 'Create your first environment to begin working with your collection.'
|
||||
: 'Create your first global environment to begin working across collections.';
|
||||
|
||||
// Environment selection handler
|
||||
const handleEnvironmentSelect = (environment) => {
|
||||
const action =
|
||||
activeTab === 'collection'
|
||||
? selectEnvironment(environment ? environment.uid : null, collection.uid)
|
||||
: selectGlobalEnvironment({ environmentUid: environment ? environment.uid : null });
|
||||
|
||||
dispatch(action)
|
||||
.then(() => {
|
||||
if (environment) {
|
||||
toast.success(`Environment changed to ${environment.name}`);
|
||||
} else {
|
||||
toast.success('No Environments are active now');
|
||||
}
|
||||
dropdownTippyRef.current.hide();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('An error occurred while selecting the environment');
|
||||
});
|
||||
};
|
||||
|
||||
// Settings handler
|
||||
const handleSettingsClick = () => {
|
||||
if (activeTab === 'collection') {
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(true));
|
||||
setShowCollectionSettings(true);
|
||||
} else {
|
||||
setShowGlobalSettings(true);
|
||||
}
|
||||
dropdownTippyRef.current.hide();
|
||||
};
|
||||
|
||||
// Create handler
|
||||
const handleCreateClick = () => {
|
||||
if (activeTab === 'collection') {
|
||||
setShowCreateCollectionModal(true);
|
||||
} else {
|
||||
setShowCreateGlobalModal(true);
|
||||
}
|
||||
dropdownTippyRef.current.hide();
|
||||
};
|
||||
|
||||
// Import handler
|
||||
const handleImportClick = () => {
|
||||
if (activeTab === 'collection') {
|
||||
setShowImportCollectionModal(true);
|
||||
} else {
|
||||
setShowImportGlobalModal(true);
|
||||
}
|
||||
dropdownTippyRef.current.hide();
|
||||
};
|
||||
|
||||
// Modal handlers
|
||||
const handleCloseSettings = () => {
|
||||
setShowGlobalSettings(false);
|
||||
setShowCollectionSettings(false);
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(false));
|
||||
};
|
||||
|
||||
// Calculate dropdown width based on the longest environment name.
|
||||
// To prevent resizing while switching between collection and global environments.
|
||||
const dropdownWidth = useMemo(() => {
|
||||
const allEnvironments = [...environments, ...globalEnvironments];
|
||||
if (allEnvironments.length === 0) return 0;
|
||||
|
||||
const maxCharLength = Math.max(...allEnvironments.map((env) => env.name?.length || 0));
|
||||
// 8 pixels per character: This is a rough estimate for the average character width in most fonts
|
||||
// (monospace fonts are typically 8-10px, proportional fonts vary but 8px is a safe average)
|
||||
return maxCharLength * 8;
|
||||
}, [environments, globalEnvironments]);
|
||||
|
||||
// Create icon component for dropdown trigger
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
const hasAnyEnv = activeGlobalEnvironment || activeCollectionEnvironment;
|
||||
|
||||
const displayContent = hasAnyEnv ? (
|
||||
<>
|
||||
{activeCollectionEnvironment && (
|
||||
<>
|
||||
<div className="flex items-center">
|
||||
<IconDatabase size={14} strokeWidth={1.5} className="env-icon" />
|
||||
<ToolHint
|
||||
text={activeCollectionEnvironment.name}
|
||||
toolhintId={`collection-env-${activeCollectionEnvironment.uid}`}
|
||||
place="bottom-start"
|
||||
delayShow={1000}
|
||||
hidden={activeCollectionEnvironment.name?.length < 7}
|
||||
>
|
||||
<span className="env-text max-w-24 truncate overflow-hidden">{activeCollectionEnvironment.name}</span>
|
||||
</ToolHint>
|
||||
</div>
|
||||
{activeGlobalEnvironment && <span className="env-separator">|</span>}
|
||||
</>
|
||||
)}
|
||||
{activeGlobalEnvironment && (
|
||||
<div className="flex items-center">
|
||||
<IconWorld size={14} strokeWidth={1.5} className="env-icon" />
|
||||
<ToolHint
|
||||
text={activeGlobalEnvironment.name}
|
||||
toolhintId={`global-env-${activeGlobalEnvironment.uid}`}
|
||||
place="bottom-start"
|
||||
delayShow={1000}
|
||||
hidden={activeGlobalEnvironment.name?.length < 7}
|
||||
>
|
||||
<span className="env-text max-w-24 truncate overflow-hidden">{activeGlobalEnvironment.name}</span>
|
||||
</ToolHint>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className="env-text-inactive max-w-36 truncate no-wrap">No environments</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="current-environment flex items-center justify-center pl-3 pr-2 py-1 select-none">
|
||||
<p className="text-nowrap truncate max-w-32">{activeEnvironment ? activeEnvironment.name : 'No Environment'}</p>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`current-environment flex align-center justify-center cursor-pointer bg-transparent ${
|
||||
!hasAnyEnv ? 'no-environments' : ''
|
||||
}`}
|
||||
data-testid="environment-selector-trigger"
|
||||
>
|
||||
{displayContent}
|
||||
<IconCaretDown className="caret" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleSettingsIconClick = () => {
|
||||
setOpenSettingsModal(true);
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(true));
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setOpenSettingsModal(false);
|
||||
dispatch(updateEnvironmentSettingsModalVisibility(false));
|
||||
};
|
||||
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const onSelect = (environment) => {
|
||||
dispatch(selectEnvironment(environment ? environment.uid : null, collection.uid))
|
||||
.then(() => {
|
||||
if (environment) {
|
||||
toast.success(`Environment changed to ${environment.name}`);
|
||||
} else {
|
||||
toast.success(`No Environments are active now`);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('An error occurred while selecting the environment'));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center cursor-pointer environment-selector">
|
||||
<StyledWrapper width={dropdownWidth}>
|
||||
<div className="environment-selector flex align-center cursor-pointer">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div className="label-item font-medium">Collection Environments</div>
|
||||
{environments && environments.length
|
||||
? environments.map((e) => (
|
||||
<div
|
||||
className={`dropdown-item ${e?.uid === activeEnvironmentUid ? 'active' : ''}`}
|
||||
key={e.uid}
|
||||
onClick={() => {
|
||||
onSelect(e);
|
||||
dropdownTippyRef.current.hide();
|
||||
}}
|
||||
>
|
||||
<IconDatabase size={18} strokeWidth={1.5} /> <span className="ml-2 break-all">{e.name}</span>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onSelect(null);
|
||||
}}
|
||||
>
|
||||
<IconDatabaseOff size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2">No Environment</span>
|
||||
{/* Tab Headers */}
|
||||
<div className="tab-header flex p-[0.75rem]">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
className={`tab-button whitespace-nowrap pb-[0.375rem] border-b-[0.125rem] bg-transparent flex align-center cursor-pointer transition-all duration-200 mr-[1.25rem] ${
|
||||
activeTab === tab.id ? 'active' : 'inactive'
|
||||
}`}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
data-testid={`env-tab-${tab.id}`}
|
||||
>
|
||||
<span className="tab-content-wrapper">
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="dropdown-item border-top" onClick={() => {
|
||||
handleSettingsIconClick();
|
||||
dropdownTippyRef.current.hide();
|
||||
}}>
|
||||
<div className="pr-2 text-gray-600">
|
||||
<IconSettings size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
<span>Configure</span>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="tab-content">
|
||||
<EnvironmentListContent
|
||||
environments={activeTab === 'collection' ? environments : globalEnvironments}
|
||||
activeEnvironmentUid={activeTab === 'collection' ? activeEnvironmentUid : activeGlobalEnvironmentUid}
|
||||
description={description}
|
||||
onEnvironmentSelect={handleEnvironmentSelect}
|
||||
onSettingsClick={handleSettingsClick}
|
||||
onCreateClick={handleCreateClick}
|
||||
onImportClick={handleImportClick}
|
||||
/>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
{openSettingsModal && <EnvironmentSettings collection={collection} onClose={handleModalClose} />}
|
||||
|
||||
{/* Modals - Rendered outside dropdown to avoid conflicts */}
|
||||
{showGlobalSettings && (
|
||||
<GlobalEnvironmentSettings
|
||||
globalEnvironments={globalEnvironments}
|
||||
collection={collection}
|
||||
activeGlobalEnvironmentUid={activeGlobalEnvironmentUid}
|
||||
onClose={handleCloseSettings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showCollectionSettings && <EnvironmentSettings collection={collection} onClose={handleCloseSettings} />}
|
||||
|
||||
{showCreateGlobalModal && (
|
||||
<CreateGlobalEnvironment
|
||||
onClose={() => setShowCreateGlobalModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
setShowGlobalSettings(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showImportGlobalModal && (
|
||||
<ImportEnvironmentModal
|
||||
type="global"
|
||||
onClose={() => setShowImportGlobalModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
setShowGlobalSettings(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showCreateCollectionModal && (
|
||||
<CreateEnvironment
|
||||
collection={collection}
|
||||
onClose={() => setShowCreateCollectionModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
setShowCollectionSettings(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showImportCollectionModal && (
|
||||
<ImportEnvironmentModal
|
||||
type="collection"
|
||||
collection={collection}
|
||||
onClose={() => setShowImportCollectionModal(false)}
|
||||
onEnvironmentCreated={() => {
|
||||
setShowCollectionSettings(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user