mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
425 Commits
fix/workfl
...
v2.6.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aebc8241cc | ||
|
|
0eda1b761d | ||
|
|
a05f7cb686 | ||
|
|
745a71700c | ||
|
|
ac9c190b41 | ||
|
|
1a1a230a1e | ||
|
|
b2e02b7762 | ||
|
|
9cbfeccbed | ||
|
|
4725300c41 | ||
|
|
f2aedf780d | ||
|
|
f03047a2f9 | ||
|
|
a7ba23d97e | ||
|
|
2521e980ea | ||
|
|
1c118fa04a | ||
|
|
b6fb5e02d4 | ||
|
|
5313704d84 | ||
|
|
b147f14fef | ||
|
|
66fe1528df | ||
|
|
a598cda624 | ||
|
|
e1c12ea699 | ||
|
|
9801e91720 | ||
|
|
364fb45e97 | ||
|
|
5c9981aca2 | ||
|
|
fc697bf81b | ||
|
|
9bc07afc77 | ||
|
|
e4ae857df3 | ||
|
|
3d26833b8a | ||
|
|
1089a52171 | ||
|
|
9dde2df475 | ||
|
|
1cc94e8ffe | ||
|
|
223f79a3e2 | ||
|
|
5dc6f6757d | ||
|
|
e20fe790a6 | ||
|
|
a006fe8230 | ||
|
|
577d54b432 | ||
|
|
afaebf6b3d | ||
|
|
6e89001825 | ||
|
|
cb611c6510 | ||
|
|
e7dd78ea53 | ||
|
|
9ad0f2d169 | ||
|
|
bf19645282 | ||
|
|
bb01199877 | ||
|
|
5627c5624f | ||
|
|
8e23a7054f | ||
|
|
d820069371 | ||
|
|
6f9daadcfb | ||
|
|
2de9b87c6f | ||
|
|
8d5d952026 | ||
|
|
178773d63a | ||
|
|
7994946c85 | ||
|
|
b020255269 | ||
|
|
afb2d3dffd | ||
|
|
73b0f0919d | ||
|
|
8975b9eef6 | ||
|
|
865e813b42 | ||
|
|
51f36b1903 | ||
|
|
6b122d7262 | ||
|
|
9f1aed3209 | ||
|
|
ce1110bdd4 | ||
|
|
788569a5f4 | ||
|
|
91397eaf57 | ||
|
|
c293ceefcf | ||
|
|
a8e5ce9c13 | ||
|
|
8ac916b0ff | ||
|
|
8d860a051c | ||
|
|
256f63dd38 | ||
|
|
0948964677 | ||
|
|
1b52bb27f7 | ||
|
|
4ac2c4ac34 | ||
|
|
7c27193983 | ||
|
|
2c3d2ff6a7 | ||
|
|
a4fff01647 | ||
|
|
2cd985faf7 | ||
|
|
9a35302d4b | ||
|
|
553f7675f2 | ||
|
|
3e714ab9f8 | ||
|
|
f2e9a6a502 | ||
|
|
b924e15afa | ||
|
|
b0c74909ba | ||
|
|
548a6b4319 | ||
|
|
b299879b82 | ||
|
|
3696562414 | ||
|
|
e02c6c274b | ||
|
|
9c9afaf78f | ||
|
|
6cde453032 | ||
|
|
8f06889996 | ||
|
|
52662f0766 | ||
|
|
ab0a4b8140 | ||
|
|
1b268ae9db | ||
|
|
8debb9fd11 | ||
|
|
7c07488e16 | ||
|
|
6073a9e2c3 | ||
|
|
9c652f86c9 | ||
|
|
3c0090d86f | ||
|
|
9132755d49 | ||
|
|
2a44691cb3 | ||
|
|
0d8a696498 | ||
|
|
bfa2706598 | ||
|
|
5fdb52388a | ||
|
|
799dc9a1ca | ||
|
|
2bb56e8a4b | ||
|
|
084d2bf692 | ||
|
|
10640c7561 | ||
|
|
9f044c48fe | ||
|
|
5567e1b7f2 | ||
|
|
3cd18d1e16 | ||
|
|
9d3e42b5d4 | ||
|
|
0f318c26c2 | ||
|
|
79f4e69a05 | ||
|
|
f13148af3d | ||
|
|
6598d23ff0 | ||
|
|
c83436655c | ||
|
|
62595c519c | ||
|
|
d2eb2d2941 | ||
|
|
942c0ee113 | ||
|
|
fbd3a38587 | ||
|
|
45b660985e | ||
|
|
0888125899 | ||
|
|
c85d9bcd84 | ||
|
|
dbf8af1146 | ||
|
|
d7ccf1454e | ||
|
|
652d447f8b | ||
|
|
8e91640084 | ||
|
|
0ca2891166 | ||
|
|
5000bb8db3 | ||
|
|
9927424826 | ||
|
|
2f58379feb | ||
|
|
c14d3f4274 | ||
|
|
d4673a2f07 | ||
|
|
3a0c94577f | ||
|
|
5a4e33e503 | ||
|
|
5649799167 | ||
|
|
c407b73c22 | ||
|
|
361add3385 | ||
|
|
9d6ab69d37 | ||
|
|
0f6da35c0b | ||
|
|
ad3f5de99a | ||
|
|
2de7ba0d0c | ||
|
|
b699088dd6 | ||
|
|
458c070004 | ||
|
|
babac6df3c | ||
|
|
f58477931f | ||
|
|
2171d491a6 | ||
|
|
aa911f88f2 | ||
|
|
bdbcaeff67 | ||
|
|
b2756b3c63 | ||
|
|
27f11ab583 | ||
|
|
2776970317 | ||
|
|
9d28bf7e82 | ||
|
|
6455b00742 | ||
|
|
16179a3b50 | ||
|
|
6a37c9d076 | ||
|
|
1915b1c00a | ||
|
|
a9982d6e28 | ||
|
|
1daeb8fe93 | ||
|
|
3dfb158382 | ||
|
|
fb7d247fa7 | ||
|
|
6bf2312a94 | ||
|
|
0cdcb83a7a | ||
|
|
e4f48e81fc | ||
|
|
1d32a95a09 | ||
|
|
4c934a78a6 | ||
|
|
c47bc86d37 | ||
|
|
a125781312 | ||
|
|
dfa951e574 | ||
|
|
76779e6f95 | ||
|
|
e9a79a32da | ||
|
|
967170a7b2 | ||
|
|
3326784315 | ||
|
|
fc553e1009 | ||
|
|
da172ff9b5 | ||
|
|
fc422853ef | ||
|
|
2852c07ec7 | ||
|
|
ead1c9ecab | ||
|
|
5b5066577f | ||
|
|
4af0bb3943 | ||
|
|
f2eaa79318 | ||
|
|
2ee7ce5829 | ||
|
|
0d7c94e7e9 | ||
|
|
9e29821012 | ||
|
|
38c307d6f1 | ||
|
|
520567793a | ||
|
|
e0fb379511 | ||
|
|
ba9362ccb2 | ||
|
|
261a36c435 | ||
|
|
cb92e46f8d | ||
|
|
126186041e | ||
|
|
6379e1703c | ||
|
|
2b246e431b | ||
|
|
526fcabffe | ||
|
|
75ff31f0cf | ||
|
|
46dab6e474 | ||
|
|
c7e8c07d40 | ||
|
|
932d2b77dc | ||
|
|
049de84cbb | ||
|
|
3bd44ea7ef | ||
|
|
317ccccfc6 | ||
|
|
220da6b58e | ||
|
|
6a7750d354 | ||
|
|
529803f791 | ||
|
|
4c23ab5664 | ||
|
|
e3c28fd0ec | ||
|
|
56ab61c29c | ||
|
|
d3056ba843 | ||
|
|
e34e2ec1f1 | ||
|
|
524bb5e4b7 | ||
|
|
3f8ea7764e | ||
|
|
f0d1e6936e | ||
|
|
9a21eec1b9 | ||
|
|
1703346bb6 | ||
|
|
b93d8e73a2 | ||
|
|
17c9813c98 | ||
|
|
e5ebe20a20 | ||
|
|
b5861dae39 | ||
|
|
54a03fd0d3 | ||
|
|
e8affcfde9 | ||
|
|
d376947a91 | ||
|
|
59e38fbdb0 | ||
|
|
492449b7c5 | ||
|
|
7cd21636d6 | ||
|
|
6ff49589be | ||
|
|
c950806541 | ||
|
|
3dcc12042f | ||
|
|
92925648e6 | ||
|
|
811c492bce | ||
|
|
73fa2e19e4 | ||
|
|
921e1af72b | ||
|
|
cc905da630 | ||
|
|
74bbfce8a0 | ||
|
|
8b67a0423d | ||
|
|
f1d527aa9c | ||
|
|
9e45d4d227 | ||
|
|
2dd0424d8f | ||
|
|
838f25b9db | ||
|
|
8809469f8e | ||
|
|
289f138c2a | ||
|
|
3d0dd60f56 | ||
|
|
9bb9a914ac | ||
|
|
44cef9999c | ||
|
|
3a792a021c | ||
|
|
2e5c63cfb9 | ||
|
|
9845363349 | ||
|
|
1a6fa7a799 | ||
|
|
6cd44662a8 | ||
|
|
9daf418886 | ||
|
|
37ee13353d | ||
|
|
8439e8871f | ||
|
|
4c1d3b4f3a | ||
|
|
cd3c66cb14 | ||
|
|
265b0114e4 | ||
|
|
17a63d599d | ||
|
|
d9e87fcd82 | ||
|
|
78c4cb11eb | ||
|
|
6feea75e45 | ||
|
|
2d1f7d0f33 | ||
|
|
841facc853 | ||
|
|
0e60bd3da7 | ||
|
|
5dc7f1ae2f | ||
|
|
6862cb4e58 | ||
|
|
0591530d44 | ||
|
|
592679538b | ||
|
|
9ef2699372 | ||
|
|
e4c37b916a | ||
|
|
7a8a0ae37e | ||
|
|
bd25097e44 | ||
|
|
3f140e818f | ||
|
|
dbba22131c | ||
|
|
8908828af0 | ||
|
|
20c9e1d406 | ||
|
|
741576526d | ||
|
|
b928ec112e | ||
|
|
559026b65c | ||
|
|
eb6fef63b3 | ||
|
|
1b767f8d26 | ||
|
|
3b061cda89 | ||
|
|
e0d7bb50f3 | ||
|
|
20e8e9167f | ||
|
|
268ede869d | ||
|
|
8b8ddaf31b | ||
|
|
f16fbeade7 | ||
|
|
d27677030d | ||
|
|
578e912faf | ||
|
|
bd8c6caa39 | ||
|
|
7635230c88 | ||
|
|
eb0d746082 | ||
|
|
61b3853390 | ||
|
|
76485cdd56 | ||
|
|
3d5401a8db | ||
|
|
6faecc2874 | ||
|
|
4650ca40c1 | ||
|
|
14ee26557a | ||
|
|
8e873013a9 | ||
|
|
b0caf46406 | ||
|
|
a0926c4064 | ||
|
|
b795b1c5ce | ||
|
|
61ba5f5c39 | ||
|
|
f177287fb6 | ||
|
|
ef929e78f3 | ||
|
|
274a55e257 | ||
|
|
9e24223085 | ||
|
|
3cd3d8bdcc | ||
|
|
eed3f2ff4c | ||
|
|
b09b4b1d17 | ||
|
|
cd9c667e8a | ||
|
|
2675e79dbd | ||
|
|
ef94f55c82 | ||
|
|
926919524b | ||
|
|
087f691544 | ||
|
|
3a81ebf0e2 | ||
|
|
3ffaaab8f3 | ||
|
|
5a98da2031 | ||
|
|
2e4014863f | ||
|
|
ccd4a14da6 | ||
|
|
98bd997665 | ||
|
|
a7cf24278e | ||
|
|
039c157f33 | ||
|
|
1009d42f92 | ||
|
|
1be0e8d31c | ||
|
|
ab9befd773 | ||
|
|
7506f83800 | ||
|
|
74d9b0aafe | ||
|
|
d3fcb42a8f | ||
|
|
3808089e60 | ||
|
|
cd2f5d5233 | ||
|
|
51be153527 | ||
|
|
5728b7c8a8 | ||
|
|
71b6907c31 | ||
|
|
eead96ca26 | ||
|
|
f99e8770f0 | ||
|
|
7ae33d05c9 | ||
|
|
df196f5d25 | ||
|
|
1f8a10d1df | ||
|
|
ccb951dadd | ||
|
|
9bde3c44f7 | ||
|
|
5ac52a531f | ||
|
|
51e60d5083 | ||
|
|
01b982a0e7 | ||
|
|
d0b16841c9 | ||
|
|
59d7141f70 | ||
|
|
11c14530eb | ||
|
|
0fb926648b | ||
|
|
d37cf28e10 | ||
|
|
f8a14e35fa | ||
|
|
eefb0f836b | ||
|
|
989e553648 | ||
|
|
cd1d4f09d2 | ||
|
|
122e0a1d02 | ||
|
|
7e16304426 | ||
|
|
0888219e95 | ||
|
|
d89fd455ff | ||
|
|
df4a682f97 | ||
|
|
2385c4d5c1 | ||
|
|
243398bcd0 | ||
|
|
2a2f2dfa15 | ||
|
|
d57634e6ea | ||
|
|
1be0b97895 | ||
|
|
6a85635c49 | ||
|
|
0fbbe8a996 | ||
|
|
4ff4e3b732 | ||
|
|
7c65317b07 | ||
|
|
b57c996564 | ||
|
|
5de75892a2 | ||
|
|
d5e828aef2 | ||
|
|
51c86bc0e9 | ||
|
|
253cb8b315 | ||
|
|
0876ad0dab | ||
|
|
a1c133b303 | ||
|
|
38cf206075 | ||
|
|
9d598db55e | ||
|
|
f6ab59ceda | ||
|
|
8cda05c431 | ||
|
|
7af7ff92bf | ||
|
|
3169e6cdf4 | ||
|
|
f1004e2e36 | ||
|
|
233c57e625 | ||
|
|
b399576dab | ||
|
|
655eec09c1 | ||
|
|
51eda3f08c | ||
|
|
a438c06b97 | ||
|
|
4977dbeb11 | ||
|
|
7cacc255b4 | ||
|
|
b28b60d4a7 | ||
|
|
2fc45de430 | ||
|
|
c58604716e | ||
|
|
31409c6206 | ||
|
|
4e88cbf318 | ||
|
|
dfb0b1b966 | ||
|
|
f8b4a0b85b | ||
|
|
200732bac5 | ||
|
|
c997924c42 | ||
|
|
711eb24c01 | ||
|
|
3b8a613914 | ||
|
|
81930f6fe6 | ||
|
|
e0750148e6 | ||
|
|
528f822294 | ||
|
|
31c11830a6 | ||
|
|
eff3b29bfb | ||
|
|
4656958f2f | ||
|
|
aa4575b0ea | ||
|
|
14d798eea7 | ||
|
|
8af285d41e | ||
|
|
6a0eb1ccde | ||
|
|
4a613ed1b7 | ||
|
|
7566d658d4 | ||
|
|
413b121ce1 | ||
|
|
90dff3d1e1 | ||
|
|
3fc0b0a668 | ||
|
|
70ee819bae | ||
|
|
b5e53ec25c | ||
|
|
26eaec4c72 | ||
|
|
01a62d66cc | ||
|
|
f668e93f52 | ||
|
|
d0419edb92 | ||
|
|
c5eeb190d3 | ||
|
|
1d1e701ccb | ||
|
|
f38c7ae03a | ||
|
|
8f754142c7 | ||
|
|
3bd8f09c88 | ||
|
|
dd9cb21f8c | ||
|
|
2064cc88ab | ||
|
|
d982e35a17 | ||
|
|
4afcd44216 | ||
|
|
63252d3ee2 | ||
|
|
22a9502976 | ||
|
|
f972733426 |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @helloanoop @maintainer-bruno @lohit-bruno @naman-bruno
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
||||
github: helloanoop
|
||||
44
.github/ISSUE_TEMPLATE/BugReport.yaml
vendored
44
.github/ISSUE_TEMPLATE/BugReport.yaml
vendored
@@ -6,26 +6,60 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
Before submitting, please make sure you've searched existing issues:
|
||||
👉 [Search existing issues](https://github.com/usebruno/bruno/issues?q=is%3Aissue)
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'I have checked the following:'
|
||||
options:
|
||||
- label: I use the newest version of bruno.
|
||||
required: true
|
||||
- label: I've searched existing issues and found nothing related to my issue.
|
||||
- label: "I have searched existing issues and found nothing related to my issue."
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'This bug is:'
|
||||
options:
|
||||
- label: making Bruno unusable for me
|
||||
required: false
|
||||
- label: slowing me down but I'm able to continue working
|
||||
required: false
|
||||
- label: annoying
|
||||
required: false
|
||||
- label: this feature was working in a previous version but is broken in the current release.
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Bruno version
|
||||
description: Please specify the version of Bruno you are using in which the issue occurs.
|
||||
placeholder: 1.38.1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: Information about the operating system the issue occurs on.
|
||||
placeholder: Windows 11 26100.3037 / macOS 15.1 (24B83) / Linux 6.13.1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of the bug.
|
||||
description: A clear and concise description of the bug and how it's effecting your work along with steps to reproduce.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: .bru file to reproduce the bug
|
||||
description: Attach your .bru file here that can reqroduce the problem.
|
||||
description: Attach your .bru file here that can reproduce the problem.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots/Live demo link
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/FeatureRequest.yaml
vendored
14
.github/ISSUE_TEMPLATE/FeatureRequest.yaml
vendored
@@ -8,13 +8,23 @@ body:
|
||||
options:
|
||||
- label: I've searched existing issues and found nothing related to my issue.
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'This feature'
|
||||
options:
|
||||
- label: blocks me from using Bruno
|
||||
required: false
|
||||
- label: would improve my quality of life in Bruno
|
||||
required: false
|
||||
- label: is something I've never seen an API client do before
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Suggest an idea for this project.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature you want to add
|
||||
label: Describe the feature you want to add, and how it would change your usage of Bruno
|
||||
description: A clear and concise description of the feature you want to be added.
|
||||
validations:
|
||||
required: true
|
||||
@@ -23,4 +33,4 @@ body:
|
||||
label: Mockups or Images of the feature
|
||||
description: Add some images to support your feature.
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
|
||||
31
.github/dependabot.yml
vendored
Normal file
31
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
groups:
|
||||
bruno-dependencies:
|
||||
patterns:
|
||||
- "*usebruno*"
|
||||
babel-dependencies:
|
||||
patterns:
|
||||
- "*babel*"
|
||||
fortawesome-dependencies:
|
||||
patterns:
|
||||
- "*fortawesome*"
|
||||
electron-dependencies:
|
||||
patterns:
|
||||
- "*electron*"
|
||||
rollup-dependencies:
|
||||
patterns:
|
||||
- "*rollup*"
|
||||
jest-dependencies:
|
||||
patterns:
|
||||
- "*jest*"
|
||||
4
.github/workflows/npm-bru-cli.yml
vendored
4
.github/workflows/npm-bru-cli.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
bru run --env Prod --output junit.xml --format junit
|
||||
|
||||
- name: Publish Test Report
|
||||
uses: dorny/test-reporter@v1
|
||||
uses: dorny/test-reporter@v2
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: Test Report
|
||||
|
||||
51
.github/workflows/tests.yml
vendored
51
.github/workflows/tests.yml
vendored
@@ -28,6 +28,11 @@ jobs:
|
||||
npm run build --workspace=packages/bruno-common
|
||||
npm run build --workspace=packages/bruno-query
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
|
||||
- name: Lint Check
|
||||
run: npm run lint
|
||||
|
||||
# tests
|
||||
- name: Test Package bruno-js
|
||||
@@ -45,6 +50,8 @@ jobs:
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
- name: Test Package bruno-common
|
||||
run: npm run test --workspace=packages/bruno-common
|
||||
- name: Test Package bruno-converters
|
||||
run: npm run test --workspace=packages/bruno-converters
|
||||
- name: Test Package bruno-electron
|
||||
run: npm run test --workspace=packages/bruno-electron
|
||||
|
||||
@@ -71,6 +78,8 @@ jobs:
|
||||
npm run build --workspace=packages/bruno-query
|
||||
npm run build --workspace=packages/bruno-common
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
@@ -82,5 +91,47 @@ jobs:
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
if: always()
|
||||
with:
|
||||
check_name: CLI Test Results
|
||||
files: packages/bruno-tests/collection/junit.xml
|
||||
comment_mode: always
|
||||
e2e-test:
|
||||
name: Playwright E2E Tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: v22.11.x
|
||||
- name: Install dependencies
|
||||
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
|
||||
npm ci --legacy-peer-deps
|
||||
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
|
||||
|
||||
- name: Install dependencies for test collection environment
|
||||
run: |
|
||||
npm ci --prefix packages/bruno-tests/collection
|
||||
|
||||
- name: Build libraries
|
||||
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
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
xvfb-run npm run test:e2e
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -46,4 +46,8 @@ yarn-error.log*
|
||||
|
||||
#dev editor
|
||||
bruno.iml
|
||||
.idea
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# Playwright
|
||||
/blob-report/
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB |
@@ -15,15 +15,15 @@
|
||||
| [正體中文](docs/contributing/contributing_zhtw.md)
|
||||
| [日本語](docs/contributing/contributing_ja.md)
|
||||
| [हिंदी](docs/contributing/contributing_hi.md)
|
||||
| [Nederlands](docs/contributing/contributing_nl.md)
|
||||
| [Dutch](docs/contributing/contributing_nl.md)
|
||||
|
||||
## Let's make Bruno better, together!!
|
||||
|
||||
We are happy that you are looking to improve Bruno. Below are the guidelines to get started bringing up Bruno on your computer.
|
||||
We are happy that you are looking to improve Bruno. Below are the guidelines to run Bruno on your computer.
|
||||
|
||||
### Technology Stack
|
||||
|
||||
Bruno is built using Next.js and React. We also use electron to ship a desktop version (that supports local collections)
|
||||
Bruno is built using React and Electron.
|
||||
|
||||
Libraries we use
|
||||
|
||||
@@ -37,38 +37,76 @@ Libraries we use
|
||||
- Filesystem Watcher - chokidar
|
||||
- i18n - i18next
|
||||
|
||||
### Dependencies
|
||||
|
||||
You would need [Node v20.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
|
||||
> [!IMPORTANT]
|
||||
> You would need [Node v22.x or the latest LTS version](https://nodejs.org/en/). We use npm workspaces in the project
|
||||
|
||||
## Development
|
||||
|
||||
Bruno is being developed as a desktop app. You need to load the app by running the Next.js app in one terminal and then run the electron app in another terminal.
|
||||
Bruno is a desktop app. Below are the instructions to run Bruno.
|
||||
|
||||
### Local Development
|
||||
> Note: We use React for the frontend and rsbuild for build and dev server.
|
||||
|
||||
## Install Dependencies
|
||||
|
||||
```bash
|
||||
# use nodejs 20 version
|
||||
# use nodejs 22 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
npm i --legacy-peer-deps
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
#### Build packages
|
||||
|
||||
##### Option 1
|
||||
|
||||
```bash
|
||||
# build packages
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# bundle js sandbox libraries
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
```
|
||||
##### Option 2
|
||||
|
||||
# run next app (terminal 1)
|
||||
```bash
|
||||
# install dependencies and setup
|
||||
npm run setup
|
||||
```
|
||||
|
||||
#### Run the app
|
||||
|
||||
##### Option 1
|
||||
|
||||
```bash
|
||||
# run react app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# run electron app (terminal 2)
|
||||
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
|
||||
|
||||
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
|
||||
@@ -86,11 +124,32 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
# run bruno-schema tests
|
||||
npm run test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# run bruno-query tests
|
||||
npm run test --workspace=packages/bruno-query
|
||||
|
||||
# run bruno-common tests
|
||||
npm run test --workspace=packages/bruno-common
|
||||
|
||||
# run bruno-converters tests
|
||||
npm run test --workspace=packages/bruno-converters
|
||||
|
||||
# run bruno-app tests
|
||||
npm run test --workspace=packages/bruno-app
|
||||
|
||||
# run bruno-electron tests
|
||||
npm run test --workspace=packages/bruno-electron
|
||||
|
||||
# run bruno-lang tests
|
||||
npm run test --workspace=packages/bruno-lang
|
||||
|
||||
# run bruno-toml tests
|
||||
npm run test --workspace=packages/bruno-toml
|
||||
|
||||
# run tests over all workspaces
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Raising Pull Requests
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testing (পরীক্ষা)
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# ব্রুনো-স্কিমা পরীক্ষা চালান
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# সমস্ত কর্মক্ষেত্রে পরীক্ষা চালান
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Raising Pull Request (পুল অনুরোধ উত্থাপন)
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# 运行 bruno-schema 测试
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# 在所有工作区上运行测试
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### 提交 Pull Request
|
||||
|
||||
@@ -21,7 +21,7 @@ Bibliotheken die wir benutzen
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
Du benötigst [Node v20.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
|
||||
Du benötigst [Node v22.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
|
||||
|
||||
### Lass uns coden
|
||||
|
||||
@@ -42,12 +42,12 @@ Bruno wird als Desktop-Anwendung entwickelt. Um die App zu starten, musst Du zue
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- NodeJS v18
|
||||
- NodeJS v22
|
||||
|
||||
### Lokales Entwickeln
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
# use nodejs 22 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testen
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# Führen Sie Bruno-Schema-Tests aus
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# Führen Sie Tests für alle Arbeitsbereiche durch
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Pruebas
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# ejecutar pruebas de esquema bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# ejecutar pruebas en todos los espacios de trabajo
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Crea un Pull Request
|
||||
|
||||
@@ -73,11 +73,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# exécuter des tests de schéma bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# exécuter des tests sur tous les espaces de travail
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Ouvrir une Pull Request
|
||||
|
||||
@@ -40,6 +40,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# Next.js ऐप चलाएँ (टर्मिनल 1 पर)
|
||||
npm run dev:web
|
||||
@@ -65,11 +67,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### परिक्षण
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# ब्रूनो-स्कीमा परीक्षण चलाएँ
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# सभी कार्यस्थानों पर परीक्षण चलाएँ
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### पुल अनुरोध प्रक्रिया
|
||||
|
||||
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# esegui i test dello schema bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# esegui test su tutti gli spazi di lavoro
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
@@ -40,6 +40,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# run next app (terminal 1)
|
||||
npm run dev:web
|
||||
@@ -65,11 +67,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### テストを動かすには
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# ブルーノスキーマのテストを実行します
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# すべてのワークスペースでテストを実行します
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### プルリクエストの手順
|
||||
|
||||
@@ -40,6 +40,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# next 앱 실행 (1번 터미널)
|
||||
npm run dev:web
|
||||
@@ -66,11 +68,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### 테스팅
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# bruno-schema 테스트 실행
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# 모든 작업 공간에서 테스트 실행
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Pull Requests 요청
|
||||
|
||||
@@ -40,6 +40,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# draai next app (terminal 1)
|
||||
npm run dev:web
|
||||
@@ -65,11 +67,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testen
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# voer bruno-schema tests uit
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# voer tests uit over alle werkruimten
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Pull Requests indienen
|
||||
|
||||
@@ -71,11 +71,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testowanie
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# uruchom testy bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# uruchom testy we wszystkich przestrzeniach roboczych
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Tworzenie Pull Request
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testando
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# executar testes do bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# executar testes em todos os ambientes de trabalho
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Envio de Pull Request
|
||||
|
||||
@@ -64,11 +64,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testarea
|
||||
|
||||
```shell
|
||||
# bruno-schema
|
||||
# executați teste bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# executați teste peste toate spațiile de lucru
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Crearea unui Pull Request
|
||||
|
||||
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Тестирование
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# запустите тесты bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# запустите тесты во всех рабочих пространствах
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
@@ -42,6 +42,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# spustite ďalšiu aplikáciu (terminál 1)
|
||||
npm run dev:web
|
||||
@@ -67,11 +69,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testovanie
|
||||
|
||||
````bash
|
||||
# bruno-schema
|
||||
# spustiť bruno-schema testy
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# spustiť testy vo všetkých pracovných priestoroch
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Vyrobenie Pull Request
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Test
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# bruno-schema testlerini çalıştır
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# tüm çalışma alanlarında testleri çalıştır
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Pull Request Oluşturma
|
||||
|
||||
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Тестування
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# запустити тести bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# запустити тести у всіх робочих просторах
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### 測試
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# 執行布魯諾架構測試
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# 對所有工作區執行測試
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### 發送 Pull Request
|
||||
|
||||
151
docs/readme/readme_hi.md
Normal file
151
docs/readme/readme_hi.md
Normal file
@@ -0,0 +1,151 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### ब्रूनो - API इंटरफेस (API) का अन्वेषण और परीक्षण करने के लिए एक ओपन-सोर्स विकास वातावरण।
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%2Fbruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
[English](../../readme.md)
|
||||
| [Українська](./readme_ua.md)
|
||||
| [Русский](./readme_ru.md)
|
||||
| [Türkçe](./readme_tr.md)
|
||||
| [Deutsch](./readme_de.md)
|
||||
| [Français](./readme_fr.md)
|
||||
| [Português (BR)](./readme_pt_br.md)
|
||||
| [한국어](./readme_kr.md)
|
||||
| [বাংলা](./readme_bn.md)
|
||||
| [Español](./readme_es.md)
|
||||
| [Italiano](./readme_it.md)
|
||||
| [Română](./readme_ro.md)
|
||||
| [Polski](./readme_pl.md)
|
||||
| [简体中文](./readme_cn.md)
|
||||
| [正體中文](./readme_zhtw.md)
|
||||
| [العربية](./readme_ar.md)
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
| **हिन्दी**
|
||||
|
||||
ब्रूनो एक नया और अभिनव API क्लाइंट है, जिसका उद्देश्य Postman और अन्य समान उपकरणों द्वारा प्रस्तुत स्थिति को बदलना है।
|
||||
|
||||
ब्रूनो आपकी कलेक्शनों को सीधे आपकी फाइल सिस्टम के एक फ़ोल्डर में संग्रहीत करता है। हम API अनुरोधों के बारे में जानकारी सहेजने के लिए एक सामान्य टेक्स्ट मार्कअप भाषा, Bru, का उपयोग करते हैं।
|
||||
|
||||
आप अपनी API कलेक्शनों पर सहयोग करने के लिए Git या अपनी पसंद के किसी भी संस्करण नियंत्रण प्रणाली का उपयोग कर सकते हैं।
|
||||
|
||||
ब्रूनो केवल ऑफ़लाइन उपयोग के लिए है। ब्रूनो में कभी भी क्लाउड-सिंक जोड़ने की कोई योजना नहीं है। हम आपके डेटा की गोपनीयता को महत्व देते हैं और मानते हैं कि इसे आपके डिवाइस पर ही रहना चाहिए। हमारी दीर्घकालिक दृष्टि [यहाँ](https://github.com/usebruno/bruno/discussions/269) पढ़ें।
|
||||
|
||||
📢 हमारे हालिया India FOSS 3.0 सम्मेलन में हमारे वार्तालाप को [यहाँ](https://www.youtube.com/watch?v=7bSMFpbcPiY) देखें।
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### गोल्डन संस्करण ✨
|
||||
|
||||
हमारी अधिकांश सुविधाएँ मुफ्त और ओपन-सोर्स हैं।
|
||||
हम [पारदर्शिता और स्थिरता के सिद्धांतों](https://github.com/usebruno/bruno/discussions/269) के बीच एक सामंजस्यपूर्ण संतुलन प्राप्त करने का प्रयास करते हैं।
|
||||
|
||||
[गोल्डन संस्करण](https://www.usebruno.com/pricing) के लिए खरीदारी जल्द ही $9 की कीमत पर उपलब्ध होगी! <br/>
|
||||
[यहाँ सदस्यता लें](https://usebruno.ck.page/4c65576bd4) ताकि आपको लॉन्च पर सूचनाएं मिलें।
|
||||
|
||||
### स्थापना
|
||||
|
||||
ब्रूनो Mac, Windows और Linux के लिए हमारे [वेबसाइट](https://www.usebruno.com/downloads) पर एक बाइनरी डाउनलोड के रूप में उपलब्ध है।
|
||||
|
||||
आप ब्रूनो को Homebrew, Chocolatey, Scoop, Snap, Flatpak और Apt जैसे पैकेज प्रबंधकों के माध्यम से भी स्थापित कर सकते हैं।
|
||||
|
||||
```sh
|
||||
# Mac पर Homebrew के माध्यम से
|
||||
brew install bruno
|
||||
|
||||
# Windows पर Chocolatey के माध्यम से
|
||||
choco install bruno
|
||||
|
||||
# Windows पर Scoop के माध्यम से
|
||||
scoop bucket add extras
|
||||
scoop install bruno
|
||||
|
||||
# Linux पर Snap के माध्यम से
|
||||
snap install bruno
|
||||
|
||||
# Linux पर Flatpak के माध्यम से
|
||||
flatpak install com.usebruno.Bruno
|
||||
|
||||
# Linux पर Apt के माध्यम से
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo gpg --no-default-keyring --keyring /etc/apt/keyrings/bruno.gpg --keyserver keyserver.ubuntu.com --recv-keys 9FA6017ECABE0266
|
||||
|
||||
echo "deb [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 />
|
||||
|
||||
Git के माध्यम से सहयोग करें 👩💻🧑💻
|
||||
या अपनी पसंद के किसी भी संस्करण नियंत्रण प्रणाली का उपयोग करें
|
||||
|
||||
<br /><br />
|
||||
|
||||
महत्वपूर्ण लिंक 📌
|
||||
हमारी दीर्घकालिक दृष्टि
|
||||
|
||||
रोडमैप
|
||||
|
||||
प्रलेखन
|
||||
|
||||
Stack Overflow
|
||||
|
||||
वेबसाइट
|
||||
|
||||
मूल्य निर्धारण
|
||||
|
||||
डाउनलोड
|
||||
|
||||
GitHub प्रायोजक
|
||||
|
||||
प्रस्तुतियाँ 🎥
|
||||
प्रशंसापत्र
|
||||
|
||||
ज्ञान केंद्र
|
||||
|
||||
Scriptmania
|
||||
|
||||
समर्थन ❤️
|
||||
यदि आप ब्रूनो को पसंद करते हैं और हमारे ओपन-सोर्स कार्य का समर्थन करना चाहते हैं, तो कृपया GitHub प्रायोजक के माध्यम से हमें प्रायोजित करने पर विचार करें।
|
||||
|
||||
प्रशंसापत्र साझा करें 📣
|
||||
यदि ब्रूनो ने आपके और आपकी टीमों के लिए काम में मदद की है, तो कृपया हमारे GitHub चर्चा में अपने प्रशंसापत्र साझा करना न भूलें
|
||||
|
||||
नए पैकेज प्रबंधकों में प्रकाशित करना
|
||||
अधिक जानकारी के लिए कृपया यहाँ देखें।
|
||||
|
||||
हमसे संपर्क करें 🌐
|
||||
𝕏 (ट्विटर) <br />
|
||||
वेबसाइट <br />
|
||||
डिस्कॉर्ड <br />
|
||||
लिंक्डइन
|
||||
|
||||
ट्रेडमार्क
|
||||
नाम
|
||||
|
||||
ब्रूनो एक ट्रेडमार्क है जो अनूप एम डी के स्वामित्व में है।
|
||||
|
||||
लोगो
|
||||
|
||||
लोगो OpenMoji से लिया गया है। लाइसेंस: CC BY-SA 4.0
|
||||
|
||||
योगदान 👩💻🧑💻
|
||||
हमें खुशी है कि आप ब्रूनो को बेहतर बनाने में रुचि रखते हैं। कृपया योगदान गाइड देखें।
|
||||
|
||||
यदि आप सीधे कोड के माध्यम से योगदान नहीं कर सकते, तो भी कृपया बग्स की रिपोर्ट करने और उन सुविधाओं का अनुरोध करने में संकोच न करें जिन्हें आपकी स्थिति को हल करने के लिए लागू किया जाना चाहिए।
|
||||
|
||||
लेखक
|
||||
<div align="center"> <a href="https://github.com/usebruno/bruno/graphs/contributors"> <img src="https://contrib.rocks/image?repo=usebruno/bruno" /> </a> </div>
|
||||
|
||||
लाइसेंस 📄
|
||||
MIT
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
|
||||
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman.
|
||||
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статусy кво, запровадженого інструментами на кшталт Postman.
|
||||
|
||||
Bruno зберігає ваші колекції напряму у теці на вашому диску. Він використовує текстову мову розмітки Bru для збереження інформації про ваші API запити.
|
||||
|
||||
Ви можете використовувати git або будь-яку іншу систему контролю версій щоб спільно працювати над вашими колекціями API запитів.
|
||||
|
||||
Bruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Взнати більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269)
|
||||
Bruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Дізнатись більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
@@ -69,13 +69,13 @@ Bruno є повністю автономним. Немає жодних план
|
||||
|
||||
### Поділитись відгуками 📣
|
||||
|
||||
Якщо Bruno допоміг вам у вашій роботі і вашим командам, будь ласка не забудьте поділитись вашими [відгуками у github дискусії](https://github.com/usebruno/bruno/discussions/343)
|
||||
Якщо Bruno допоміг у роботі вам або вашій команді, будь ласка не забудьте поділитись вашими [відгуками у github дискусії](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
### Зробити свій внесок 👩💻🧑💻
|
||||
|
||||
Я радий що ви бажаєте покращити Bruno. Будь ласка переглянте [інструкцію по контрибуції](../contributing/contributing_ua.md)
|
||||
|
||||
Навіть якщо ви не можете зробити свій внесок пишучи програмний код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі.
|
||||
Навіть якщо ви не можете зробити свій внесок пишучи код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі.
|
||||
|
||||
### Автори
|
||||
|
||||
|
||||
5
e2e-tests/001-sanity-tests/001-home-screen.spec.ts
Normal file
5
e2e-tests/001-sanity-tests/001-home-screen.spec.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Check if the logo on top left is visible', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible();
|
||||
});
|
||||
31
e2e-tests/001-sanity-tests/002-create-new-collection.spec.ts
Normal file
31
e2e-tests/001-sanity-tests/002-create-new-collection.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
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 ModeBETA').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.getByPlaceholder('Request URL').click();
|
||||
await page.getByPlaceholder('Request URL').fill('http://localhost:8081');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.locator('pre').filter({ hasText: 'http://localhost:' }).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');
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": ["{{projectRoot}}/packages/bruno-tests/collection"]
|
||||
}
|
||||
49
e2e-tests/bruno-testbench/run-testbench-requests.spec.ts
Normal file
49
e2e-tests/bruno-testbench/run-testbench-requests.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
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 ModeBETA').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));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Should verify all support links with correct URL in preference > Support tab', async ({ page }) => {
|
||||
|
||||
// Open Preferences
|
||||
await page.getByLabel('Open Preferences').click();
|
||||
|
||||
// Verify Support tab
|
||||
await page.getByRole('tab', { name: 'Support' }).click();
|
||||
|
||||
const locator_twitter = page.getByRole('link', { name: 'Twitter' });
|
||||
expect(await locator_twitter.getAttribute('href')).toEqual('https://twitter.com/use_bruno');
|
||||
|
||||
const locator_github = page.getByRole('link', { name: 'GitHub', exact: true });
|
||||
expect(await locator_github.getAttribute('href')).toEqual('https://github.com/usebruno/bruno');
|
||||
|
||||
const locator_discord = page.getByRole('link', { name: 'Discord', exact: true });
|
||||
expect(await locator_discord.getAttribute('href')).toEqual('https://discord.com/invite/KgcZUncpjq');
|
||||
|
||||
const locator_reportissues = page.getByRole('link', { name: 'Report Issues', exact: true });
|
||||
expect(await locator_reportissues.getAttribute('href')).toEqual('https://github.com/usebruno/bruno/issues');
|
||||
|
||||
const locator_documentation = page.getByRole('link', { name: 'Documentation', exact: true });
|
||||
expect(await locator_documentation.getAttribute('href')).toEqual('https://docs.usebruno.com');
|
||||
|
||||
|
||||
});
|
||||
41
eslint.config.js
Normal file
41
eslint.config.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// eslint.config.js
|
||||
const { defineConfig } = require("eslint/config");
|
||||
const globals = require("globals");
|
||||
|
||||
module.exports = defineConfig([
|
||||
{
|
||||
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
|
||||
ignores: ["**/*.config.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.jest,
|
||||
global: false,
|
||||
require: false,
|
||||
Buffer: false,
|
||||
process: false
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-electron/**/*.{js}"],
|
||||
ignores: ["**/*.config.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
}
|
||||
]);
|
||||
8696
package-lock.json
generated
8696
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@@ -6,38 +6,49 @@
|
||||
"packages/bruno-electron",
|
||||
"packages/bruno-cli",
|
||||
"packages/bruno-common",
|
||||
"packages/bruno-converters",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-query",
|
||||
"packages/bruno-js",
|
||||
"packages/bruno-lang",
|
||||
"packages/bruno-tests",
|
||||
"packages/bruno-toml",
|
||||
"packages/bruno-graphql-docs"
|
||||
"packages/bruno-graphql-docs",
|
||||
"packages/bruno-requests"
|
||||
],
|
||||
"homepage": "https://usebruno.com",
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.14.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^9.26.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"husky": "^8.0.3",
|
||||
"globals": "^16.1.0",
|
||||
"jest": "^29.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"playwright": "^1.51.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"ts-jest": "^29.0.5"
|
||||
"ts-jest": "^29.2.6"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "node ./scripts/setup.js",
|
||||
"watch:converters": "npm run watch --workspace=packages/bruno-converters",
|
||||
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
|
||||
"dev:watch": "node ./scripts/dev-hot-reload.js",
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
|
||||
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
|
||||
"build:bruno-requests": "npm run build --workspace=packages/bruno-requests",
|
||||
"build:bruno-converters": "npm run build --workspace=packages/bruno-converters",
|
||||
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
"build:electron": "node ./scripts/build-electron.js",
|
||||
@@ -47,12 +58,18 @@
|
||||
"build:electron:deb": "./scripts/build-electron.sh deb",
|
||||
"build:electron:rpm": "./scripts/build-electron.sh rpm",
|
||||
"build:electron:snap": "./scripts/build-electron.sh snap",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report",
|
||||
"watch:common": "npm run watch --workspace=packages/bruno-common",
|
||||
"test:codegen": "node playwright/codegen.ts",
|
||||
"test:e2e": "playwright test",
|
||||
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||
"prepare": "husky install"
|
||||
"lint": "node --max_old_space_size=4096 $(npx which eslint)"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.29.5"
|
||||
"rollup": "3.29.5",
|
||||
"electron-store": {
|
||||
"conf": {
|
||||
"json-schema-typed": "8.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/bruno-app/babel.config.js
Normal file
9
packages/bruno-app/babel.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@babel/preset-env',
|
||||
['@babel/preset-react', {
|
||||
runtime: 'automatic'
|
||||
}]
|
||||
],
|
||||
plugins: ['babel-plugin-styled-components']
|
||||
};
|
||||
@@ -12,5 +12,14 @@ module.exports = {
|
||||
},
|
||||
clearMocks: true,
|
||||
moduleDirectories: ['node_modules', 'src'],
|
||||
testEnvironment: 'node'
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
'^.+\\.[jt]sx?$': 'babel-jest'
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!(nanoid|xml-formatter)/)'
|
||||
],
|
||||
testMatch: [
|
||||
'<rootDir>/src/**/*.spec.[jt]s?(x)'
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@usebruno/app",
|
||||
"version": "0.3.0",
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "rsbuild dev",
|
||||
@@ -11,7 +12,6 @@
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
"@prantlf/jsonlint": "^16.0.0",
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
@@ -24,6 +24,7 @@
|
||||
"codemirror": "5.65.2",
|
||||
"codemirror-graphql": "2.1.1",
|
||||
"cookie": "0.7.1",
|
||||
"dompurify": "^3.2.4",
|
||||
"escape-html": "^1.0.3",
|
||||
"file": "^0.2.2",
|
||||
"file-dialog": "^0.0.8",
|
||||
@@ -35,17 +36,20 @@
|
||||
"graphql-request": "^3.7.0",
|
||||
"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",
|
||||
"jshint": "^2.13.6",
|
||||
"json5": "^2.2.3",
|
||||
"jsonc-parser": "^3.2.1",
|
||||
"jsonpath-plus": "10.2.0",
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.2",
|
||||
"markdown-it-replace-link": "^1.2.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.47",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nanoid": "3.3.8",
|
||||
"path": "^0.12.7",
|
||||
@@ -68,6 +72,7 @@
|
||||
"react-redux": "^7.2.9",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
"semver": "^7.7.1",
|
||||
"strip-json-comments": "^5.0.1",
|
||||
"styled-components": "^5.3.3",
|
||||
"system": "^2.0.1",
|
||||
@@ -77,19 +82,28 @@
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.27.1",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@rsbuild/core": "^1.1.2",
|
||||
"@rsbuild/plugin-babel": "^1.0.3",
|
||||
"@rsbuild/plugin-node-polyfill": "^1.2.0",
|
||||
"@rsbuild/plugin-react": "^1.0.7",
|
||||
"@rsbuild/plugin-sass": "^1.1.0",
|
||||
"@rsbuild/plugin-styled-components": "1.1.0",
|
||||
"@testing-library/dom": "^9.3.3",
|
||||
"@testing-library/jest-dom": "^6.1.5",
|
||||
"@testing-library/react": "^14.1.2",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-jest": "^29.7.0",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
|
||||
"babel-plugin-styled-components": "^2.1.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "7.1.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^3.0.1",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"mini-css-extract-plugin": "^2.4.5",
|
||||
"postcss": "8.4.47",
|
||||
"style-loader": "^3.3.1",
|
||||
@@ -97,4 +111,4 @@
|
||||
"webpack": "^5.64.4",
|
||||
"webpack-cli": "^4.9.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export default defineConfig({
|
||||
})
|
||||
],
|
||||
source: {
|
||||
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file
|
||||
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file,
|
||||
},
|
||||
html: {
|
||||
title: 'Bruno'
|
||||
@@ -34,6 +34,16 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
ignoreWarnings: [
|
||||
(warning) => warning.message.includes('Critical dependency: the request of a dependency is an expression') && warning?.moduleDescriptor?.name?.includes('flow-parser')
|
||||
],
|
||||
// Add externals configuration to exclude Node.js libraries
|
||||
externals: {
|
||||
// List specific Node.js modules you want to exclude
|
||||
// Format: 'module-name': 'commonjs module-name'
|
||||
'worker_threads': 'commonjs worker_threads',
|
||||
// 'path': 'commonjs path'
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
62
packages/bruno-app/src/components/Accordion/index.js
Normal file
62
packages/bruno-app/src/components/Accordion/index.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import { IconChevronDown } from '@tabler/icons';
|
||||
import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrapper';
|
||||
|
||||
const AccordionContext = createContext();
|
||||
|
||||
const Accordion = ({ children, defaultIndex }) => {
|
||||
const [openIndex, setOpenIndex] = useState(defaultIndex);
|
||||
|
||||
const toggleItem = (index) => {
|
||||
setOpenIndex(openIndex === index ? null : index);
|
||||
};
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={{ openIndex, toggleItem }}>
|
||||
<div>{children}</div>
|
||||
</AccordionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = ({ index, children, ...props }) => {
|
||||
return (
|
||||
<AccordionItem {...props}>
|
||||
{React.Children.map(children, (child) => React.cloneElement(child, { index }))}
|
||||
</AccordionItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const Header = ({ index, children, ...props }) => {
|
||||
const { openIndex, toggleItem } = useContext(AccordionContext);
|
||||
const isOpen = openIndex === index;
|
||||
|
||||
return (
|
||||
<AccordionHeader onClick={() => toggleItem(index)} {...props} className={isOpen ? 'open' : ''}>
|
||||
<div className="w-full">{children}</div>
|
||||
|
||||
<IconChevronDown
|
||||
className="w-5 h-5 ml-auto"
|
||||
style={{
|
||||
transform: `rotate(${isOpen ? '180deg' : '0deg'})`,
|
||||
transition: 'transform 0.3s ease-in-out'
|
||||
}}
|
||||
/>
|
||||
</AccordionHeader>
|
||||
);
|
||||
};
|
||||
|
||||
const Content = ({ index, children, ...props }) => {
|
||||
const { openIndex } = useContext(AccordionContext);
|
||||
const isOpen = openIndex === index;
|
||||
|
||||
return (
|
||||
<AccordionContent isOpen={isOpen} {...props}>
|
||||
{children}
|
||||
</AccordionContent>
|
||||
);
|
||||
};
|
||||
|
||||
Accordion.Item = Item;
|
||||
Accordion.Header = Header;
|
||||
Accordion.Content = Content;
|
||||
export default Accordion;
|
||||
28
packages/bruno-app/src/components/Accordion/styledWrapper.js
Normal file
28
packages/bruno-app/src/components/Accordion/styledWrapper.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const AccordionItem = styled.div`
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const AccordionHeader = styled.button`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 0.75rem 1rem;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
|
||||
&.open, &:hover {
|
||||
background-color: ${(props) => props.theme.plainGrid.hoverBg};
|
||||
}
|
||||
`;
|
||||
|
||||
const AccordionContent = styled.div`
|
||||
padding: ${(props) => (props.isOpen ? '1rem' : '0')};
|
||||
max-height: ${(props) => (props.isOpen ? 'auto' : '0')};
|
||||
`;
|
||||
|
||||
export { AccordionItem, AccordionHeader, AccordionContent };
|
||||
@@ -102,6 +102,13 @@ const StyledWrapper = styled.div`
|
||||
.cm-s-default span.cm-variable {
|
||||
color: #397d13 !important;
|
||||
}
|
||||
|
||||
//matching bracket fix
|
||||
.CodeMirror-matchingbracket {
|
||||
background: #5cc0b48c !important;
|
||||
text-decoration:unset;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -31,9 +31,11 @@ if (!SERVER_RENDERED) {
|
||||
'res.body',
|
||||
'res.responseTime',
|
||||
'res.getStatus()',
|
||||
'res.getStatusText()',
|
||||
'res.getHeader(name)',
|
||||
'res.getHeaders()',
|
||||
'res.getBody()',
|
||||
'res.setBody(data)',
|
||||
'res.getResponseTime()',
|
||||
'req',
|
||||
'req.url',
|
||||
@@ -56,6 +58,7 @@ if (!SERVER_RENDERED) {
|
||||
'req.getTimeout()',
|
||||
'req.setTimeout(timeout)',
|
||||
'req.getExecutionMode()',
|
||||
'req.getName()',
|
||||
'bru',
|
||||
'bru.cwd()',
|
||||
'bru.getEnvName()',
|
||||
@@ -78,12 +81,14 @@ if (!SERVER_RENDERED) {
|
||||
'bru.getAssertionResults()',
|
||||
'bru.getTestResults()',
|
||||
'bru.sleep(ms)',
|
||||
'bru.getCollectionName()',
|
||||
'bru.getGlobalEnvVar(key)',
|
||||
'bru.setGlobalEnvVar(key, value)',
|
||||
'bru.runner',
|
||||
'bru.runner.setNextRequest(requestName)',
|
||||
'bru.runner.skipRequest()',
|
||||
'bru.runner.stopExecution()',
|
||||
'bru.interpolate(str)'
|
||||
];
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
const cursor = editor.getCursor();
|
||||
@@ -174,11 +179,21 @@ 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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
cm.execCommand('findPersistent');
|
||||
this._bindSearchHandler();
|
||||
this._appendSearchResultsCount();
|
||||
@@ -351,7 +366,7 @@ export default class CodeEditor extends React.Component {
|
||||
let variables = getAllVariables(this.props.collection, this.props.item);
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode);
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
@@ -365,6 +380,10 @@ 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
|
||||
*/
|
||||
|
||||
@@ -95,7 +95,7 @@ const AuthMode = ({ collection }) => {
|
||||
onModeChange('oauth2');
|
||||
}}
|
||||
>
|
||||
Oauth2
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
|
||||
@@ -21,12 +21,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -38,12 +38,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -55,12 +55,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -72,12 +72,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -89,12 +89,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: region,
|
||||
profileName: awsv4Auth.profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: region || '',
|
||||
profileName: awsv4Auth.profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -106,12 +106,12 @@ const AwsV4Auth = ({ collection }) => {
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: profileName
|
||||
accessKeyId: awsv4Auth.accessKeyId || '',
|
||||
secretAccessKey: awsv4Auth.secretAccessKey || '',
|
||||
sessionToken: awsv4Auth.sessionToken || '',
|
||||
service: awsv4Auth.service || '',
|
||||
region: awsv4Auth.region || '',
|
||||
profileName: profileName || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -21,8 +21,8 @@ const BasicAuth = ({ collection }) => {
|
||||
mode: 'basic',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: basicAuth.password
|
||||
username: username || '',
|
||||
password: basicAuth.password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -34,8 +34,8 @@ const BasicAuth = ({ collection }) => {
|
||||
mode: 'basic',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: basicAuth.username,
|
||||
password: password
|
||||
username: basicAuth.username || '',
|
||||
password: password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -21,8 +21,8 @@ const DigestAuth = ({ collection }) => {
|
||||
mode: 'digest',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: digestAuth.password
|
||||
username: username || '',
|
||||
password: digestAuth.password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -34,8 +34,8 @@ const DigestAuth = ({ collection }) => {
|
||||
mode: 'digest',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: digestAuth.username,
|
||||
password: password
|
||||
username: digestAuth.username || '',
|
||||
password: password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -28,9 +28,9 @@ const NTLMAuth = ({ collection }) => {
|
||||
mode: 'ntlm',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: ntlmAuth.password,
|
||||
domain: ntlmAuth.domain
|
||||
username: username || '',
|
||||
password: ntlmAuth.password || '',
|
||||
domain: ntlmAuth.domain || ''
|
||||
|
||||
}
|
||||
})
|
||||
@@ -43,9 +43,9 @@ const NTLMAuth = ({ collection }) => {
|
||||
mode: 'ntlm',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: ntlmAuth.username,
|
||||
password: password,
|
||||
domain: ntlmAuth.domain
|
||||
username: ntlmAuth.username || '',
|
||||
password: password || '',
|
||||
domain: ntlmAuth.domain || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -57,9 +57,9 @@ const NTLMAuth = ({ collection }) => {
|
||||
mode: 'ntlm',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: ntlmAuth.username,
|
||||
password: ntlmAuth.password,
|
||||
domain: domain
|
||||
username: ntlmAuth.username || '',
|
||||
password: ntlmAuth.password || '',
|
||||
domain: domain || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { clearOauth2Cache } from 'utils/network/index';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
state,
|
||||
pkce,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handlePKCEToggle = (e) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
state,
|
||||
pkce: !Boolean(oAuth?.['pkce'])
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
clearOauth2Cache(collection?.uid)
|
||||
.then(() => {
|
||||
toast.success('cleared cache successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-row w-full gap-4" key="pkce">
|
||||
<label className="block font-medium">Use PKCE</label>
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
type="checkbox"
|
||||
checked={Boolean(oAuth?.['pkce'])}
|
||||
onChange={handlePKCEToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4">
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
||||
@@ -1,33 +0,0 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'callbackUrl',
|
||||
label: 'Callback URL'
|
||||
},
|
||||
{
|
||||
key: 'authorizationUrl',
|
||||
label: 'Authorization URL'
|
||||
},
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
},
|
||||
{
|
||||
key: 'state',
|
||||
label: 'State'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
||||
@@ -1,16 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.single-line-editor-wrapper {
|
||||
max-width: 400px;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,70 +0,0 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const OAuth2ClientCredentials = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'client_credentials',
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2ClientCredentials;
|
||||
@@ -1,21 +0,0 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
||||
@@ -1,54 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.grant-type-mode-selector {
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: fit-content;
|
||||
}
|
||||
.tippy-box {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
|
||||
.tippy-content: {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grant-type-label {
|
||||
width: fit-content;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,98 +0,0 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeGrantType } from 'utils/collections';
|
||||
import { useEffect } from 'react';
|
||||
import { updateCollectionAuth, updateCollectionAuthMode } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const GrantTypeSelector = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end grant-type-label select-none">
|
||||
{humanizeGrantType(oAuth?.grantType)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onGrantTypeChange = (grantType) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// initialize redux state with a default oauth2 grant type
|
||||
// authorization_code - default option
|
||||
!oAuth?.grantType &&
|
||||
dispatch(
|
||||
updateCollectionAuthMode({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
!oAuth?.grantType &&
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code'
|
||||
}
|
||||
})
|
||||
);
|
||||
}, [oAuth]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<label className="block font-medium mb-2">Grant Type</label>
|
||||
<div className="inline-flex items-center cursor-pointer grant-type-mode-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('password');
|
||||
}}
|
||||
>
|
||||
Password Credentials
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('authorization_code');
|
||||
}}
|
||||
>
|
||||
Authorization Code
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('client_credentials');
|
||||
}}
|
||||
>
|
||||
Client Credentials
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default GrantTypeSelector;
|
||||
@@ -1,16 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.single-line-editor-wrapper {
|
||||
max-width: 400px;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,72 +0,0 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'password',
|
||||
accessTokenUrl,
|
||||
username,
|
||||
password,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
||||
@@ -1,29 +0,0 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Username'
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Password'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
||||
@@ -1,21 +1,33 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import GrantTypeSelector from './GrantTypeSelector/index';
|
||||
import OAuth2PasswordCredentials from './PasswordCredentials/index';
|
||||
import OAuth2AuthorizationCode from './AuthorizationCode/index';
|
||||
import OAuth2ClientCredentials from './ClientCredentials/index';
|
||||
import { saveCollectionRoot } 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';
|
||||
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
|
||||
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
|
||||
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
|
||||
|
||||
const GrantTypeComponentMap = ({collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const save = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
};
|
||||
|
||||
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', {});
|
||||
|
||||
const grantTypeComponentMap = (grantType, collection) => {
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
return <OAuth2PasswordCredentials collection={collection} />;
|
||||
return <OAuth2PasswordCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
|
||||
break;
|
||||
case 'authorization_code':
|
||||
return <OAuth2AuthorizationCode collection={collection} />;
|
||||
return <OAuth2AuthorizationCode save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
|
||||
break;
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials collection={collection} />;
|
||||
return <OAuth2ClientCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
|
||||
break;
|
||||
default:
|
||||
return <div>TBD</div>;
|
||||
@@ -24,12 +36,12 @@ const grantTypeComponentMap = (grantType, collection) => {
|
||||
};
|
||||
|
||||
const OAuth2 = ({ collection }) => {
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<GrantTypeSelector collection={collection} />
|
||||
{grantTypeComponentMap(oAuth?.grantType, collection)}
|
||||
<GrantTypeSelector request={request} updateAuth={updateCollectionAuth} collection={collection} />
|
||||
<GrantTypeComponentMap collection={collection} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,8 +21,8 @@ const WsseAuth = ({ collection }) => {
|
||||
mode: 'wsse',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username,
|
||||
password: wsseAuth.password
|
||||
username: username || '',
|
||||
password: wsseAuth.password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -34,8 +34,8 @@ const WsseAuth = ({ collection }) => {
|
||||
mode: 'wsse',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: wsseAuth.username,
|
||||
password
|
||||
username: wsseAuth.username || '',
|
||||
password: password || ''
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -8,9 +8,7 @@ import { useState } from 'react';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useRef } from 'react';
|
||||
import path from 'path';
|
||||
import slash from 'utils/common/slash';
|
||||
import { isWindowsOS } from 'utils/common/platform';
|
||||
import path from 'utils/common/path';
|
||||
|
||||
const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
const certFilePathInputRef = useRef();
|
||||
@@ -27,7 +25,10 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
passphrase: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
domain: Yup.string().required(),
|
||||
domain: Yup.string()
|
||||
.required()
|
||||
.trim()
|
||||
.test('not-empty-after-trim', 'Domain is required', value => value && value.trim().length > 0),
|
||||
type: Yup.string().required().oneOf(['cert', 'pfx']),
|
||||
certFilePath: Yup.string().when('type', {
|
||||
is: (type) => type == 'cert',
|
||||
@@ -47,7 +48,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
let relevantValues = {};
|
||||
if (values.type === 'cert') {
|
||||
relevantValues = {
|
||||
domain: values.domain,
|
||||
domain: values.domain?.trim(),
|
||||
type: values.type,
|
||||
certFilePath: values.certFilePath,
|
||||
keyFilePath: values.keyFilePath,
|
||||
@@ -55,7 +56,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
};
|
||||
} else {
|
||||
relevantValues = {
|
||||
domain: values.domain,
|
||||
domain: values.domain?.trim(),
|
||||
type: values.type,
|
||||
pfxFilePath: values.pfxFilePath,
|
||||
passphrase: values.passphrase
|
||||
@@ -70,12 +71,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
const getFile = (e) => {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]);
|
||||
if (filePath) {
|
||||
let relativePath;
|
||||
if (isWindowsOS()) {
|
||||
relativePath = slash(path.win32.relative(root, filePath));
|
||||
} else {
|
||||
relativePath = path.posix.relative(root, filePath);
|
||||
}
|
||||
let relativePath = path.relative(root, filePath);
|
||||
formik.setFieldValue(e.name, relativePath);
|
||||
}
|
||||
};
|
||||
@@ -109,23 +105,23 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<ul className="mt-4">
|
||||
{!clientCertConfig.length
|
||||
? 'No client certificates added'
|
||||
: clientCertConfig.map((clientCert) => (
|
||||
<li key={uuid()} 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">
|
||||
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
|
||||
{clientCert.domain}
|
||||
</div>
|
||||
<div className="flex w-full items-center">
|
||||
<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">
|
||||
<IconTrash size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
: clientCertConfig.map((clientCert, index) => (
|
||||
<li key={`client-cert-${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">
|
||||
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
|
||||
{clientCert.domain}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
<div className="flex w-full items-center">
|
||||
<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">
|
||||
<IconTrash size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h1 className="font-semibold mt-8 mb-2">Add Client Certificate</h1>
|
||||
@@ -134,15 +130,20 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<label className="settings-label" htmlFor="domain">
|
||||
Domain
|
||||
</label>
|
||||
<input
|
||||
id="domain"
|
||||
type="text"
|
||||
name="domain"
|
||||
placeholder="*.example.org"
|
||||
className="block textbox non-passphrase-input"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.domain || ''}
|
||||
/>
|
||||
<div className="relative flex items-center">
|
||||
<div className="absolute left-0 pl-2 text-gray-400 pointer-events-none flex items-center h-full">
|
||||
https://
|
||||
</div>
|
||||
<input
|
||||
id="domain"
|
||||
type="text"
|
||||
name="domain"
|
||||
placeholder="example.org"
|
||||
className="block textbox non-passphrase-input !pl-[60px]"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.domain || ''}
|
||||
/>
|
||||
</div>
|
||||
{formik.touched.domain && formik.errors.domain ? (
|
||||
<div className="ml-1 text-red-500">{formik.errors.domain}</div>
|
||||
) : null}
|
||||
@@ -198,9 +199,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<div
|
||||
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
|
||||
title={path.basename(slash(formik.values.certFilePath))}
|
||||
title={path.basename(formik.values.certFilePath)}
|
||||
>
|
||||
{path.basename(slash(formik.values.certFilePath))}
|
||||
{path.basename(formik.values.certFilePath)}
|
||||
</div>
|
||||
<IconTrash
|
||||
size={18}
|
||||
@@ -238,9 +239,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<div
|
||||
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
|
||||
title={path.basename(slash(formik.values.keyFilePath))}
|
||||
title={path.basename(formik.values.keyFilePath)}
|
||||
>
|
||||
{path.basename(slash(formik.values.keyFilePath))}
|
||||
{path.basename(formik.values.keyFilePath)}
|
||||
</div>
|
||||
<IconTrash
|
||||
size={18}
|
||||
@@ -281,9 +282,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<div
|
||||
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
|
||||
title={path.basename(slash(formik.values.pfxFilePath))}
|
||||
title={path.basename(formik.values.pfxFilePath)}
|
||||
>
|
||||
{path.basename(slash(formik.values.pfxFilePath))}
|
||||
{path.basename(formik.values.pfxFilePath)}
|
||||
</div>
|
||||
<IconTrash
|
||||
size={18}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
.CodeMirror-scroll {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.editing-mode {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -119,6 +119,6 @@ This documentation supports Markdown formatting! You can use:
|
||||
- **Bold** and *italic* text
|
||||
- \`code blocks\` and syntax highlighting
|
||||
- Tables and lists
|
||||
- [Links](https://example.com)
|
||||
- [Links](https://usebruno.com)
|
||||
- And more!
|
||||
`;
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import { getTotalRequestCountInCollection } from 'utils/collections/';
|
||||
import { IconFolder, IconFileOff, IconWorld, IconApi } from '@tabler/icons';
|
||||
import { IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
|
||||
import { areItemsLoading, getItemsLoadStats } from "utils/collections/index";
|
||||
import { useState } from "react";
|
||||
import ShareCollection from "components/ShareCollection/index";
|
||||
|
||||
const Info = ({ collection }) => {
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
|
||||
|
||||
const isCollectionLoading = areItemsLoading(collection);
|
||||
const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
|
||||
const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
|
||||
|
||||
const handleToggleShowShareCollectionModal = (value) => (e) => {
|
||||
toggleShowShareCollectionModal(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col h-fit">
|
||||
<div className="rounded-lg py-6">
|
||||
@@ -42,15 +53,30 @@ const Info = ({ collection }) => {
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-semibold text-sm">Requests</div>
|
||||
<div className="mt-1 text-sm text-muted">
|
||||
{totalRequestsInCollection} request{totalRequestsInCollection !== 1 ? 's' : ''} in collection
|
||||
<div className="mt-1 text-sm text-muted font-mono">
|
||||
{
|
||||
isCollectionLoading? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start group cursor-pointer" onClick={handleToggleShowShareCollectionModal(true)}>
|
||||
<div className="flex-shrink-0 p-3 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg">
|
||||
<IconShare className="w-5 h-5 text-indigo-500" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4 h-full flex flex-col justify-start">
|
||||
<div className="font-semibold text-sm h-fit my-auto">Share</div>
|
||||
<div className="mt-1 text-sm group-hover:underline text-link">
|
||||
Share Collection
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showShareCollectionModal && <ShareCollection collectionUid={collection.uid} onClose={handleToggleShowShareCollectionModal(false)} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Info;
|
||||
export default Info;
|
||||
@@ -2,8 +2,15 @@ import React from 'react';
|
||||
import { flattenItems } from "utils/collections";
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { isItemARequest, itemIsOpenedInTabs } from 'utils/tabs/index';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections/index';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
const RequestsNotLoaded = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const flattenedItems = flattenItems(collection.items);
|
||||
const itemsFailedLoading = flattenedItems?.filter(item => item?.partial && !item?.loading);
|
||||
|
||||
@@ -11,6 +18,29 @@ const RequestsNotLoaded = ({ collection }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleRequestClick = (item) => e => {
|
||||
e.preventDefault();
|
||||
if (isItemARequest(item)) {
|
||||
dispatch(hideHomePage());
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
focusTab({
|
||||
uid: item.uid
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full card my-2">
|
||||
<div className="flex items-center gap-2 px-3 py-2 title bg-yellow-50 dark:bg-yellow-900/20">
|
||||
@@ -31,7 +61,7 @@ const RequestsNotLoaded = ({ collection }) => {
|
||||
<tbody>
|
||||
{flattenedItems?.map((item, index) => (
|
||||
item?.partial && !item?.loading ? (
|
||||
<tr key={index}>
|
||||
<tr key={index} className='cursor-pointer' onClick={handleRequestClick(item)}>
|
||||
<td className="py-1.5 px-3">
|
||||
{item?.pathname?.split(`${collection?.pathname}/`)?.[1]}
|
||||
</td>
|
||||
|
||||
@@ -104,18 +104,15 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
Config
|
||||
<InfoTip
|
||||
text={`
|
||||
<InfoTip infotipId="request-var">
|
||||
<div>
|
||||
<ul>
|
||||
<li><span style="width: 50px;display:inline-block;">global</span> - use global proxy config</li>
|
||||
<li><span style="width: 50px;display:inline-block;">enabled</span> - use collection proxy config</li>
|
||||
<li><span style="width: 50px;display:inline-block;">disable</span> - disable proxy</li>
|
||||
<li><span style={{width: "50px", display: "inline-block"}}>global</span> - use global proxy config</li>
|
||||
<li><span style={{width: "50px", display: "inline-block"}}>enabled</span> - use collection proxy config</li>
|
||||
<li><span style={{width: "50px", display: "inline-block"}}>disable</span> - disable proxy</li>
|
||||
</ul>
|
||||
</div>
|
||||
`}
|
||||
infotipId="request-var"
|
||||
/>
|
||||
</InfoTip>
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
|
||||
@@ -89,7 +89,7 @@ const VarsTable = ({ collection, vars, varType }) => {
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
<InfoTip content="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
|
||||
@@ -49,7 +49,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
const requestVars = get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = get(collection, 'root.request.vars.res', []);
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
const auth = get(collection, 'root.request.auth', {}).mode;
|
||||
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
|
||||
|
||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
@@ -155,7 +155,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
{auth !== 'none' && <ContentIndicator />}
|
||||
{authMode !== 'none' && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
|
||||
Script
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div``;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,371 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { modifyCookie, addCookie, getParsedCookie, createCookieString } from 'providers/ReduxStore/slices/app';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
import ToggleSwitch from 'components/ToggleSwitch/index';
|
||||
import { IconInfoCircle } from '@tabler/icons';
|
||||
import moment from 'moment';
|
||||
import 'moment-timezone';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
const removeEmptyValues = (obj) => {
|
||||
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== null && value !== undefined));
|
||||
};
|
||||
|
||||
const ModifyCookieModal = ({ onClose, domain, cookie }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [isRawMode, setIsRawMode] = useState(false);
|
||||
const [cookieString, setCookieString] = useState('');
|
||||
const initialParseRef = useRef(false);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
...(cookie ? cookie : {}),
|
||||
key: cookie?.key || '',
|
||||
value: cookie?.value || '',
|
||||
path: cookie?.path || '/',
|
||||
domain: cookie?.domain || domain || '',
|
||||
expires: cookie?.expires ? moment(cookie.expires).format(moment.HTML5_FMT.DATETIME_LOCAL) : '',
|
||||
secure: cookie?.secure || false,
|
||||
httpOnly: cookie?.httpOnly || false
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
key: Yup.string().required('Key is required'),
|
||||
value: Yup.string().required('Value is required'),
|
||||
domain: Yup.string().required('Domain is required'),
|
||||
secure: Yup.boolean(),
|
||||
httpOnly: Yup.boolean(),
|
||||
expires: Yup.mixed()
|
||||
.nullable()
|
||||
.transform((value) => {
|
||||
if (!value || value === '') return null;
|
||||
return moment(value).isValid() ? moment(value).toDate() : null;
|
||||
})
|
||||
.test('future-date', 'Expiration date must be in the future', (value) => {
|
||||
if (!value) return true;
|
||||
return moment(value).isAfter(moment());
|
||||
})
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
const modValues = removeEmptyValues({
|
||||
...(cookie ? cookie : {}),
|
||||
...values,
|
||||
expires: values.expires
|
||||
? moment(values.expires).isValid()
|
||||
? moment(values.expires).toDate()
|
||||
: Infinity
|
||||
: Infinity
|
||||
});
|
||||
|
||||
handleCookieDispatch(cookie, domain, modValues, onClose);
|
||||
}
|
||||
});
|
||||
|
||||
const title = cookie ? 'Modify Cookie' : 'Add Cookie';
|
||||
|
||||
const handleCookieDispatch = (cookie, domain, modValues, onClose) => {
|
||||
if (cookie) {
|
||||
dispatch(modifyCookie(domain, cookie, modValues))
|
||||
.then(() => {
|
||||
toast.success('Cookie modified successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('An error occurred while modifying cookie');
|
||||
console.error(err);
|
||||
});
|
||||
} else {
|
||||
dispatch(addCookie(domain, modValues))
|
||||
.then(() => {
|
||||
toast.success('Cookie added successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('An error occurred while adding cookie');
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
if (isRawMode) {
|
||||
const cookieObj = await dispatch(getParsedCookie(cookieString));
|
||||
|
||||
const modifiedCookie = removeEmptyValues({
|
||||
...formik.values,
|
||||
...cookieObj,
|
||||
expires: cookieObj?.expires
|
||||
? moment(cookieObj.expires).isValid()
|
||||
? moment(cookieObj.expires).toDate()
|
||||
: Infinity
|
||||
: Infinity
|
||||
});
|
||||
|
||||
if (!cookieObj) {
|
||||
toast.error('Please enter a valid cookie string');
|
||||
return;
|
||||
}
|
||||
|
||||
const validationErrors = await formik.setValues(
|
||||
(values) => ({
|
||||
...values,
|
||||
...modifiedCookie,
|
||||
expires:
|
||||
modifiedCookie?.expires && moment(modifiedCookie.expires).isValid()
|
||||
? moment(new Date(modifiedCookie.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL)
|
||||
: ''
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
if (!isEmpty(validationErrors)) {
|
||||
toast.error(Object.values(validationErrors).join("\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
handleCookieDispatch(cookie, domain, modifiedCookie, onClose);
|
||||
} else {
|
||||
formik.handleSubmit();
|
||||
}
|
||||
} catch (error) {
|
||||
const errMsg = error.message || 'An error occurred while parsing cookie string';
|
||||
toast.error(errMsg);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRawMode) return;
|
||||
const loadCookieString = async () => {
|
||||
if (cookie) {
|
||||
const str = await dispatch(createCookieString(cookie));
|
||||
setCookieString(str);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
loadCookieString();
|
||||
}, [cookie, isRawMode]);
|
||||
|
||||
// create the cookieString when raw mode is enabled
|
||||
useEffect(() => {
|
||||
if (isRawMode) {
|
||||
const createCookieStr = async () => {
|
||||
const str = await dispatch(createCookieString(formik.values));
|
||||
setCookieString(str);
|
||||
};
|
||||
|
||||
createCookieStr();
|
||||
}
|
||||
}, [isRawMode, formik.values]);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset the ref when raw mode changes
|
||||
if (isRawMode) {
|
||||
initialParseRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const setParsedCookie = async () => {
|
||||
if (!isRawMode && cookieString && !initialParseRef.current) {
|
||||
initialParseRef.current = true;
|
||||
|
||||
try {
|
||||
const cookieObj = await dispatch(getParsedCookie(cookieString));
|
||||
|
||||
if (!cookieObj) return;
|
||||
|
||||
formik.setValues(
|
||||
(values) => ({
|
||||
...values,
|
||||
...removeEmptyValues(cookieObj),
|
||||
expires:
|
||||
cookieObj?.expires && moment(cookieObj.expires).isValid()
|
||||
? moment(new Date(cookieObj.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL)
|
||||
: ''
|
||||
}),
|
||||
true
|
||||
);
|
||||
} catch (error) {
|
||||
const errMsg = error.message || 'An error occurred while parsing cookie string';
|
||||
toast.error(errMsg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setParsedCookie();
|
||||
}, [isRawMode, cookieString, dispatch, formik]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="lg"
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
handleCancel={onClose}
|
||||
handleConfirm={onSubmit}
|
||||
customHeader={
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<h2 className="text-sm font-bold">{title}</h2>
|
||||
<div className="ml-auto flex items-center ">
|
||||
<ToggleSwitch
|
||||
className="mr-2"
|
||||
isOn={isRawMode}
|
||||
size="2xs"
|
||||
handleToggle={(e) => {
|
||||
setIsRawMode(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
<label className="text-sm font-normal mr-4 normal-case">Edit Raw</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<form onSubmit={(e) => e.preventDefault()} className="px-2">
|
||||
{isRawMode ? (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<label className="block text-sm">Set-Cookie String</label>
|
||||
<IconInfoCircle id="cookie-raw-info" size={16} strokeWidth={1.5} className="text-gray-400" />
|
||||
<Tooltip
|
||||
anchorId="cookie-raw-info"
|
||||
className="tooltip-mod"
|
||||
html="Key, Path, and Domain are immutable properties and cannot be modified for existing cookies"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
value={cookieString}
|
||||
onChange={(e) => setCookieString(e.target.value)}
|
||||
className="block textbox w-full h-24"
|
||||
placeholder="key=value; key2=value2"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Domain<span className="text-red-600">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="domain"
|
||||
// Auto-focus if its add-new i.e. when domain prop is empty
|
||||
autoFocus={!domain && !formik.values.domain}
|
||||
value={formik.values.domain}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.domain && formik.errors.domain && (
|
||||
<div className="text-red-500 text-sm mt-1">{formik.errors.domain}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Path</label>
|
||||
<input
|
||||
type="text"
|
||||
name="path"
|
||||
value={formik.values.path}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.path && formik.errors.path && (
|
||||
<div className="text-red-500 text-sm mt-1">{formik.errors.path}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Key<span className="text-red-600">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="key"
|
||||
// Auto focus when add-for-domain i.e. if domain is already prefilled
|
||||
autoFocus={!!domain && !formik.values.key}
|
||||
value={formik.values.key}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.key && formik.errors.key && (
|
||||
<div className="text-red-500 text-sm mt-1">{formik.errors.key}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Value<span className="text-red-600">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="value"
|
||||
// Auto-focus when its in edit mode i.e. cookie prop is present
|
||||
autoFocus={!!cookie}
|
||||
value={formik.values.value}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full"
|
||||
/>
|
||||
{formik.touched.value && formik.errors.value && (
|
||||
<div className="text-red-500 text-sm mt-1">{formik.errors.value}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date Picker */}
|
||||
<div className="w-full flex items-end">
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Expiration ({moment.tz.guess()})</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="expires"
|
||||
value={formik.values.expires}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
}}
|
||||
className="block textbox non-passphrase-input w-full"
|
||||
min={moment().format(moment.HTML5_FMT.DATETIME_LOCAL)}
|
||||
/>
|
||||
{formik.touched.expires && formik.errors.expires && (
|
||||
<div className="text-red-500 text-sm mt-1">{formik.errors.expires}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Checkboxes */}
|
||||
<div className="flex space-x-4 ml-auto">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="secure"
|
||||
checked={formik.values.secure}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm">Secure</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="httpOnly"
|
||||
checked={formik.values.httpOnly}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm">HTTP Only</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModifyCookieModal;
|
||||
@@ -11,6 +11,65 @@ const Wrapper = styled.div`
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.header {
|
||||
input {
|
||||
padding: 0.3rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.textbox {
|
||||
line-height: 1.42857143;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.45rem;
|
||||
box-shadow: none;
|
||||
border-radius: 0px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-box {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
|
||||
background:
|
||||
/* Shadow Cover TOP */
|
||||
linear-gradient(
|
||||
${(props) => props.theme.modal.body.bg} 20%,
|
||||
rgba(255, 255, 255, 0)
|
||||
) center top,
|
||||
|
||||
/* Shadow Cover BOTTOM */
|
||||
linear-gradient(
|
||||
rgba(255, 255, 255, 0),
|
||||
${(props) => props.theme.modal.body.bg} 80%
|
||||
) center bottom,
|
||||
|
||||
/* Shadow TOP */
|
||||
linear-gradient(
|
||||
rgba(0, 0, 0, 0.1) 0%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
) center top,
|
||||
|
||||
/* Shadow BOTTOM */
|
||||
linear-gradient(
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.1) 100%
|
||||
) center bottom;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 30px, 100% 30px, 100% 10px, 100% 10px;
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,53 +1,331 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import Modal from 'components/Modal';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { deleteCookiesForDomain } from 'providers/ReduxStore/slices/app';
|
||||
import Accordion from 'components/Accordion/index';
|
||||
import { IconTrash, IconEdit, IconCirclePlus, IconCookieOff, IconAlertTriangle, IconSearch } from '@tabler/icons';
|
||||
import { deleteCookiesForDomain, deleteCookie } from 'providers/ReduxStore/slices/app';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import ModifyCookieModal from 'components/Cookies/ModifyCookieModal/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import moment from 'moment';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
|
||||
const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
|
||||
<Modal onClose={onClose} handleCancel={onClose} title="Clear Domain Cookies" hideFooter={true}>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
Are you sure you want to clear all cookies for the domain {domain}?
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-close" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onClear}>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const DeleteCookieModal = ({ onClose, cookieName, onDelete }) => (
|
||||
<Modal onClose={onClose} handleCancel={onClose} title="Delete Cookie" hideFooter={true}>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
Are you sure you want to delete the cookie {cookieName}?
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-close" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onDelete}>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const CollectionProperties = ({ onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const cookies = useSelector((state) => state.app.cookies) || [];
|
||||
const [isModifyCookieModalOpen, setIsModifyCookieModalOpen] = useState(false);
|
||||
const [currentDomain, setCurrentDomain] = useState(null);
|
||||
const [cookieToEdit, setCookieToEdit] = useState(null);
|
||||
|
||||
const handleDeleteDomain = (domain) => {
|
||||
dispatch(deleteCookiesForDomain(domain))
|
||||
.then(() => {
|
||||
toast.success('Domain deleted successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to delete domain'));
|
||||
const [domainToClear, setDomainToClear] = useState(null);
|
||||
const [cookieToDelete, setCookieToDelete] = useState(null);
|
||||
const [searchText, setSearchText] = useState(null);
|
||||
|
||||
const handleAddCookie = (domain) => {
|
||||
if(domain) setCurrentDomain(domain);
|
||||
setIsModifyCookieModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEditCookie = (domain, cookie) => {
|
||||
setCurrentDomain(domain);
|
||||
setCookieToEdit(cookie);
|
||||
setIsModifyCookieModalOpen(true);
|
||||
};
|
||||
|
||||
const handleClearDomainCookies = (domain) => {
|
||||
setDomainToClear(domain);
|
||||
};
|
||||
|
||||
const clearDomainCookiesAction = () => {
|
||||
dispatch(deleteCookiesForDomain(domainToClear))
|
||||
.then(() => {
|
||||
toast.success('Domain cookies cleared successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to clear domain cookies'));
|
||||
setDomainToClear(null);
|
||||
};
|
||||
|
||||
const handleDeleteCookie = (domain, path, key) => {
|
||||
setCookieToDelete({ key, domain, path });
|
||||
};
|
||||
|
||||
const deleteCookieAction = () => {
|
||||
if (cookieToDelete) {
|
||||
const { domain, path, key } = cookieToDelete;
|
||||
dispatch(deleteCookie(domain, path, key))
|
||||
.then(() => {
|
||||
toast.success('Cookie deleted successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to delete cookie'));
|
||||
}
|
||||
setCookieToDelete(null);
|
||||
};
|
||||
|
||||
const filteredCookies = useMemo(() => {
|
||||
if (!searchText) return cookies;
|
||||
|
||||
return cookies.filter((cookie) =>
|
||||
cookie.domain.toLowerCase().includes(searchText.toLowerCase())
|
||||
);
|
||||
}, [cookies, searchText]);
|
||||
|
||||
const shouldShowHeader = cookies && cookies.length > 0;
|
||||
|
||||
return (
|
||||
<Modal size="md" title="Cookies" hideFooter={true} handleCancel={onClose}>
|
||||
<StyledWrapper>
|
||||
<table className="w-full border-collapse" style={{ marginTop: '-1rem' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-2 px-2 text-left">Domain</th>
|
||||
<th className="py-2 px-2 text-left">Cookie</th>
|
||||
<th className="py-2 px-2 text-center" style={{ width: 80 }}>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cookies.map((cookie) => (
|
||||
<tr key={cookie.domain}>
|
||||
<td className="py-2 px-2">{cookie.domain}</td>
|
||||
<td className="py-2 px-2 break-all">{cookie.cookieString}</td>
|
||||
<td className="text-center">
|
||||
<button tabIndex="-1" onClick={() => handleDeleteDomain(cookie.domain)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
<>
|
||||
<Modal
|
||||
size="xl"
|
||||
title="Cookies"
|
||||
hideFooter={true}
|
||||
handleCancel={onClose}
|
||||
customHeader={shouldShowHeader ? (
|
||||
<StyledWrapper className="header flex items-center justify-between w-full">
|
||||
<h2 className="text-xs font-medium">Cookies</h2>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search by domain"
|
||||
value={searchText || ''}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
className="block textbox non-passphrase-input ml-auto font-normal"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-sm btn-secondary flex items-center gap-1 mx-4 font-medium"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddCookie();
|
||||
}}
|
||||
>
|
||||
<IconCirclePlus strokeWidth={1.5} size={16} />
|
||||
<span>Add Cookie</span>
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
) : null}
|
||||
>
|
||||
<StyledWrapper>
|
||||
{!cookies || !cookies.length ? (
|
||||
// No cookies found
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<IconCookieOff size={48} strokeWidth={1.5} className="text-gray-500" />
|
||||
<h2 className="text-lg font-semibold mt-4">No cookies found</h2>
|
||||
<p className="text-gray-500 mt-2">Add cookies to get started</p>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-sm btn-secondary flex items-center gap-1 mt-8"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddCookie();
|
||||
}}
|
||||
>
|
||||
<IconCirclePlus strokeWidth={1.5} size={16} />
|
||||
<span>Add Cookie</span>
|
||||
</button>
|
||||
</div>
|
||||
) : cookies.length && !filteredCookies.length ? (
|
||||
// No search results
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<IconSearch size={48} />
|
||||
<h2 className="text-lg font-semibold mt-4">No search results</h2>
|
||||
<p className="text-gray-500 mt-2">Try a different search term</p>
|
||||
</div>
|
||||
) : (
|
||||
// Show cookies list
|
||||
<div className="scroll-box">
|
||||
<Accordion defaultIndex={0}>
|
||||
{filteredCookies.map((domainWithCookies, i) => (
|
||||
<Accordion.Item key={i} index={i}>
|
||||
<Accordion.Header index={i} className="flex items-center">
|
||||
<div className="flex items-center">
|
||||
<span>{domainWithCookies.domain}</span>
|
||||
<span className="ml-2 text-xs dark:text-gray-300 text-gray-500">
|
||||
({domainWithCookies.cookies.length}{' '}
|
||||
{domainWithCookies.cookies.length === 1 ? 'cookie' : 'cookies'})
|
||||
</span>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center gap-1 text-gray-500 hover:text-gray-950 dark:text-white dark:hover:text-gray-300"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddCookie(domainWithCookies.domain);
|
||||
}}
|
||||
>
|
||||
<IconCirclePlus strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClearDomainCookies(domainWithCookies.domain);
|
||||
}}
|
||||
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600 mr-2"
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content index={i}>
|
||||
<div className="flex items-center justify-between">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="text-left border-b border-gray-200 dark:border-neutral-600 text-gray-700 dark:text-gray-300">
|
||||
<th className="py-2 px-4 font-semibold w-32">Name</th>
|
||||
<th className="py-2 px-4 font-semibold w-52">Value</th>
|
||||
<th className="py-2 px-4 font-semibold">Path</th>
|
||||
<th className="py-2 px-4 font-semibold">Expires</th>
|
||||
<th className="py-2 px-4 font-semibold text-center">Secure</th>
|
||||
<th className="py-2 px-4 font-semibold text-center">HTTP Only</th>
|
||||
<th className="py-2 px-4 font-semibold text-right w-24">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{domainWithCookies.cookies.map((cookie) => (
|
||||
<tr key={cookie.key} className="border-b border-gray-200 dark:border-neutral-600 last:border-none">
|
||||
<td className="py-2 px-4 truncate">
|
||||
<span id={`cookie-key-${cookie.key}`}>{cookie.key}</span>
|
||||
<Tooltip
|
||||
anchorId={`cookie-key-${cookie.key}`}
|
||||
className="tooltip-mod"
|
||||
html={cookie.key}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-4 truncate">
|
||||
<span id={`cookie-value-${cookie.key}`}>{cookie.value}</span>
|
||||
<Tooltip
|
||||
anchorId={`cookie-value-${cookie.key}`}
|
||||
className="tooltip-mod"
|
||||
html={cookie.value}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-4 truncate">{cookie.path || '/'}</td>
|
||||
<td className="py-2 px-4 truncate">
|
||||
<span id={`cookie-expires-${cookie.key}`}>
|
||||
{cookie.expires && moment(cookie.expires).isValid()
|
||||
? new Date(cookie.expires).toLocaleString()
|
||||
: 'Session'}
|
||||
</span>
|
||||
{cookie.expires && moment(cookie.expires).isValid() && (
|
||||
<Tooltip
|
||||
anchorId={`cookie-expires-${cookie.key}`}
|
||||
className="tooltip-mod"
|
||||
html={new Date(cookie.expires).toLocaleString()}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-4 text-center">{cookie.secure ? '✓' : ''}</td>
|
||||
<td className="py-2 px-4 text-center">{cookie.httpOnly ? '✓' : ''}</td>
|
||||
<td className="py-2 px-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditCookie(domainWithCookies.domain, cookie);
|
||||
}}
|
||||
className="text-gray-700 hover:text-gray-950
|
||||
dark:text-white dark:hover:text-gray-300"
|
||||
>
|
||||
<IconEdit strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteCookie(domainWithCookies.domain, cookie.path, cookie.key);
|
||||
}}
|
||||
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600"
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
{isModifyCookieModalOpen && (
|
||||
<ModifyCookieModal
|
||||
onClose={() => {
|
||||
setCookieToEdit(null);
|
||||
setCurrentDomain(null);
|
||||
setIsModifyCookieModalOpen(false);
|
||||
}}
|
||||
domain={currentDomain}
|
||||
cookie={cookieToEdit}
|
||||
/>
|
||||
)}
|
||||
{domainToClear ? (
|
||||
<ClearDomainCookiesModal
|
||||
onClose={() => setDomainToClear(null)}
|
||||
domain={domainToClear}
|
||||
onClear={clearDomainCookiesAction}
|
||||
/>
|
||||
) : null}
|
||||
{cookieToDelete ? (
|
||||
<DeleteCookieModal
|
||||
onClose={() => setCookieToDelete(null)}
|
||||
cookieName={cookieToDelete.key}
|
||||
onDelete={deleteCookieAction}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
|
||||
const CreateEnvironment = ({ collection, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -23,7 +24,11 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'Must be at least 1 character')
|
||||
.max(50, 'Must be 50 characters or less')
|
||||
.max(255, 'Must be 255 characters or less')
|
||||
.test('is-valid-filename', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('Name is required')
|
||||
.test('duplicate-name', 'Environment already exists', validateEnvironmentName)
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useRef, useEffect } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -13,11 +13,18 @@ import { variableNameRegex } from 'utils/common/regex';
|
||||
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const addButtonRef = useRef(null);
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
|
||||
let _collection = cloneDeep(collection);
|
||||
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
@@ -160,7 +167,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
collection={collection}
|
||||
collection={_collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
isSecret={variable.secret}
|
||||
|
||||
@@ -28,7 +28,10 @@ const ImportEnvironment = ({ collection, onClose }) => {
|
||||
.then(() => {
|
||||
toast.success('Environment imported successfully');
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while importing the environment'));
|
||||
.catch((error) => {
|
||||
toast.error('An error occurred while importing the environment');
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useFormik } from 'formik';
|
||||
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
|
||||
const RenameEnvironment = ({ onClose, environment, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -18,7 +19,11 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.max(255, 'Must be 255 characters or less')
|
||||
.test('is-valid-filename', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import path from 'path';
|
||||
import path from 'utils/common/path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconX } from '@tabler/icons';
|
||||
import { isWindowsOS } from 'utils/common/platform';
|
||||
import slash from 'utils/common/slash';
|
||||
|
||||
const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -27,7 +26,7 @@ const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = fa
|
||||
const collectionDir = collection.pathname;
|
||||
|
||||
if (filePath.startsWith(collectionDir)) {
|
||||
return path.relative(slash(collectionDir), slash(filePath));
|
||||
return path.relative(collectionDir, filePath);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
|
||||
@@ -11,6 +11,12 @@ const Wrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
.inherit-mode-text {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
.auth-mode-label {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
export default Wrapper;
|
||||
226
packages/bruno-app/src/components/FolderSettings/Auth/index.js
Normal file
226
packages/bruno-app/src/components/FolderSettings/Auth/index.js
Normal file
@@ -0,0 +1,226 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
|
||||
import { updateFolderAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
|
||||
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
|
||||
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
|
||||
import AuthMode from '../AuthMode';
|
||||
import BasicAuth from 'components/RequestPane/Auth/BasicAuth';
|
||||
import BearerAuth from 'components/RequestPane/Auth/BearerAuth';
|
||||
import DigestAuth from 'components/RequestPane/Auth/DigestAuth';
|
||||
import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth';
|
||||
import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
|
||||
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
|
||||
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
|
||||
import { findItemInCollection, findParentItemInCollection, humanizeRequestAuthMode } from 'utils/collections/index';
|
||||
|
||||
const GrantTypeComponentMap = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const save = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
let request = get(folder, 'root.request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', 'authorization_code');
|
||||
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
return <OAuth2PasswordCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
case 'authorization_code':
|
||||
return <OAuth2AuthorizationCode save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
default:
|
||||
return <div>TBD</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const Auth = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
let request = get(folder, 'root.request', {});
|
||||
const authMode = get(folder, 'root.request.auth.mode');
|
||||
|
||||
const getTreePathFromCollectionToFolder = (collection, _folder) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _folder?.uid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
// Get path from collection to current folder
|
||||
const folderTreePath = getTreePathFromCollectionToFolder(collection, folder);
|
||||
|
||||
// Check parent folders to find closest auth configuration
|
||||
// Skip the last item which is the current folder
|
||||
for (let i = 0; i < folderTreePath.length - 1; i++) {
|
||||
const parentFolder = folderTreePath[i];
|
||||
if (parentFolder.type === 'folder') {
|
||||
const folderAuth = get(parentFolder, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: parentFolder.name,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'basic': {
|
||||
return (
|
||||
<BasicAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'bearer': {
|
||||
return (
|
||||
<BearerAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'digest': {
|
||||
return (
|
||||
<DigestAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'ntlm': {
|
||||
return (
|
||||
<NTLMAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'wsse': {
|
||||
return (
|
||||
<WsseAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'apikey': {
|
||||
return (
|
||||
<ApiKeyAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'awsv4': {
|
||||
return (
|
||||
<AwsV4Auth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'oauth2': {
|
||||
return (
|
||||
<>
|
||||
<GrantTypeSelector
|
||||
request={request}
|
||||
updateAuth={updateFolderAuth}
|
||||
collection={collection}
|
||||
item={folder}
|
||||
/>
|
||||
<GrantTypeComponentMap collection={collection} folder={folder} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div>Auth inherited from {source.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'none': {
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Configures authentication for the entire folder. This applies to all requests using the{' '}
|
||||
<span className="font-medium">Inherit</span> option in the <span className="font-medium">Auth</span> tab.
|
||||
</div>
|
||||
<div className="flex flex-grow justify-start items-center mb-4">
|
||||
<AuthMode collection={collection} folder={folder} />
|
||||
</div>
|
||||
{getAuthView()}
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Auth;
|
||||
@@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.auth-mode-selector {
|
||||
border: 1px solid ${({ theme }) => theme.colors.border};
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.auth-mode-label {
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,134 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateFolderAuthMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const AuthMode = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const authMode = get(folder, 'root.request.auth.mode');
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onModeChange = (value) => {
|
||||
dispatch(
|
||||
updateFolderAuthMode({
|
||||
mode: value,
|
||||
collectionUid: collection.uid,
|
||||
folderUid: folder.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('awsv4');
|
||||
}}
|
||||
>
|
||||
AWS Sig v4
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('basic');
|
||||
}}
|
||||
>
|
||||
Basic Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('bearer');
|
||||
}}
|
||||
>
|
||||
Bearer Token
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('digest');
|
||||
}}
|
||||
>
|
||||
Digest Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('ntlm');
|
||||
}}
|
||||
>
|
||||
NTLM Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('oauth2');
|
||||
}}
|
||||
>
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('wsse');
|
||||
}}
|
||||
>
|
||||
WSSE Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('apikey');
|
||||
}}
|
||||
>
|
||||
API Key
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('inherit');
|
||||
}}
|
||||
>
|
||||
Inherit
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('none');
|
||||
}}
|
||||
>
|
||||
No Auth
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthMode;
|
||||
@@ -88,7 +88,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<InfoTip text="You can write any valid JS expression here" infotipId="response-var" />
|
||||
<InfoTip content="You can write any valid JS expression here" infotipId="response-var" />
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
|
||||
@@ -8,7 +8,9 @@ import Tests from './Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars';
|
||||
import Documentation from './Documentation';
|
||||
import Auth from './Auth';
|
||||
import DotIcon from 'components/Icons/Dot';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const ContentIndicator = () => {
|
||||
return (
|
||||
@@ -26,7 +28,7 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
tab = folderLevelSettingsSelectedTab[folder?.uid];
|
||||
}
|
||||
|
||||
const folderRoot = collection?.items.find((item) => item.uid === folder?.uid)?.root;
|
||||
const folderRoot = folder?.root;
|
||||
const hasScripts = folderRoot?.request?.script?.res || folderRoot?.request?.script?.req;
|
||||
const hasTests = folderRoot?.request?.tests;
|
||||
|
||||
@@ -37,6 +39,9 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
const responseVars = folderRoot?.request?.vars?.res || [];
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
|
||||
const auth = get(folderRoot, 'request.auth.mode');
|
||||
const hasAuth = auth && auth !== 'none';
|
||||
|
||||
const setTab = (tab) => {
|
||||
dispatch(
|
||||
updatedFolderSettingsSelectedTab({
|
||||
@@ -61,6 +66,9 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
case 'vars': {
|
||||
return <Vars collection={collection} folder={folder} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth collection={collection} folder={folder} />;
|
||||
}
|
||||
case 'docs': {
|
||||
return <Documentation collection={collection} folder={folder} />;
|
||||
}
|
||||
@@ -93,6 +101,10 @@ const FolderSettings = ({ collection, folder }) => {
|
||||
Vars
|
||||
{activeVarsCount > 0 && <sup className="ml-1 font-medium">{activeVarsCount}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
{hasAuth && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
|
||||
@@ -81,7 +81,10 @@ const EnvironmentSelector = () => {
|
||||
<IconDatabaseOff size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2">No Environment</span>
|
||||
</div>
|
||||
<div className="dropdown-item border-top" onClick={handleSettingsIconClick}>
|
||||
<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>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
|
||||
const CreateEnvironment = ({ onClose }) => {
|
||||
const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);
|
||||
@@ -25,7 +26,11 @@ const CreateEnvironment = ({ onClose }) => {
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'Must be at least 1 character')
|
||||
.max(50, 'Must be 50 characters or less')
|
||||
.max(255, 'Must be 255 characters or less')
|
||||
.test('is-valid-filename', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('Name is required')
|
||||
.test('duplicate-name', 'Global Environment already exists', validateEnvironmentName)
|
||||
}),
|
||||
|
||||
@@ -34,7 +34,10 @@ const ImportEnvironment = ({ onClose }) => {
|
||||
.then(() => {
|
||||
toast.success('Global Environment imported successfully');
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while importing the environment'));
|
||||
.catch((error) => {
|
||||
toast.error('An error occurred while importing the environment');
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
@@ -3,10 +3,10 @@ import Portal from 'components/Portal/index';
|
||||
import Modal from 'components/Modal/index';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { renameGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
|
||||
const RenameEnvironment = ({ onClose, environment }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -19,7 +19,11 @@ const RenameEnvironment = ({ onClose, environment }) => {
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.max(255, 'Must be 255 characters or less')
|
||||
.test('is-valid-filename', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
|
||||
11
packages/bruno-app/src/components/Help/StyledWrapper.js
Normal file
11
packages/bruno-app/src/components/Help/StyledWrapper.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-weight: 400;
|
||||
font-size: 0.75rem;
|
||||
background-color: ${props => props.theme.infoTip.bg};
|
||||
border: 1px solid ${props => props.theme.infoTip.border};
|
||||
box-shadow: ${props => props.theme.infoTip.boxShadow};
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
40
packages/bruno-app/src/components/Help/index.js
Normal file
40
packages/bruno-app/src/components/Help/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* The InfoTip components needs to be nuked
|
||||
* This component will be the future replacement
|
||||
* We should allow icon and placement props to be passed in
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import HelpIcon from 'components/Icons/Help';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Help = ({ children, width = 200 }) => {
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex items-center relative">
|
||||
<span
|
||||
className="flex items-center"
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
>
|
||||
<HelpIcon size={14}/>
|
||||
</span>
|
||||
{showTooltip && (
|
||||
<StyledWrapper
|
||||
className="absolute z-50 rounded-md p-3"
|
||||
style={{
|
||||
top: '50%',
|
||||
left: 'calc(100% + 8px)',
|
||||
transform: 'translateY(-50%)',
|
||||
width: `${width}px`
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</StyledWrapper>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Help;
|
||||
@@ -8,7 +8,7 @@ const DotIcon = ({ width }) => {
|
||||
className='inline-block'
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor" />
|
||||
<path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" strokeWidth="0" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
20
packages/bruno-app/src/components/Icons/Help/index.js
Normal file
20
packages/bruno-app/src/components/Icons/Help/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
const HelpIcon = ({ size = 14 }) => {
|
||||
return (
|
||||
<svg
|
||||
tabIndex="-1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
fill="currentColor"
|
||||
className="inline-block ml-2 cursor-pointer"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default HelpIcon;
|
||||
104
packages/bruno-app/src/components/Icons/OpenAPILogo/index.js
Normal file
104
packages/bruno-app/src/components/Icons/OpenAPILogo/index.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const OpenApiLogo = () => {
|
||||
return (
|
||||
<svg width="28" height="28" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
style={{
|
||||
fill: "#91d400",
|
||||
fillOpacity: 1,
|
||||
fillRule: "nonzero",
|
||||
stroke: "none"
|
||||
}} d="M43.125 51.148H20.781l.012.325c.012.21.027.418.039.625.004.09.008.18.016.27a41.442 41.442 0 0 0 .164 1.687c0 .027.004.05.008.078.035.285.07.574.113.86 0 .003 0 .007.004.01a36.98 36.98 0 0 0 1.152 5.255c.004.008.008.012.008.02a27.978 27.978 0 0 0 .265.859c.004.015.012.031.016.047.078.242.164.484.246.73.024.059.043.121.067.184.074.207.148.418.226.629.04.093.074.187.11.285.07.172.136.343.203.52.054.128.11.257.164.39.054.133.113.27.168.406.074.164.148.328.218.492.047.102.09.2.133.297.09.195.184.395.278.59l.093.191c.11.227.223.45.336.672.02.035.035.07.051.102a36.344 36.344 0 0 0 .41.773c.028.051.059.102.086.149L44.45 56.156l.07-.043a15.031 15.031 0 0 1-1.394-4.965Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: "#91d400",
|
||||
fillOpacity: 1,
|
||||
fillRule: "nonzero",
|
||||
stroke: "none"
|
||||
}} d="m22.563 61.137-.727.207.008.02zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: "#4c5930",
|
||||
fillOpacity: 1,
|
||||
fillRule: "nonzero",
|
||||
stroke: "none"
|
||||
}} d="m48.613 61.355-.05.055-15.739 15.664c.082.074.16.149.242.223.149.133.297.266.446.394.078.067.152.137.23.204.18.152.36.3.54.449.046.043.097.082.144.12a21.669 21.669 0 0 0 .691.556c.227.175.45.343.676.515.012.008.02.012.027.02.95.707 1.93 1.367 2.946 1.98.035.024.07.043.105.067l.578.34c.117.066.239.132.356.203.113.062.222.125.336.187.207.11.41.223.617.328a35.567 35.567 0 0 0 1.824.887l.559-1.348 7.918-19.133.027-.07a15.337 15.337 0 0 1-2.473-1.64Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: "#68a338",
|
||||
fillOpacity: 1,
|
||||
fillRule: "nonzero",
|
||||
stroke: "none"
|
||||
}} d="M46.977 59.797a16.778 16.778 0 0 1-.899-1.102c-.152-.203-.3-.406-.437-.617a15.93 15.93 0 0 1-.41-.633L26.124 68.902c.297.485.602.957.914 1.422.012.016.02.035.031.051l.012.016c.008.015.02.03.027.046.004.004.004.004.004.008.028.035.051.07.078.11 0 .004 0 .004.004.007v.004a35.121 35.121 0 0 0 1.051 1.465c.008.012.016.02.02.031.156.2.308.403.464.602.024.027.043.05.063.078.164.203.328.406.496.61.04.046.078.093.117.144.153.18.305.36.457.535.067.078.137.153.203.227.13.148.262.297.395.445.074.082.152.16.226.242.032.035.067.075.102.11l.293.316.121.121c.176.184.352.363.531.54l15.758-15.684a22.18 22.18 0 0 1-.515-.551Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: "#4c5930",
|
||||
fillOpacity: 1,
|
||||
fillRule: "nonzero",
|
||||
stroke: "none"
|
||||
}} d="M67.867 61.348c-.176.136-.347.273-.527.406l.039.066L78.87 80.805a38.54 38.54 0 0 0 1.57-1.078 38.099 38.099 0 0 0 3.227-2.653L67.93 61.41Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: "#91d400",
|
||||
fillOpacity: 1,
|
||||
fillRule: "nonzero",
|
||||
stroke: "none"
|
||||
}} d="m77.418 81.707.023-.012-.023.012zm.023-.012c.051-.027.102-.054.153-.086l-.004-.004c-.05.032-.098.06-.149.09zm-.023.012-.008.004zm-.039-.039.027.047zm.039.039.023-.012-.023.012zm-.016.012.004-.004zm.008-.009-.004.005c.004-.004.008-.004.012-.008-.004.004-.004.004-.008.004zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: "#91d400",
|
||||
fillOpacity: 1,
|
||||
fillRule: "nonzero",
|
||||
stroke: "none"
|
||||
}} d="M77.441 81.695c.051-.03.102-.054.153-.086-.051.032-.102.059-.153.086zm.149-.09.004.004zm-.2.118.005-.004zm-.19-.763-.391-.644-10.727-17.722c-.215.133-.437.25-.66.367-.223.121-.45.23-.676.34a15.303 15.303 0 0 1-6.523 1.469c-1.465 0-2.93-.211-4.344-.63-.238-.074-.473-.167-.711-.25-.238-.085-.48-.156-.715-.253l-7.91 19.117-.309.75-.265.644h-.004l.062.024c.024.012.043.016.067.027h.004c.004 0 .007.004.011.004.188.078.375.145.563.215.238.094.473.184.707.27.121.042.238.097.36.136a38.13 38.13 0 0 0 7.648 1.824c.105.012.207.028.308.04l.32.035c.2.023.4.047.602.066l.149.012c.25.023.496.043.742.062.082.004.168.008.25.016.219.016.433.027.648.039.133.004.266.008.399.016.172.004.343.011.515.015.246.008.496.008.746.012h.176c2.082 0 4.164-.172 6.223-.516l.105-.015c.215-.04.434-.078.653-.117.125-.024.246-.047.37-.075.126-.023.255-.05.384-.078.21-.043.421-.09.632-.14l.118-.024a37.8 37.8 0 0 0 8.992-3.34c.187-.097.367-.207.554-.308.22-.121.438-.246.657-.371.152-.086.308-.164.457-.254l.004-.004h.004l.003-.004c.004 0 .004 0 .004-.004l-.027-.047.027.047h.004c.004-.004.008-.004.008-.004.008-.008.016-.012.023-.016.051-.03.102-.058.149-.09zM48.625 37.938c.172-.141.348-.274.523-.407l-.039-.066L37.621 18.48c-.535.348-1.062.704-1.578 1.082a37.213 37.213 0 0 0-3.219 2.649l15.739 15.664Zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: "#4c5930",
|
||||
fillOpacity: 1,
|
||||
fillRule: "nonzero",
|
||||
stroke: "none"
|
||||
}} d="M31.73 23.254c-.18.18-.347.363-.523.543-.172.18-.352.36-.523.543a38.163 38.163 0 0 0-3.32 4.125c-.106.156-.216.312-.321.469-.11.164-.219.328-.324.496-.04.058-.078.12-.117.18a37.099 37.099 0 0 0-5.82 18.527c-.009.25-.016.504-.02.754s-.012.5-.012.75h22.29c0-.25.023-.5.034-.75.012-.254.016-.504.043-.754a15.002 15.002 0 0 1 3.36-8.078c.16-.192.34-.375.507-.559.172-.188.329-.379.508-.559zm45.993-5.508a46.43 46.43 0 0 0-.684-.402c-.117-.067-.23-.133-.348-.196-.117-.066-.23-.128-.347-.195-.2-.11-.403-.219-.606-.324-.031-.016-.062-.031-.093-.05a38.014 38.014 0 0 0-4.02-1.798c-.035-.015-.074-.027-.11-.039a37.527 37.527 0 0 0-8.41-2.102c-.101-.015-.207-.027-.312-.042l-.313-.036a31.754 31.754 0 0 0-.777-.078c-.238-.023-.48-.043-.719-.062-.093-.004-.187-.012-.28-.016-.204-.015-.415-.027-.618-.039l-.328-.012v22.239c1.144.117 2.281.363 3.383.734l16.445-16.367a41.117 41.117 0 0 0-1.863-1.215zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: "#68a338",
|
||||
fillOpacity: 1,
|
||||
fillRule: "nonzero",
|
||||
stroke: "none"
|
||||
}} d="m38.898 17.68.391.644zm18.59-5.34c-.25.004-.504.004-.754.015a38.117 38.117 0 0 0-4.71.48c-.036.009-.07.013-.106.02-.219.036-.434.079-.652.118l-.371.07c-.13.027-.258.05-.383.082a36.99 36.99 0 0 0-.75.164 37.925 37.925 0 0 0-8.996 3.336c-.184.098-.368.21-.551.312-.219.118-.438.243-.66.368-.16.093-.325.18-.489.277h-.003a.162.162 0 0 1-.036.02c-.043.027-.086.046-.129.074l.004.004.387.644 11.117 18.367c.215-.132.438-.25.66-.367a15.15 15.15 0 0 1 5.668-1.73c.25-.028.5-.047.754-.063.25-.011.504-.023.758-.023V12.324c-.254 0-.504.008-.758.016zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: "#4c5930",
|
||||
fillOpacity: 1,
|
||||
fillRule: "nonzero",
|
||||
stroke: "none"
|
||||
}} d="m95.695 47.809-.035-.598c-.008-.102-.012-.2-.02-.3l-.058-.704c-.008-.059-.012-.121-.016-.18a53.501 53.501 0 0 0-.086-.785c-.003-.023-.003-.043-.007-.062 0-.012 0-.02-.004-.032-.035-.28-.07-.562-.114-.843 0-.012 0-.02-.003-.028a36.772 36.772 0 0 0-1.153-5.246 47.929 47.929 0 0 0-.258-.832c-.011-.035-.023-.07-.03-.105-.083-.242-.161-.48-.247-.719-.023-.063-.043-.129-.066-.195a39.302 39.302 0 0 0-.34-.91c-.067-.172-.133-.344-.2-.512-.054-.137-.109-.27-.163-.403-.055-.132-.114-.261-.168-.394l-.223-.504-.129-.285c-.09-.2-.188-.402-.281-.602-.032-.058-.059-.12-.086-.18-.113-.23-.227-.456-.34-.683-.016-.031-.031-.062-.05-.094-.13-.25-.259-.5-.395-.746l-.012-.023a36.958 36.958 0 0 0-2.133-3.446L72.628 44.77c.376 1.097.618 2.23.735 3.37h22.344l-.012-.331zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: "#68a338",
|
||||
fillOpacity: 1,
|
||||
fillRule: "nonzero",
|
||||
stroke: "none"
|
||||
}} d="M73.45 49.64c0 .255-.024.505-.036.755-.012.25-.016.503-.043.753a15.002 15.002 0 0 1-3.36 8.079c-.16.191-.335.375-.507.558-.168.188-.328.38-.508.559L84.758 76.03c.18-.18.347-.363.523-.543.172-.183.352-.363.52-.547a36.965 36.965 0 0 0 3.195-3.937c.04-.055.074-.11.11-.16.117-.168.234-.34.347-.508.102-.152.203-.3.3-.453.048-.074.095-.153.142-.223a37.055 37.055 0 0 0 5.808-18.515c.012-.25.016-.5.024-.75.003-.25.011-.5.011-.754zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
|
||||
/>
|
||||
<path
|
||||
style={{
|
||||
fill: "#3f3f42",
|
||||
fillOpacity: 1,
|
||||
fillRule: "nonzero",
|
||||
stroke: "none"
|
||||
}} d="M102.36 5.734c-4.079-4.058-10.688-4.058-14.766 0-3.254 3.235-3.903 8.075-1.965 11.961l-22.746 22.64c-3.906-1.925-8.77-1.28-12.024 1.958-4.078 4.059-4.074 10.64 0 14.7 4.082 4.058 10.692 4.054 14.77 0a10.356 10.356 0 0 0 1.965-11.966l22.746-22.64c3.906 1.93 8.765 1.281 12.02-1.957a10.355 10.355 0 0 0 0-14.696zm0 0" transform="translate(-26.793 -.606) scale(1.44332)"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenApiLogo;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Tooltip as ReactInfoTip } from 'react-tooltip';
|
||||
|
||||
const InfoTip = ({ text, infotipId }) => {
|
||||
const InfoTip = ({ html: _ignored, infotipId, ...props }) => {
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
@@ -17,7 +17,7 @@ const InfoTip = ({ text, infotipId }) => {
|
||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||
<path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z" />
|
||||
</svg>
|
||||
<ReactInfoTip anchorId={infotipId} html={text} />
|
||||
<ReactInfoTip anchorId={infotipId} {...props} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,14 +15,14 @@ const StyledMarkdownBodyWrapper = styled.div`
|
||||
margin: 0.67em 0;
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
padding-bottom: 0.3em;
|
||||
font-size: 1.3;
|
||||
font-size: 1.3em;
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
padding-bottom: 0.3em;
|
||||
font-size: 1.2;
|
||||
font-size: 1.2em;
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user