mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 17:38:36 +00:00
Compare commits
515 Commits
v2.12.0
...
feat/theme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19840f8546 | ||
|
|
d03d8f01a1 | ||
|
|
97c700beba | ||
|
|
b6a27bc66c | ||
|
|
76a2889206 | ||
|
|
d506c37516 | ||
|
|
0c4ad0ed60 | ||
|
|
30dbe34e2e | ||
|
|
c83c055654 | ||
|
|
33f47ca5e4 | ||
|
|
475707848e | ||
|
|
2d76c444f6 | ||
|
|
33361ba659 | ||
|
|
1e3a0d9af3 | ||
|
|
2a5ec854cc | ||
|
|
d758645144 | ||
|
|
902d9ff968 | ||
|
|
781def844d | ||
|
|
15065eb2f1 | ||
|
|
481fa4ed12 | ||
|
|
a35b455041 | ||
|
|
bfc8968e24 | ||
|
|
f90f256f5f | ||
|
|
4253080f6b | ||
|
|
7a83641436 | ||
|
|
7105e4e8ed | ||
|
|
849465d62a | ||
|
|
058d2e0e61 | ||
|
|
c03c5eb927 | ||
|
|
156e798f90 | ||
|
|
a1a90d19e8 | ||
|
|
ec40d7fc85 | ||
|
|
5fda0866f8 | ||
|
|
f611c05fe8 | ||
|
|
b3764e1703 | ||
|
|
f961e692ee | ||
|
|
a7237c2e41 | ||
|
|
3ccaf29ddd | ||
|
|
002a5d16eb | ||
|
|
877b4dcf3a | ||
|
|
b9856f8c64 | ||
|
|
e63bac6ce4 | ||
|
|
23e809e827 | ||
|
|
1a4a30c8f2 | ||
|
|
2c973bbd35 | ||
|
|
8e74fa6233 | ||
|
|
1ec8f55a9e | ||
|
|
1ae05dfb0e | ||
|
|
72f186b38a | ||
|
|
ea1002c7a0 | ||
|
|
89ed1da4de | ||
|
|
4d519df8bc | ||
|
|
71413b9154 | ||
|
|
ce9773b7c9 | ||
|
|
8a394cdafc | ||
|
|
ddc88b3b05 | ||
|
|
746a49faed | ||
|
|
2827a6f133 | ||
|
|
1ed9d61ee8 | ||
|
|
ac0b69787d | ||
|
|
ab04850367 | ||
|
|
1c9db0886d | ||
|
|
b75c9fdd6d | ||
|
|
27dff7567c | ||
|
|
8fa8ae5fed | ||
|
|
0848393319 | ||
|
|
76f8bce9ac | ||
|
|
676f8223ec | ||
|
|
36b0a90de3 | ||
|
|
d7cef7aa4e | ||
|
|
5dad137631 | ||
|
|
8b1d59fa74 | ||
|
|
3a6f2f26ee | ||
|
|
6f71717105 | ||
|
|
2b28d37c74 | ||
|
|
7675c1a4d8 | ||
|
|
6afbaa0d91 | ||
|
|
c0ac24d090 | ||
|
|
41b37c7805 | ||
|
|
6d77cacbc4 | ||
|
|
0d536fb365 | ||
|
|
9a78432dc0 | ||
|
|
63d31825ff | ||
|
|
018f39239f | ||
|
|
1b57b6bee6 | ||
|
|
646f63dbeb | ||
|
|
c714e9b5d6 | ||
|
|
f5ed96ad16 | ||
|
|
f40e4d2d79 | ||
|
|
84f572fa88 | ||
|
|
faec95f623 | ||
|
|
cd6ffc2447 | ||
|
|
d37bf7a5ad | ||
|
|
b41f4974f9 | ||
|
|
2446301e41 | ||
|
|
67903f26bc | ||
|
|
1b8eece173 | ||
|
|
1f05ffd469 | ||
|
|
c2acc25461 | ||
|
|
dc9df80638 | ||
|
|
c5abe4122b | ||
|
|
3081c06964 | ||
|
|
8c7ed3fe51 | ||
|
|
ce33cee03d | ||
|
|
d93d1eacdb | ||
|
|
aeb6b12b06 | ||
|
|
41ed51b4e3 | ||
|
|
b85f60e1d6 | ||
|
|
49ffdd1b8f | ||
|
|
f1961a8988 | ||
|
|
4831434e37 | ||
|
|
87c8934c45 | ||
|
|
01d4d3dc2a | ||
|
|
70178f60b3 | ||
|
|
cba164bc9b | ||
|
|
669c99f40a | ||
|
|
9967d863f5 | ||
|
|
3552801ca5 | ||
|
|
6f2804ea0f | ||
|
|
41efa8505b | ||
|
|
f47e9e9304 | ||
|
|
5f88e7d201 | ||
|
|
e4e17b0c74 | ||
|
|
83feffd41d | ||
|
|
7d783d473f | ||
|
|
6a177e17d3 | ||
|
|
3552e7e609 | ||
|
|
7164119695 | ||
|
|
6a05b04676 | ||
|
|
8c1975ba7b | ||
|
|
397ccbb425 | ||
|
|
336496a1d7 | ||
|
|
aadbf8c33f | ||
|
|
c5827dfa72 | ||
|
|
9738a2afb7 | ||
|
|
a1c4113897 | ||
|
|
052d143d6e | ||
|
|
aac219d4cd | ||
|
|
b188a9e9a9 | ||
|
|
6ab8fcb710 | ||
|
|
1cc117ceb9 | ||
|
|
62b8784972 | ||
|
|
5e6444b8b5 | ||
|
|
bc2efb9686 | ||
|
|
678fa88a7c | ||
|
|
80e09d1a26 | ||
|
|
78ee99eab9 | ||
|
|
73124fd715 | ||
|
|
4c1fba611a | ||
|
|
3cfbf890ac | ||
|
|
395aa4246e | ||
|
|
639c8e573f | ||
|
|
7d317a775b | ||
|
|
2eb8db9b45 | ||
|
|
30d2a6d141 | ||
|
|
231776ca4b | ||
|
|
dbd966850c | ||
|
|
dc111ecce2 | ||
|
|
fdff792476 | ||
|
|
a9c63e6f2a | ||
|
|
014817810d | ||
|
|
71cf1a8f26 | ||
|
|
a769ca3ae4 | ||
|
|
3d61106cc1 | ||
|
|
6cc114100f | ||
|
|
c11266a96f | ||
|
|
8b0f41e3cb | ||
|
|
1b9ea478da | ||
|
|
8cbda5f5cc | ||
|
|
2f5537c8db | ||
|
|
2327b21c85 | ||
|
|
6652cca642 | ||
|
|
575f37124c | ||
|
|
50a72a16bc | ||
|
|
98513c65f0 | ||
|
|
b61d2212f6 | ||
|
|
1ed957978a | ||
|
|
c00cbf6cb2 | ||
|
|
632f8705e5 | ||
|
|
f8548225e1 | ||
|
|
7fe6b47aa0 | ||
|
|
43f24ad0f1 | ||
|
|
a798b32f25 | ||
|
|
4d1c3f9e52 | ||
|
|
879d2271b7 | ||
|
|
cf4c896431 | ||
|
|
f6363389d0 | ||
|
|
03e8f2d67d | ||
|
|
8e855e53bf | ||
|
|
599636d56b | ||
|
|
9b9534c1eb | ||
|
|
0197ae37c8 | ||
|
|
cf969dfcd6 | ||
|
|
a66be21523 | ||
|
|
4016754d71 | ||
|
|
f3aebf6374 | ||
|
|
f87460b00e | ||
|
|
354e8d7496 | ||
|
|
dc107f8b96 | ||
|
|
cd0f1e45ba | ||
|
|
33022843f2 | ||
|
|
facdf3264a | ||
|
|
4ffb447c53 | ||
|
|
3e5ae613f5 | ||
|
|
42bef4ae1e | ||
|
|
e93e545b81 | ||
|
|
4a8d787f31 | ||
|
|
f5211f6a08 | ||
|
|
57222d2500 | ||
|
|
f479e0d325 | ||
|
|
5302addda0 | ||
|
|
80b017f224 | ||
|
|
b18d582004 | ||
|
|
109394c65b | ||
|
|
c355153f26 | ||
|
|
b87a02beb3 | ||
|
|
4624ffb116 | ||
|
|
a9ce97fb1b | ||
|
|
72ce6cadeb | ||
|
|
c4ff2918a2 | ||
|
|
9972eb3de6 | ||
|
|
ebe0203415 | ||
|
|
b3ef91fe8e | ||
|
|
6786f19d04 | ||
|
|
05fe8b1b27 | ||
|
|
b5722bf11c | ||
|
|
2b8da39bcf | ||
|
|
38ba53be9f | ||
|
|
9159f523d9 | ||
|
|
a3d2d35d2e | ||
|
|
9caef9e573 | ||
|
|
893058067d | ||
|
|
4a38f2d49f | ||
|
|
d56e4f625b | ||
|
|
9bbcf7ecbe | ||
|
|
ee4c923bc5 | ||
|
|
bc82536a82 | ||
|
|
b95ef99ef2 | ||
|
|
2a251b1a62 | ||
|
|
06a024a1d9 | ||
|
|
8cee7bad39 | ||
|
|
bc4062b950 | ||
|
|
b3ffc904ad | ||
|
|
af707de684 | ||
|
|
be94224cfd | ||
|
|
786a3414b8 | ||
|
|
7de56bd85c | ||
|
|
4ce5debc4c | ||
|
|
8716e2b2a6 | ||
|
|
9d6486ba3e | ||
|
|
dd72ee5d77 | ||
|
|
7f204a8769 | ||
|
|
32990db3fb | ||
|
|
4f8d2c0c67 | ||
|
|
8c06a229e9 | ||
|
|
f367ea5a89 | ||
|
|
55a6af1ce3 | ||
|
|
e9efcb48ac | ||
|
|
fa94efaa24 | ||
|
|
1b2df9fba4 | ||
|
|
7ee366eb81 | ||
|
|
59514127d5 | ||
|
|
9d98eb86c4 | ||
|
|
bb0096eb38 | ||
|
|
6e88671788 | ||
|
|
d17048f80c | ||
|
|
172479edad | ||
|
|
486b91894c | ||
|
|
ca8ef36f9f | ||
|
|
7ed474c8ba | ||
|
|
086d0d98ef | ||
|
|
b0405b1e1a | ||
|
|
c2d000e805 | ||
|
|
6aaccabc04 | ||
|
|
daf23c9e2d | ||
|
|
f952688032 | ||
|
|
f429fa94e3 | ||
|
|
fb420fcea4 | ||
|
|
cc3d6a961a | ||
|
|
27c37192b2 | ||
|
|
faa2ef5de2 | ||
|
|
c05d56fd21 | ||
|
|
b4d19ab8ca | ||
|
|
0cedf48e68 | ||
|
|
4e7bc1a351 | ||
|
|
9d3c8b2401 | ||
|
|
39dfd8d360 | ||
|
|
460832f3ed | ||
|
|
50442d960d | ||
|
|
2ac41806a2 | ||
|
|
e9111c0529 | ||
|
|
48a09f6f50 | ||
|
|
e613e4cbcd | ||
|
|
4631eda281 | ||
|
|
3f7ab31b2b | ||
|
|
27a7b623c7 | ||
|
|
95bc670d8c | ||
|
|
6d8f428140 | ||
|
|
ed18cb6d90 | ||
|
|
bb83fbfb9d | ||
|
|
ddfdeda4d6 | ||
|
|
adb0b90457 | ||
|
|
8c7888533a | ||
|
|
0a188575a0 | ||
|
|
2be602d16c | ||
|
|
8ec1925b9f | ||
|
|
d28f2f32e9 | ||
|
|
76a1532695 | ||
|
|
efad149afc | ||
|
|
2d2a17c90f | ||
|
|
3d8d93f20d | ||
|
|
94c33e6833 | ||
|
|
f7ea1f8dbb | ||
|
|
2ef451c80b | ||
|
|
044fcce49f | ||
|
|
dffb600dab | ||
|
|
99478b7068 | ||
|
|
252fd386b7 | ||
|
|
b982f6db16 | ||
|
|
3b4e5686b8 | ||
|
|
2ef1a1948b | ||
|
|
f2273821b0 | ||
|
|
8a22f6acb8 | ||
|
|
6049530634 | ||
|
|
5784b04129 | ||
|
|
fec37f43e0 | ||
|
|
cf19035b0b | ||
|
|
b8fef7b796 | ||
|
|
04f8dba1b1 | ||
|
|
d9a3f74cb7 | ||
|
|
cd1500bd01 | ||
|
|
e8a8b5d220 | ||
|
|
bc3dfc59f6 | ||
|
|
2c399ca33c | ||
|
|
ccac4d6112 | ||
|
|
fc5093eab4 | ||
|
|
631b05330d | ||
|
|
be34c86c47 | ||
|
|
67c9f1373e | ||
|
|
6628f95677 | ||
|
|
44ed0b01d8 | ||
|
|
45cfbc5c49 | ||
|
|
14bece8696 | ||
|
|
9e19244665 | ||
|
|
f439f2de9a | ||
|
|
e844d35b03 | ||
|
|
26e140aca0 | ||
|
|
7bd6a9a915 | ||
|
|
e1045372d5 | ||
|
|
914b858024 | ||
|
|
36e9a9c137 | ||
|
|
995899dedb | ||
|
|
408dd6bccf | ||
|
|
ab7ead91d5 | ||
|
|
a186df3ac4 | ||
|
|
3fe5299d8e | ||
|
|
57c08350b6 | ||
|
|
68b2625259 | ||
|
|
1719cee440 | ||
|
|
ab4e9eb3bd | ||
|
|
b15c421270 | ||
|
|
24a36bc355 | ||
|
|
1ac128e35c | ||
|
|
4510cc3414 | ||
|
|
f3cb0d4bae | ||
|
|
7b183887ce | ||
|
|
fa28ab9b50 | ||
|
|
60b437ef9d | ||
|
|
1ef8852a01 | ||
|
|
17cc70f36e | ||
|
|
2deee11718 | ||
|
|
bdc8f391b7 | ||
|
|
1656e951fb | ||
|
|
d8adb59d04 | ||
|
|
de05fb6137 | ||
|
|
3e3884a6af | ||
|
|
23843bb621 | ||
|
|
c85a1ec1a5 | ||
|
|
68cbb7d9df | ||
|
|
396ff2b196 | ||
|
|
6826e98945 | ||
|
|
e47d1ed353 | ||
|
|
08c182a875 | ||
|
|
f3afb4bf84 | ||
|
|
21e8615247 | ||
|
|
6e8751a27a | ||
|
|
c9a96ee94f | ||
|
|
b69db7b44b | ||
|
|
73caaef42b | ||
|
|
e68b2ae3b7 | ||
|
|
cc7f1ea58f | ||
|
|
6e8cd55b76 | ||
|
|
384aabf2af | ||
|
|
a15dcdb133 | ||
|
|
18848cdb26 | ||
|
|
29b90a7e0d | ||
|
|
4fbe371eb0 | ||
|
|
6fd2b8be6d | ||
|
|
be7f92d77f | ||
|
|
c5325c732f | ||
|
|
a538b27f24 | ||
|
|
77bb8f40fe | ||
|
|
8f1f5e3861 | ||
|
|
e9251a1f3f | ||
|
|
3a011b2a18 | ||
|
|
fa1498e2a8 | ||
|
|
77681ca51e | ||
|
|
045141efaf | ||
|
|
c997b91698 | ||
|
|
986d5b0b2a | ||
|
|
a2a521477a | ||
|
|
8e70adcbf9 | ||
|
|
87296776fa | ||
|
|
9df70cd759 | ||
|
|
8f9fb3b3c9 | ||
|
|
6d018f5648 | ||
|
|
789d0b23c0 | ||
|
|
81e1e403e4 | ||
|
|
ad2add4026 | ||
|
|
02554c3ad9 | ||
|
|
62815e3429 | ||
|
|
9859b69559 | ||
|
|
440c688bbb | ||
|
|
416eb754b7 | ||
|
|
b85d6efa60 | ||
|
|
19dea18629 | ||
|
|
636901c23d | ||
|
|
a4b1941817 | ||
|
|
7d8fde9180 | ||
|
|
4197304bf9 | ||
|
|
b75422a010 | ||
|
|
e9f03c46c7 | ||
|
|
73e828621f | ||
|
|
2becf49542 | ||
|
|
4c3a9928bc | ||
|
|
b694a41c96 | ||
|
|
ff9a4d97e3 | ||
|
|
6ab6e5ed57 | ||
|
|
3837a7612c | ||
|
|
6589dc51cd | ||
|
|
509f4da667 | ||
|
|
9d2b070ed9 | ||
|
|
d0c524cd9a | ||
|
|
74f0f67795 | ||
|
|
45664bdb65 | ||
|
|
98cb2df3fe | ||
|
|
d478102b30 | ||
|
|
924bc2e79e | ||
|
|
c2d40fe99f | ||
|
|
944674d208 | ||
|
|
0c30357b01 | ||
|
|
ce40949564 | ||
|
|
c6ce40c245 | ||
|
|
6890bbee70 | ||
|
|
4993c61e29 | ||
|
|
a66e849cfb | ||
|
|
9f47200e7b | ||
|
|
10739c32c4 | ||
|
|
c1853e613b | ||
|
|
c393dfe5d6 | ||
|
|
cf17539a47 | ||
|
|
608a9d1954 | ||
|
|
3a04d43ffe | ||
|
|
5c9a391cc6 | ||
|
|
df4b7c1337 | ||
|
|
db6a639c15 | ||
|
|
85319769a5 | ||
|
|
8d2f087206 | ||
|
|
1cc3a6432a | ||
|
|
28907a203f | ||
|
|
6204e90e9c | ||
|
|
1d0ba135ff | ||
|
|
3c72975314 | ||
|
|
3fa9fea6a4 | ||
|
|
239f1dc9f5 | ||
|
|
28e37d8f6f | ||
|
|
8b28070695 | ||
|
|
4ae55b8f1a | ||
|
|
8bad0262c6 | ||
|
|
c7029d1cda | ||
|
|
bb44d9e193 | ||
|
|
14966f6e6c | ||
|
|
56f0741121 | ||
|
|
b1840d189d | ||
|
|
aacb1e0b8e | ||
|
|
fa0f3b3b7b | ||
|
|
2a00add966 | ||
|
|
41e0615f77 | ||
|
|
191a997b05 | ||
|
|
123fe7d542 | ||
|
|
187f5ca011 | ||
|
|
e1b4043ca5 | ||
|
|
9c9cfdf0b2 | ||
|
|
daf6a6d5d6 | ||
|
|
95a2ca9558 | ||
|
|
f359303927 | ||
|
|
65f52961c5 | ||
|
|
2a3db96c9b | ||
|
|
a1a7c9a136 | ||
|
|
c15d47c0dc | ||
|
|
e4f8945e89 | ||
|
|
e6c136d2bb | ||
|
|
6f8c543ee3 | ||
|
|
40b44de294 | ||
|
|
f24e1e78fe | ||
|
|
e57162b79a | ||
|
|
1de9203dd5 | ||
|
|
cffa37ed50 | ||
|
|
bcf61f507a | ||
|
|
325b573da9 | ||
|
|
4b5c7dcca6 | ||
|
|
d2888daa88 | ||
|
|
ec9d63219f | ||
|
|
9173ffbdee | ||
|
|
5f112a318d |
66
.coderabbit.yaml
Normal file
66
.coderabbit.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
|
||||
|
||||
language: 'en-US'
|
||||
early_access: false
|
||||
tone_instructions: 'You are an expert code reviewer in TypeScript, JavaScript, NodeJS, and ElectronJS. You work in an enterprise software developer team, providing concise and clear code review advice. You only elaborate or provide detailed explanations when requested.'
|
||||
|
||||
knowledge_base:
|
||||
opt_out: false
|
||||
code_guidelines:
|
||||
enabled: true
|
||||
filePatterns:
|
||||
- '**/CODING_STANDARDS.md'
|
||||
|
||||
reviews:
|
||||
profile: 'chill'
|
||||
request_changes_workflow: false
|
||||
high_level_summary: true
|
||||
poem: true
|
||||
review_status: true
|
||||
collapse_walkthrough: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
drafts: false
|
||||
base_branches: ['main', 'release/*']
|
||||
path_instructions:
|
||||
- path: 'tests/**/**.*'
|
||||
instructions: |
|
||||
Review the following e2e test code written using the Playwright test library. Ensure that:
|
||||
- Follow best practices for Playwright code and e2e automation
|
||||
- Try to reduce usage of `page.waitForTimeout();` in code unless absolutely necessary and the locator cannot be found using existing `expect()` playwright calls
|
||||
- Avoid using `page.pause()` in code
|
||||
- Use locator variables for locators
|
||||
- Avoid using test.only
|
||||
- Use multiple assertions
|
||||
- Promote the use of `test.step` as much as possible so the generated reports are easier to read
|
||||
- Ensure that the `fixtures` like the collections are nested inside the `fixtures` folder
|
||||
|
||||
|
||||
|
||||
**Fixture Example***: Here's an example of possible fixture and test pair
|
||||
```
|
||||
.
|
||||
├── fixtures
|
||||
│ └── collection
|
||||
│ ├── base.bru
|
||||
│ ├── bruno.json
|
||||
│ ├── collection.bru
|
||||
│ ├── ws-test-request-with-headers.bru
|
||||
│ ├── ws-test-request-with-subproto.bru
|
||||
│ └── ws-test-request.bru
|
||||
├── connection.spec.ts # <- Depends on the collection in ./fixtures/collection
|
||||
├── headers.spec.ts
|
||||
├── persistence.spec.ts
|
||||
├── variable-interpolation
|
||||
│ ├── fixtures
|
||||
│ │ └── collection
|
||||
│ │ ├── environments
|
||||
│ │ ├── bruno.json
|
||||
│ │ └── ws-interpolation-test.bru
|
||||
│ ├── init-user-data
|
||||
│ └── variable-interpolation.spec.ts # <- Depends on the collection in ./variable-interpolation/fixtures/collection
|
||||
└── subproto.spec.ts
|
||||
```
|
||||
|
||||
chat:
|
||||
auto_reply: true
|
||||
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,9 +1,10 @@
|
||||
# Description
|
||||
### Description
|
||||
|
||||
<!-- Explain here the changes your PR introduces and text to help us understand the context of this change. -->
|
||||
|
||||
### Contribution Checklist:
|
||||
#### Contribution Checklist:
|
||||
|
||||
- [ ] **I've used AI significantly to create this pull request**
|
||||
- [ ] **The pull request only addresses one issue or adds one feature.**
|
||||
- [ ] **The pull request does not introduce any breaking changes**
|
||||
- [ ] **I have added screenshots or gifs to help explain the change if applicable.**
|
||||
@@ -12,6 +13,6 @@
|
||||
|
||||
Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.
|
||||
|
||||
### Publishing to New Package Managers
|
||||
#### Publishing to New Package Managers
|
||||
|
||||
Please see [here](../publishing.md) for more information.
|
||||
|
||||
@@ -23,4 +23,5 @@ runs:
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
npm run build:schema-types
|
||||
npm run build:bruno-filestore
|
||||
|
||||
4
.github/workflows/npm-bru-cli.yml
vendored
4
.github/workflows/npm-bru-cli.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
run: |
|
||||
cd packages/bruno-tests/collection
|
||||
npm install
|
||||
bru run --env Prod --output junit.xml --format junit
|
||||
bru run --env Prod --output junit.xml --format junit --sandbox developer
|
||||
|
||||
- name: Publish Test Report
|
||||
uses: dorny/test-reporter@v2
|
||||
|
||||
6
.github/workflows/ssl-tests.yml
vendored
6
.github/workflows/ssl-tests.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node Dependencies
|
||||
uses: ./.github/actions/common/setup-node-deps
|
||||
|
||||
15
.github/workflows/tests.yml
vendored
15
.github/workflows/tests.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -30,10 +30,13 @@ jobs:
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
npm run build --workspace=packages/bruno-schema-types
|
||||
npm run build --workspace=packages/bruno-filestore
|
||||
|
||||
- name: Lint Check
|
||||
run: npm run lint
|
||||
env:
|
||||
ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || 'main' }}
|
||||
|
||||
# tests
|
||||
- name: Test Package bruno-js
|
||||
@@ -64,7 +67,7 @@ jobs:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
@@ -81,6 +84,7 @@ jobs:
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
npm run build --workspace=packages/bruno-schema-types
|
||||
npm run build --workspace=packages/bruno-filestore
|
||||
|
||||
- name: Run Local Testbench
|
||||
@@ -92,7 +96,7 @@ jobs:
|
||||
run: |
|
||||
cd packages/bruno-tests/collection
|
||||
npm install
|
||||
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit
|
||||
node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit --sandbox developer
|
||||
|
||||
- name: Publish Test Report
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
@@ -106,7 +110,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: v22.11.x
|
||||
@@ -132,12 +136,13 @@ jobs:
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
npm run build:schema-types
|
||||
npm run build:bruno-filestore
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
xvfb-run npm run test:e2e
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v6
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -51,3 +51,13 @@ bruno.iml
|
||||
|
||||
# Playwright
|
||||
/blob-report/
|
||||
|
||||
# Development plan files
|
||||
CLAUDE.md
|
||||
*.plan.md
|
||||
|
||||
# packages dist
|
||||
packages/bruno-filestore/dist
|
||||
packages/bruno-requests/dist
|
||||
packages/bruno-schema-types/dist
|
||||
packages/bruno-converters/dist
|
||||
|
||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
||||
npx nano-staged
|
||||
78
CODING_STANDARDS.md
Normal file
78
CODING_STANDARDS.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Bruno Coding Standards
|
||||
|
||||
- No diffs unless an actual change is made, the code changes need to be as minimal as possible, avoid making un-necessary whitespace diffs. This is already handled by eslint but make sure you check your code changes before commiting and raising a PR.
|
||||
|
||||
## General Style Rules
|
||||
|
||||
- Use 2 spaces for indentation. No tabs, just spaces – keeps everything neat and uniform.
|
||||
|
||||
- Stick to single quotes for strings. For JSX/TSX attributes, use double quotes (e.g., <svg xmlns="..." viewBox="...">) to follow React conventions.
|
||||
|
||||
- Always add semicolons at the end of statements. It's like putting a period at the end of a sentence – clarity matters.
|
||||
|
||||
- JSX is enabled, so feel free to use it where it makes sense.
|
||||
|
||||
## Punctuation and Spacing
|
||||
|
||||
- No trailing commas. Keep it clean, no extra commas hanging around.
|
||||
|
||||
- Always use parentheses around parameters in arrow functions. Even for single params – consistency is key.
|
||||
|
||||
- For multiline constructs, put opening braces on the same line, and ensure consistency. Minimum 2 elements for multiline.
|
||||
|
||||
- No newlines inside function parentheses. Keep 'em tight.
|
||||
|
||||
- Space before and after the arrow in arrow functions. `() => {}` is good.
|
||||
|
||||
- No space between function name and parentheses. `func()` not `func ()`.
|
||||
|
||||
- Semicolons go at the end of the line, not on a new line.
|
||||
|
||||
- No strict max length – write readable code, not cramped lines.
|
||||
|
||||
- Multiple expressions per line in JSX are fine – flexibility is nice.
|
||||
|
||||
Remember, these rules are here to make our codebase harmonious. If something doesn't fit perfectly, let's chat about it. Happy coding! 🚀
|
||||
|
||||
|
||||
## Tests
|
||||
|
||||
- Add tests for any new functionality or meaningful changes. If code is added, removed, or significantly modified, corresponding tests should be updated or created.
|
||||
|
||||
- Prioritise high-value tests over maximum coverage. Focus on testing behaviour that is critical, complex, or likely to break—don’t chase coverage numbers for their own sake.
|
||||
|
||||
- Write behaviour-driven tests, not implementation-driven ones. Tests should validate real expected output and observable behaviour, not internal details or mocked-out logic unless absolutely necessary.
|
||||
|
||||
- Minimise mocking unless it meaningfully increases clarity or isolates external dependencies. Prefer real flows where practical; only mock external services, slow systems, or non-deterministic behaviour.
|
||||
|
||||
- Keep tests readable and maintainable. Optimise for clarity over cleverness. Name tests descriptively, keep setup minimal, and avoid unnecessary abstraction.
|
||||
|
||||
- Aim for tests that fail usefully. When a test fails, it should clearly indicate what behaviour broke and why.
|
||||
|
||||
- Cover both the “happy path” and the realistically problematic paths. Validate expected success behaviour, but also validate error handling, edge cases, and degraded-mode behaviour when appropriate.
|
||||
|
||||
- Ensure tests are deterministic and reproducible. No randomness, timing dependencies, or environment-specific assumptions without explicit control.
|
||||
|
||||
- Avoid overfitting tests to current behaviour if future flexibility matters. Only assert what needs to be true, not incidental details.
|
||||
|
||||
- Use consistent patterns and helper utilities where they improve clarity. Prefer shared test utilities over copy-pasted setup code, but only when it actually reduces complexity.
|
||||
|
||||
- Tests should be fast enough to run continuously. Avoid long-running operations unless absolutely necessary; prefer lightweight fixtures and isolated units.
|
||||
|
||||
|
||||
## UI Specific instructions
|
||||
|
||||
### React
|
||||
|
||||
- Use styled component's theme prop to manage CSS colors and not CSS variables when in the context of a styled component or any react component using the styled component
|
||||
- Styled Components are used as wrappers to define both self and children components style, tailwind classes are used specifically for layout based styles.
|
||||
- Styled Component CSS might also change layout but tailwind classes shouldn't define colors.
|
||||
|
||||
## Readability and Abstractions
|
||||
|
||||
- Avoid abstractions unless the exact same code is being used in more than 3 places.
|
||||
- Names for functions need to be concise and descriptive.
|
||||
- Add in JSDoc comments to add more details to the abstractions if needed.
|
||||
- Follow functional programming but just enough to be readable, we don't need to go as deep as ADTs and Monads, we want to keep the code pipeline obvious and easy for everyone to read and contribute to.
|
||||
- Avoid single line abstractions where all that's being done is increasing the call stack with one additional function.
|
||||
- Add in meaningful comments instead of obvious ones where complex code flow is explained properly.
|
||||
@@ -16,6 +16,7 @@
|
||||
| [日本語](docs/contributing/contributing_ja.md)
|
||||
| [हिंदी](docs/contributing/contributing_hi.md)
|
||||
| [Dutch](docs/contributing/contributing_nl.md)
|
||||
| [فارسی](docs/contributing/contributing_fa.md)
|
||||
|
||||
## Let's make Bruno better, together!!
|
||||
|
||||
@@ -69,11 +70,13 @@ npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
npm run build:schema-types
|
||||
npm run build:bruno-filestore
|
||||
|
||||
# bundle js sandbox libraries
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
```
|
||||
|
||||
##### Option 2
|
||||
|
||||
```bash
|
||||
@@ -94,18 +97,22 @@ npm run dev:electron
|
||||
```
|
||||
|
||||
##### Option 2
|
||||
|
||||
```bash
|
||||
# run electron and react app concurrently
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### Customize Electron `userData` path
|
||||
|
||||
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
|
||||
|
||||
e.g.
|
||||
|
||||
```sh
|
||||
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
|
||||
```
|
||||
|
||||
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
92
docs/contributing/contributing_fa.md
Normal file
92
docs/contributing/contributing_fa.md
Normal file
@@ -0,0 +1,92 @@
|
||||
[English](../../contributing.md)
|
||||
|
||||
## با هم، Bruno را بهتر میکنیم!
|
||||
|
||||
خوشحالم که قصد دارید Bruno را بهبود ببخشید. در ادامه قوانین و راهنماها برای راهاندازی Bruno روی سیستم شما آورده شده است.
|
||||
|
||||
### فناوریهای استفادهشده
|
||||
|
||||
به فارسی برونو Bruno با استفاده از Next.js و React ساخته شده است. همچنین از Electron برای بستهبندی نسخه دسکتاپ (که امکان مجموعههای محلی را فراهم میکند) استفاده میکنیم.
|
||||
|
||||
کتابخانههایی که استفاده میکنیم:
|
||||
|
||||
- CSS - Tailwind استایل
|
||||
- Codemirror - ویرایشگر کد
|
||||
- Redux - مدیریت وضعیت
|
||||
- Tabler Icons - آیکونها
|
||||
- formik - فرمها
|
||||
- Yup اعتبارسنجی اسکیمـا
|
||||
- axios - کلاینت درخواست
|
||||
- chokidar - پایشگر سیستم فایل
|
||||
|
||||
### پیشنیازها
|
||||
|
||||
شما به [نود v20.x یا اخرین نسخه پایدار](https://nodejs.org/en/) و npm 8.x نیاز دارید. در این پروژه از فضای کاری npm (npm workspaces) استفاده میکنیم.
|
||||
|
||||
### شروع به کدنویسی
|
||||
|
||||
برای راهاندازی محیط توسعه محلی به فایل [مستندات توسعه](docs/development_fa.md) مراجعه کنید:
|
||||
|
||||
### ارسال Pull Request
|
||||
|
||||
1 - لطفاً Pull Requestها (PR) را کوتاه و متمرکز نگه دارید و تنها یک هدف مشخص را دنبال کنند. </br>
|
||||
2 - لطفاً از فرمت نامگذاری شاخهها استفاده کنید:
|
||||
|
||||
- feature/[name]: این شاخه باید شامل یک قابلیت مشخص باشد.
|
||||
- feature/dark-mode : مثال
|
||||
- bugfix/[name]: این شاخه باید تنها شامل رفع یک باگ مشخص باشد.
|
||||
- bugfix/bug-1 : مثال
|
||||
|
||||
## توسعه
|
||||
|
||||
به فارسی برونو یا Bruno بهصورت یک اپلیکیشن «سنگین» توسعه داده میشود. برای اجرا باید ابتدا Next.js را در یک پنجره ترمینال اجرا کنید و سپس اپلیکیشن Electron را در پنجره ترمینال دیگری راهاندازی نمایید.
|
||||
|
||||
### نیازمندی توسعه
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### اجرای محلی
|
||||
|
||||
```bash
|
||||
# از ورژن NodeJS 18 استفاده کنید
|
||||
nvm use
|
||||
|
||||
# نصب وابستگیها
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# ساخت مستندات GraphQL
|
||||
npm run build:graphql-docs
|
||||
|
||||
# ساخت bruno-query
|
||||
npm run build:bruno-query
|
||||
|
||||
# اجرای اپ Next (ترمینال 1)
|
||||
npm run dev:web
|
||||
|
||||
# اجرای اپ Electron (ترمینال 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### عیبیابی
|
||||
|
||||
ممکن است هنگام اجرای `npm install` خطای `Unsupported platform` ببینید. برای رفع این مشکل، پوشه `node_modules` و فایل `package-lock.json` را حذف کرده و سپس دوباره `npm install` را اجرا کنید. این کار معمولاً همه پکیجهای لازم را نصب میکند.
|
||||
|
||||
```shell
|
||||
# حذف پوشه node_modules در زیردایرکتوریها
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# حذف فایل package-lock.json در زیردایرکتوریها
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### تستها
|
||||
|
||||
```bash
|
||||
# اجرای تستهای schema مربوط به bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# اجرای تستها در همه فضاهای کاری (در صورت وجود)
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
8
docs/publishing/publishing_fa.md
Normal file
8
docs/publishing/publishing_fa.md
Normal file
@@ -0,0 +1,8 @@
|
||||
[English](../../publishing.md)
|
||||
|
||||
### انتشار Bruno در یک پکیج منیجر جدید
|
||||
|
||||
اگرچه کد ما متنباز است و همه میتوانند از آن استفاده کنند، لطفاً قبل از انتشار Bruno در مدیر بستههای جدید با ما تماس بگیرید. به عنوان سازنده Bruno، علامت تجاری `Bruno` را برای این پروژه دارم و مایلم توزیع آن را مدیریت کنم. اگر دوست دارید Bruno را در یک مدیر بسته جدید ببینید، لطفاً یک issue در گیتهاب ثبت کنید.
|
||||
|
||||
اگرچه بیشتر قابلیتهای ما رایگان و متنباز هستند (شامل REST و GraphQL Apis)،
|
||||
ما تلاش میکنیم بین اصول متنباز و توسعه پایدار تعادل مناسبی برقرار کنیم - https://github.com/usebruno/bruno/discussions/269
|
||||
@@ -41,13 +41,6 @@
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### الطبعة الذهبية ✨
|
||||
|
||||
غالبية ميزاتنا مجانية ومفتوحة المصدر.
|
||||
نحن نسعى لتحقيق توازن متناغم بين [مبادئ الشفافية والاستدامة](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
طلبات الشراء لـ [الطبعة الذهبية](https://www.usebruno.com/pricing) ستطلق قريبًا بسعر ~~$19~~ **$9** ! <br/>
|
||||
[اشترك هنا](https://usebruno.ck.page/4c65576bd4) لتصلك إشعارات عند الإطلاق.
|
||||
|
||||
### التثبيت
|
||||
|
||||
|
||||
@@ -37,13 +37,37 @@ Bruno 直接在您的电脑文件夹中存储您的 API 信息。我们使用纯
|
||||
|
||||
Bruno 仅限离线使用。我们计划永不向 Bruno 添加云同步功能。我们重视您的数据隐私,并认为它应该留在您的设备上。阅读我们的长期愿景 [点击查看](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
[下载 Bruno](https://www.usebruno.com/downloads)
|
||||
|
||||
📢 观看我们在印度 FOSS 3.0 会议上的最新演讲 [点击查看](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### 安装
|
||||
## 商业版本 ✨
|
||||
|
||||
Bruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) Mac、Windows 和 Linux 的可执行文件。
|
||||
我们的大多数功能都是免费且开源的。
|
||||
我们致力于在 [开源与可持续性发展](https://github.com/usebruno/bruno/discussions/269) 之间取得和谐的平衡
|
||||
|
||||
欢迎使用我们的 [付费版本](https://www.usebruno.com/pricing) ,看看附加的功能是否对您或团队有所帮助! <br/>
|
||||
|
||||
## 目录
|
||||
- [安装](#安装)
|
||||
- [特性](#特性)
|
||||
- [跨平台使用 🖥️](#跨平台使用-)
|
||||
- [通过Git协作 👩💻🧑💻](#通过git协作-)
|
||||
- [重要链接 📌](#重要链接-)
|
||||
- [展示 🎥](#展示-)
|
||||
- [分享评价 📣](#分享评价-)
|
||||
- [发布到新的包管理器](#发布到新的包管理器)
|
||||
- [联系方式 🌐](#联系方式-)
|
||||
- [商标](#商标)
|
||||
- [贡献 👩💻🧑💻](#贡献-)
|
||||
- [作者](#作者)
|
||||
- [许可证 📄](#许可证-)
|
||||
|
||||
## 安装
|
||||
|
||||
Bruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) 适用于Mac、Windows 和 Linux 的可执行文件。
|
||||
|
||||
您也可以通过包管理器如 Homebrew、Chocolatey、Scoop、Snap 和 Apt 安装 Bruno。
|
||||
|
||||
@@ -58,9 +82,15 @@ choco install bruno
|
||||
scoop bucket add extras
|
||||
scoop install bruno
|
||||
|
||||
# 在 Windows 上用 winget 安装
|
||||
winget install Bruno.Bruno
|
||||
|
||||
# 在 Linux 上用 Snap 安装
|
||||
snap install bruno
|
||||
|
||||
# 在 Linux 上用 Flatpak 安装
|
||||
flatpak install com.usebruno.Bruno
|
||||
|
||||
# 在 Linux 上用 Apt 安装
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
@@ -73,67 +103,50 @@ echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebr
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
### 在 Mac 上通过 Homebrew 安装 🖥️
|
||||
## 特性
|
||||
|
||||
### 跨平台使用 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Collaborate 安装 👩💻🧑💻
|
||||
### 通过Git协作 👩💻🧑💻
|
||||
|
||||
或者任何您选择的版本控制系统
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### 重要链接 📌
|
||||
## 重要链接 📌
|
||||
|
||||
- [我们的愿景](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [路线图](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [路线图](https://www.usebruno.com/roadmap)
|
||||
- [文档](https://docs.usebruno.com)
|
||||
- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)
|
||||
- [网站](https://www.usebruno.com)
|
||||
- [价格](https://www.usebruno.com/pricing)
|
||||
- [下载](https://www.usebruno.com/downloads)
|
||||
- [GitHub 赞助](https://github.com/sponsors/helloanoop).
|
||||
|
||||
### 展示 🎥
|
||||
## 展示 🎥
|
||||
|
||||
- [Testimonials](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### 支持 ❤️
|
||||
|
||||
如果您喜欢 Bruno 并想支持我们的开源工作,请考虑通过 [GitHub Sponsors](https://github.com/sponsors/helloanoop) 来赞助我们。
|
||||
|
||||
### 分享评价 📣
|
||||
## 分享评价 📣
|
||||
|
||||
如果 Bruno 在您的工作和团队中帮助了您,请不要忘记在我们的 GitHub 讨论上分享您的 [评价](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
### 发布到新的包管理器
|
||||
## 发布到新的包管理器
|
||||
|
||||
有关更多信息,请参见 [此处](../publishing/publishing_cn.md) 。
|
||||
如需了解更多信息,请参见 [此处](../publishing/publishing_cn.md) 。
|
||||
|
||||
### 贡献 👩💻🧑💻
|
||||
|
||||
我很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。
|
||||
|
||||
即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。
|
||||
|
||||
### 作者
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### 联系方式 🌐
|
||||
## 联系方式 🌐
|
||||
|
||||
[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />
|
||||
[Website](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[LinkedIn](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### 商标
|
||||
## 商标
|
||||
|
||||
**名称**
|
||||
|
||||
@@ -143,6 +156,20 @@ sudo apt update && sudo apt install bruno
|
||||
|
||||
Logo 源自 [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### 许可证 📄
|
||||
## 贡献 👩💻🧑💻
|
||||
|
||||
很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。
|
||||
|
||||
即使您无法通过代码做出贡献,我们仍然欢迎您提出 BUG 和新的功能需求。
|
||||
|
||||
## 作者
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## 许可证 📄
|
||||
|
||||
[MIT](../../license.md)
|
||||
|
||||
@@ -43,13 +43,6 @@ Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Syn
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Golden Edition ✨
|
||||
|
||||
Die meisten unserer Funktionen sind kostenlos und quelloffen.
|
||||
Wir bemühen uns um ein Gleichgewicht zwischen [Open-Source-Prinzipien und Nachhaltigkeit](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
Du kannst die [Golden Edition](https://www.usebruno.com/pricing) bestellen **$19**! <br/>
|
||||
|
||||
### Installation
|
||||
|
||||
Bruno ist als Download [auf unserer Website](https://www.usebruno.com/downloads) für Mac, Windows und Linux verfügbar.
|
||||
|
||||
@@ -43,13 +43,6 @@ Bruno funciona sin conexión a internet. No tenemos intenciones de añadir sincr
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Golden Edition ✨
|
||||
|
||||
La mayoría de nuestras funcionalidades son gratis y de código abierto.
|
||||
Queremos alcanzar un equilibrio en armonía entre los [principios open-source y la sostenibilidad](https://github.com/usebruno/bruno/discussions/269).
|
||||
|
||||
¡Puedes reservar la [Golden Edition](https://www.usebruno.com/pricing) por ~~$19~~ **$9**! <br/>
|
||||
|
||||
### Instalación
|
||||
|
||||
Bruno está disponible para su descarga [en nuestro sitio web](https://www.usebruno.com/downloads) para Mac, Windows y Linux.
|
||||
|
||||
143
docs/readme/readme_fa.md
Normal file
143
docs/readme/readme_fa.md
Normal file
@@ -0,0 +1,143 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### برونو یا Bruno - محیط توسعه متن باز برای تست و توسعه API ها
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/actions/workflows/tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
[English](../../readme.md)
|
||||
| [Українська](./readme_ua.md)
|
||||
| [Русский](./readme_ru.md)
|
||||
| [Türkçe](./readme_tr.md)
|
||||
| [Deutsch](./readme_de.md)
|
||||
| [Français](./readme_fr.md)
|
||||
| [Português (BR)](./readme_pt_br.md)
|
||||
| [한국어](./readme_kr.md)
|
||||
| [বাংলা](./readme_bn.md)
|
||||
| [Español](./readme_es.md)
|
||||
| **فارسی**
|
||||
| [Română](./readme_ro.md)
|
||||
| [Polski](./readme_pl.md)
|
||||
| [简体中文](./readme_cn.md)
|
||||
| [正體中文](./readme_zhtw.md)
|
||||
| [العربية](./readme_ar.md)
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
|
||||
برونو یک کلاینت API جدید و نوآورانه است که هدفش تغییر وضعیت فعلی ابزارهایی مانند Postman و سایر ابزارهای مشابه است.
|
||||
|
||||
برونو مجموعههای شما را مستقیماً در یک پوشه روی فایلسیستم شما ذخیره میکند. ما از یک زبان نشانهگذاری ساده به نام Bru برای ذخیره اطلاعات درخواستهای API استفاده میکنیم.
|
||||
|
||||
شما میتوانید برای همکاری روی مجموعههای API خود، از Git یا هر سیستم کنترل نسخه دلخواهتان استفاده کنید.
|
||||
|
||||
برونو فقط به صورت آفلاین کار میکند. هیچ برنامهای برای اضافه کردن همگامسازی ابری به برونو در آینده وجود ندارد. ما به حریم خصوصی دادههای شما اهمیت میدهیم و معتقدیم که باید روی دستگاه خودتان باقی بمانند. میتوانید چشمانداز بلندمدت ما را مطالعه کنید. [اینجا (به انگلیسی)](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
📢 جدیدترین ارائه ما را در کنفرانس India FOSS 3.0 تماشا کنید.
|
||||
[اینجا](https://www.youtube.com/watch?v=7bSMFpbcPiY)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### نصب
|
||||
|
||||
برونو به صورت یک فایل باینری برای دانلود در دسترس است. [بر روی وبسایت ما](https://www.usebruno.com/downloads) برای مک لینکوس و ویندوز.
|
||||
|
||||
همچنین میتوانید برونو را از طریق مدیر بستههایی مانند Homebrew، Chocolatey، Snap و Apt نصب کنید.
|
||||
|
||||
```sh
|
||||
# بر روی مک از طریق brew
|
||||
brew install bruno
|
||||
|
||||
# بر روی ویندوز از طریق Chocolatey
|
||||
choco install bruno
|
||||
|
||||
# بر روی لینوکس از طریق Snap
|
||||
snap install bruno
|
||||
|
||||
# بر روی لینوکس از طریق Apt
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo apt update && sudo apt install gpg curl
|
||||
curl -fsSL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266" \
|
||||
| gpg --dearmor \
|
||||
| sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null
|
||||
sudo chmod 644 /etc/apt/keyrings/bruno.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/bruno.list
|
||||
sudo apt update && sudo apt install bruno
|
||||
```
|
||||
|
||||
### روی پلتفرمهای مختلف کار میکند 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### همکاری از طریق گیت 👩💻🧑💻
|
||||
|
||||
یا هر سیستم کنترل نسخهای که ترجیح میدهید
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### لینکهای مهم 📌
|
||||
|
||||
- [آخرین نسخه پایدار ما](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [نقشه راه](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [مستندات](https://docs.usebruno.com)
|
||||
- [وبسایت](https://www.usebruno.com)
|
||||
- [اشتراک ها](https://www.usebruno.com/pricing)
|
||||
- [دانلود](https://www.usebruno.com/downloads)
|
||||
|
||||
### ویدیوها 🎥
|
||||
|
||||
- [تجربه ها](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [مرکز دانش](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [اسکریپ مانیا](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### حمایت ❤️
|
||||
|
||||
جوون! اگر این پروژه را دوست دارید، روی دکمه ⭐ کلیک کنید!
|
||||
|
||||
### تجربههای به اشتراک گذاشتهشده 📣
|
||||
|
||||
اگر برونو به شما یا تیمتان کمک کرده است، لطفاً فراموش نکنید تجربههای خود را به اشتراک بگذارید. [تجربههای خود را در بحث گیتهاب ما به اشتراک بگذارید](https://github.com/usebruno/bruno/discussions/343).
|
||||
|
||||
### انتشار برونو در یک پکیچ منیجر جدید
|
||||
|
||||
لطفا چک بکنید [اینجارو](../../publishing.md) برای اطلاعات بیشتر.
|
||||
|
||||
### مشارکت 👩💻🧑💻
|
||||
|
||||
خوشحالم که میخواهید برونو را بهتر کنید. لطفا [راهنمای مشارکت را بررسی کنید](../contributing/contributing_fa.md).
|
||||
|
||||
حتی اگر نمیتوانید از طریق کدنویسی مشارکت کنید، در گزارش باگها و درخواست قابلیتهای جدید که به حل نیازهای شما کمک میکند تردید نکنید.
|
||||
|
||||
### نویسنده ها
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### در ارتباط باشید 🌐
|
||||
|
||||
[𝕏 (تویتر)](https://twitter.com/use_bruno) <br />
|
||||
[وبسایت](https://www.usebruno.com) <br />
|
||||
[دیسکورد](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[لینکدین](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### برند
|
||||
|
||||
**نام**
|
||||
|
||||
به فارسی برونو - `Bruno` یک علامت تجاری ثبتشده متعلق به [Anoop M D](https://www.helloanoop.com/)
|
||||
|
||||
**لوگو**
|
||||
|
||||
لوگو توسط [OpenMoji](https://openmoji.org/library/emoji-1F436/) ساخته شده است. مجوز: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### مجوز 📄
|
||||
|
||||
[MIT](../../license.md)
|
||||
@@ -43,13 +43,6 @@ Bruno はオフラインのみで利用できます。Bruno にクラウド同
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### ゴールデンエディション ✨
|
||||
|
||||
機能のほとんどが無料で使用でき、オープンソースとなっています。
|
||||
私たちは[オープンソースの原則と長期的な維持](https://github.com/usebruno/bruno/discussions/269)の間でうまくバランスを取ろうと努力しています。
|
||||
|
||||
[ゴールデンエディション](https://www.usebruno.com/pricing)を **19 ドル** (買い切り)で購入できます!
|
||||
|
||||
### インストール方法
|
||||
|
||||
Bruno は[私たちのウェブサイト](https://www.usebruno.com/downloads)からバイナリをダウンロードできます。Mac, Windows, Linux に対応しています。
|
||||
|
||||
@@ -43,12 +43,6 @@
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### ოქროს გამოცემა ✨
|
||||
|
||||
მთავარი ფუნქციების უმეტესობა უფასოა და ღია წყაროა. ჩვენ ვცდილობთ ჰარმონიული ბალანსის დაცვას [ღია წყაროების პრინციპებსა და მდგრადობას შორის](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
თქვენ შეგიძლიათ შეიძინოთ [ოქროს გამოცემა](https://www.usebruno.com/pricing) ერთჯერადი გადახდით **19 დოლარად**! <br/>
|
||||
|
||||
### ინსტალაცია
|
||||
|
||||
ბრუნო ხელმისაწვდომია როგორც ბინარული ჩამოტვირთვა [ჩვენ的网站上](https://www.usebruno.com/downloads) Mac-ის, Windows-ისა და Linux-ისთვის.
|
||||
|
||||
@@ -26,13 +26,6 @@ Bruno is uitsluitend offline. Er zijn geen plannen om ooit cloud-synchronisatie
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Golden Edition ✨
|
||||
|
||||
De meeste van onze functies zijn gratis en open source.
|
||||
We streven naar een harmonieuze balans tussen [open-source principes en duurzaamheid](https://github.com/usebruno/bruno/discussions/269).
|
||||
|
||||
Je kunt de [Golden Edition](https://www.usebruno.com/pricing) kopen voor een eenmalige betaling van **$19**! <br/>
|
||||
|
||||
### Installatie
|
||||
|
||||
Bruno is beschikbaar als binaire download [op onze website](https://www.usebruno.com/downloads) voor Mac, Windows en Linux.
|
||||
|
||||
@@ -41,13 +41,6 @@ Bruno é totalmente offline. Não há planos de adicionar sincronização em nuv
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Golden Edition ✨
|
||||
|
||||
A grande maioria dos nossos recursos são gratuitos e de código aberto.
|
||||
Nós nos esforçamos para encontrar um equilíbrio harmônico entre [princípios de código aberto e sustentabilidade](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
Você pode pré encomendar o plano [Golden Edition](https://www.usebruno.com/pricing) por ~~USD $19~~ **USD $9**! <br/>
|
||||
|
||||
### Instalação
|
||||
|
||||
Bruno está disponível para download como binário [em nosso site](https://www.usebruno.com/downloads) para Mac, Windows e Linux.
|
||||
|
||||
272
eslint.config.js
272
eslint.config.js
@@ -1,11 +1,89 @@
|
||||
// eslint.config.js
|
||||
const { defineConfig } = require("eslint/config");
|
||||
const globals = require("globals");
|
||||
const { defineConfig } = require('eslint/config');
|
||||
const globals = require('globals');
|
||||
const { fixupPluginRules } = require('@eslint/compat');
|
||||
const eslintPluginDiff = require('eslint-plugin-diff');
|
||||
|
||||
module.exports = defineConfig([
|
||||
let stylistic;
|
||||
|
||||
const runESMImports = async () => {
|
||||
stylistic = await import('@stylistic/eslint-plugin').then((d) => d.default);
|
||||
};
|
||||
|
||||
module.exports = runESMImports().then(() => defineConfig([
|
||||
// Global ignores - must be a standalone object with ONLY ignores
|
||||
{
|
||||
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
|
||||
ignores: ["**/*.config.js", "**/public/**/*"],
|
||||
ignores: [
|
||||
'**/node_modules/**/*',
|
||||
'**/dist/**/*',
|
||||
'**/*.bru',
|
||||
'packages/bruno-js/src/sandbox/bundle-browser-rollup.js',
|
||||
'packages/bruno-app/public/static/**/*',
|
||||
'packages/bruno-app/.next/**/*',
|
||||
'packages/bruno-electron/web/**/*'
|
||||
]
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
'diff': fixupPluginRules(eslintPluginDiff),
|
||||
'@stylistic': stylistic
|
||||
},
|
||||
languageOptions: {
|
||||
parser: require('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
files: [
|
||||
'./eslint.config.js',
|
||||
'tests/**/*.{ts,js}',
|
||||
'playwright/**/*.{js,ts}',
|
||||
'packages/bruno-app/**/*.{js,jsx,ts}',
|
||||
'packages/bruno-app/src/test-utils/mocks/codemirror.js',
|
||||
'packages/bruno-cli/**/*.js',
|
||||
'packages/bruno-common/**/*.ts',
|
||||
'packages/bruno-converters/**/*.js',
|
||||
'packages/bruno-electron/**/*.js',
|
||||
'packages/bruno-filestore/**/*.ts',
|
||||
'packages/bruno-schema-types/**/*.ts',
|
||||
'packages/bruno-js/**/*.js',
|
||||
'packages/bruno-lang/**/*.js',
|
||||
'packages/bruno-requests/**/*.ts',
|
||||
'packages/bruno-requests/**/*.js',
|
||||
'packages/bruno-tests/**/*.{js,ts}'
|
||||
],
|
||||
rules: {
|
||||
...stylistic.configs.customize({
|
||||
indent: 2,
|
||||
quotes: 'single',
|
||||
semi: true,
|
||||
jsx: true
|
||||
}).rules,
|
||||
'@stylistic/comma-dangle': ['error', 'never'],
|
||||
'@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],
|
||||
'@stylistic/arrow-parens': ['error', 'always'],
|
||||
'@stylistic/curly-newline': ['error', {
|
||||
multiline: true,
|
||||
minElements: 2,
|
||||
consistent: true
|
||||
}],
|
||||
'@stylistic/function-paren-newline': ['off'],
|
||||
'@stylistic/array-bracket-spacing': ['error', 'never'],
|
||||
'@stylistic/arrow-spacing': ['error', { before: true, after: true }],
|
||||
'@stylistic/function-call-spacing': ['error', 'never'],
|
||||
'@stylistic/multiline-ternary': ['off'],
|
||||
'@stylistic/padding-line-between-statements': ['off'],
|
||||
'@stylistic/semi-style': ['error', 'last'],
|
||||
'@stylistic/max-len': ['off'],
|
||||
'@stylistic/jsx-one-expression-per-line': ['off'],
|
||||
'@stylistic/max-statements-per-line': ['off'],
|
||||
'@stylistic/no-mixed-operators': ['off']
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-app/**/*.{js,jsx,ts}'],
|
||||
ignores: ['**/*.config.js', '**/public/**/*'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
@@ -18,114 +96,126 @@ module.exports = defineConfig([
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
jsx: true
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
// It prevents lint errors when using CommonJS exports (module.exports) in Jest mocks.
|
||||
files: ["packages/bruno-app/src/test-utils/mocks/codemirror.js"],
|
||||
files: ['packages/bruno-app/src/test-utils/mocks/codemirror.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
...globals.jest
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-cli/**/*.js"],
|
||||
ignores: ["**/*.config.js"],
|
||||
// Storybook config files use CommonJS with __dirname and module.exports
|
||||
files: ['packages/bruno-app/storybook/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['packages/bruno-cli/**/*.js'],
|
||||
ignores: ['**/*.config.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
...globals.jest
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest"
|
||||
},
|
||||
ecmaVersion: 'latest'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-common/**/*.ts"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
files: ['packages/bruno-common/**/*.ts'],
|
||||
ignores: ['**/*.config.js', '**/dist/**/*'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
...globals.jest
|
||||
},
|
||||
parser: require("@typescript-eslint/parser"),
|
||||
parser: require('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./packages/bruno-common/tsconfig.json",
|
||||
},
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: './packages/bruno-common/tsconfig.json'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-converters/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
files: ['packages/bruno-converters/**/*.js'],
|
||||
ignores: ['**/*.config.js', '**/dist/**/*'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
...globals.jest
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-electron/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/web/**/*"],
|
||||
files: ['packages/bruno-electron/**/*.js'],
|
||||
ignores: ['**/*.config.js', '**/web/**/*'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
...globals.jest
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-filestore/**/*.ts"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
files: ['packages/bruno-filestore/**/*.ts'],
|
||||
ignores: ['**/*.config.js', '**/dist/**/*'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
...globals.jest
|
||||
},
|
||||
parser: require("@typescript-eslint/parser"),
|
||||
parser: require('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./packages/bruno-filestore/tsconfig.json",
|
||||
},
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: './packages/bruno-filestore/tsconfig.json'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-js/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
files: ['packages/bruno-js/**/*.js'],
|
||||
ignores: ['**/*.config.js', '**/dist/**/*'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
@@ -136,65 +226,65 @@ module.exports = defineConfig([
|
||||
typeDetectGlobalObject: false
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-lang/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
files: ['packages/bruno-lang/**/*.js'],
|
||||
ignores: ['**/*.config.js', '**/dist/**/*'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
...globals.jest
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-requests/**/*.ts"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
files: ['packages/bruno-requests/**/*.ts'],
|
||||
ignores: ['**/*.config.js', '**/dist/**/*'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
...globals.jest
|
||||
},
|
||||
parser: require("@typescript-eslint/parser"),
|
||||
parser: require('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
project: "./packages/bruno-requests/tsconfig.json",
|
||||
},
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: './packages/bruno-requests/tsconfig.json'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
'no-undef': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-requests/**/*.js"],
|
||||
ignores: ["**/*.config.js", "**/dist/**/*"],
|
||||
files: ['packages/bruno-requests/**/*.js'],
|
||||
ignores: ['**/*.config.js', '**/dist/**/*'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
...globals.jest
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
]);
|
||||
'no-undef': 'error'
|
||||
}
|
||||
}
|
||||
]));
|
||||
|
||||
5444
package-lock.json
generated
5444
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@@ -8,6 +8,7 @@
|
||||
"packages/bruno-common",
|
||||
"packages/bruno-converters",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-schema-types",
|
||||
"packages/bruno-query",
|
||||
"packages/bruno-js",
|
||||
"packages/bruno-lang",
|
||||
@@ -19,30 +20,41 @@
|
||||
],
|
||||
"homepage": "https://usebruno.com",
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.3.2",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@opencollection/types": "~0.7.0",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@storybook/addon-webpack5-compiler-babel": "^4.0.0",
|
||||
"@storybook/builder-webpack5": "^10.1.10",
|
||||
"@storybook/react": "^10.1.10",
|
||||
"@storybook/react-webpack5": "^10.1.10",
|
||||
"@stylistic/eslint-plugin": "^5.3.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.14.1",
|
||||
"@typescript-eslint/parser": "^8.39.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-plugin-diff": "^2.0.3",
|
||||
"fs-extra": "^11.1.1",
|
||||
"globals": "^16.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nano-staged": "^0.8.0",
|
||||
"playwright": "^1.51.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"storybook": "^10.1.10",
|
||||
"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": "node ./scripts/dev.js",
|
||||
"watch": "npm run dev:watch",
|
||||
"dev:watch": "node ./scripts/dev-hot-reload.js",
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
@@ -50,12 +62,14 @@
|
||||
"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",
|
||||
"storybook": "npm run storybook --workspace=packages/bruno-app",
|
||||
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
|
||||
"build:bruno-requests": "npm run build --workspace=packages/bruno-requests",
|
||||
"build:bruno-filestore": "npm run build --workspace=packages/bruno-filestore",
|
||||
"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:schema-types": "npm run build --workspace=packages/bruno-schema-types",
|
||||
"build:electron": "node ./scripts/build-electron.js",
|
||||
"build:electron:mac": "./scripts/build-electron.sh mac",
|
||||
"build:electron:win": "./scripts/build-electron.sh win",
|
||||
@@ -68,7 +82,14 @@
|
||||
"test:e2e": "playwright test --project=default",
|
||||
"test:e2e:ssl": "playwright test --project=ssl",
|
||||
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||
"lint": "node --max_old_space_size=4096 $(npx which eslint)"
|
||||
"lint": "node --max_old_space_size=4096 $(npx which eslint)",
|
||||
"lint:fix": "node --max_old_space_size=4096 $(npx which eslint) --fix",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"nano-staged": {
|
||||
"*.{js,ts,jsx}": [
|
||||
"npm run lint:fix"
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.29.5",
|
||||
@@ -77,5 +98,8 @@
|
||||
"json-schema-typed": "8.0.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^8.17.1"
|
||||
}
|
||||
}
|
||||
|
||||
4
packages/bruno-app/.gitignore
vendored
4
packages/bruno-app/.gitignore
vendored
@@ -22,6 +22,7 @@ build
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*.log
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
@@ -33,4 +34,5 @@ yarn-error.log*
|
||||
.next/
|
||||
dist/
|
||||
|
||||
.env
|
||||
.env
|
||||
storybook-static/
|
||||
@@ -6,4 +6,4 @@ module.exports = {
|
||||
}]
|
||||
],
|
||||
plugins: ['babel-plugin-styled-components']
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
module.exports = {
|
||||
rootDir: '.',
|
||||
transform: {
|
||||
'^.+\\.[jt]sx?$': 'babel-jest',
|
||||
'^.+\\.[jt]sx?$': 'babel-jest'
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
"/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/",
|
||||
'/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/'
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^assets/(.*)$': '<rootDir>/src/assets/$1',
|
||||
@@ -22,9 +22,9 @@ module.exports = {
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['@testing-library/jest-dom'],
|
||||
setupFiles: [
|
||||
'<rootDir>/jest.setup.js',
|
||||
'<rootDir>/jest.setup.js'
|
||||
],
|
||||
testMatch: [
|
||||
'<rootDir>/src/**/*.spec.[jt]s?(x)'
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"preview": "rsbuild preview",
|
||||
"test": "jest",
|
||||
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\"",
|
||||
"storybook": "storybook dev -p 6006 --config-dir storybook",
|
||||
"build-storybook": "storybook build --config-dir storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
@@ -21,12 +23,16 @@
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.7.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "5.65.2",
|
||||
"codemirror-graphql": "2.1.1",
|
||||
"cookie": "0.7.1",
|
||||
"dompurify": "^3.2.4",
|
||||
"escape-html": "^1.0.3",
|
||||
"fast-fuzzy": "^1.12.0",
|
||||
"fast-json-format": "~0.4.0",
|
||||
"file": "^0.2.2",
|
||||
"file-dialog": "^0.0.8",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -35,20 +41,24 @@
|
||||
"graphiql": "3.7.1",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"hexy": "^0.3.5",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"i18next": "24.1.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsesc": "^3.0.2",
|
||||
"jshint": "^2.13.6",
|
||||
"json5": "^2.2.3",
|
||||
"jsonc-parser": "^3.2.1",
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"jsonschema": "^1.5.0",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.2",
|
||||
"markdown-it-replace-link": "^1.2.0",
|
||||
"mime-types": "^3.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.47",
|
||||
"mousetrap": "^1.6.5",
|
||||
@@ -56,6 +66,7 @@
|
||||
"path": "^0.12.7",
|
||||
"pdfjs-dist": "4.4.168",
|
||||
"platform": "^1.3.6",
|
||||
"polished": "^4.3.1",
|
||||
"posthog-node": "4.2.1",
|
||||
"prettier": "^2.7.1",
|
||||
"qs": "^6.11.0",
|
||||
@@ -78,9 +89,11 @@
|
||||
"shell-quote": "^1.8.3",
|
||||
"strip-json-comments": "^5.0.1",
|
||||
"styled-components": "^5.3.3",
|
||||
"swagger-ui-react": "5.17.12",
|
||||
"system": "^2.0.1",
|
||||
"url": "^0.11.3",
|
||||
"xml-formatter": "^3.5.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const darkTheme = {
|
||||
brand: '#546de5',
|
||||
text: 'rgb(52 52 52)',
|
||||
'brand': '#546de5',
|
||||
'text': 'rgb(52 52 52)',
|
||||
'primary-text': '#ffffff',
|
||||
'primary-theme': '#1e1e1e',
|
||||
'secondary-text': '#929292',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const lightTheme = {
|
||||
brand: '#546de5',
|
||||
text: 'rgb(52 52 52)',
|
||||
'brand': '#546de5',
|
||||
'text': 'rgb(52 52 52)',
|
||||
'primary-text': 'rgb(52 52 52)',
|
||||
'primary-theme': '#ffffff',
|
||||
'secondary-text': '#929292',
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrappe
|
||||
|
||||
const AccordionContext = createContext();
|
||||
|
||||
const Accordion = ({ children, defaultIndex }) => {
|
||||
const Accordion = ({ children, defaultIndex, dataTestId }) => {
|
||||
const [openIndex, setOpenIndex] = useState(defaultIndex);
|
||||
|
||||
const toggleItem = (index) => {
|
||||
@@ -13,7 +13,7 @@ const Accordion = ({ children, defaultIndex }) => {
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={{ openIndex, toggleItem }}>
|
||||
<div>{children}</div>
|
||||
<div data-testid={dataTestId}>{children}</div>
|
||||
</AccordionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
const yamlPlugin = (cm) => {
|
||||
cm.defineMode('yaml', function () {
|
||||
var cons = ['true', 'false', 'on', 'off', 'yes', 'no'];
|
||||
var keywordRegex = new RegExp('\\b((' + cons.join(')|(') + '))$', 'i');
|
||||
|
||||
return {
|
||||
token: function (stream, state) {
|
||||
var ch = stream.peek();
|
||||
var esc = state.escaped;
|
||||
state.escaped = false;
|
||||
/* comments */
|
||||
if (ch == '#' && (stream.pos == 0 || /\s/.test(stream.string.charAt(stream.pos - 1)))) {
|
||||
stream.skipToEnd();
|
||||
return 'comment';
|
||||
}
|
||||
|
||||
if (stream.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/)) return 'string';
|
||||
|
||||
if (state.literal && stream.indentation() > state.keyCol) {
|
||||
stream.skipToEnd();
|
||||
return 'string';
|
||||
} else if (state.literal) {
|
||||
state.literal = false;
|
||||
}
|
||||
if (stream.sol()) {
|
||||
state.keyCol = 0;
|
||||
state.pair = false;
|
||||
state.pairStart = false;
|
||||
/* document start */
|
||||
if (stream.match('---')) {
|
||||
return 'def';
|
||||
}
|
||||
/* document end */
|
||||
if (stream.match('...')) {
|
||||
return 'def';
|
||||
}
|
||||
/* array list item */
|
||||
if (stream.match(/\s*-\s+/)) {
|
||||
return 'meta';
|
||||
}
|
||||
}
|
||||
/* inline pairs/lists */
|
||||
if (stream.match(/^(\{|\}|\[|\])/)) {
|
||||
if (ch == '{') state.inlinePairs++;
|
||||
else if (ch == '}') state.inlinePairs--;
|
||||
else if (ch == '[') state.inlineList++;
|
||||
else state.inlineList--;
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
/* list separator */
|
||||
if (state.inlineList > 0 && !esc && ch == ',') {
|
||||
stream.next();
|
||||
return 'meta';
|
||||
}
|
||||
/* pairs separator */
|
||||
if (state.inlinePairs > 0 && !esc && ch == ',') {
|
||||
state.keyCol = 0;
|
||||
state.pair = false;
|
||||
state.pairStart = false;
|
||||
stream.next();
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
/* start of value of a pair */
|
||||
if (state.pairStart) {
|
||||
/* block literals */
|
||||
if (stream.match(/^\s*(\||\>)\s*/)) {
|
||||
state.literal = true;
|
||||
return 'meta';
|
||||
}
|
||||
/* references */
|
||||
if (stream.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i)) {
|
||||
return 'variable-2';
|
||||
}
|
||||
/* numbers */
|
||||
if (state.inlinePairs == 0 && stream.match(/^\s*-?[0-9\.\,]+\s?$/)) {
|
||||
return 'number';
|
||||
}
|
||||
if (state.inlinePairs > 0 && stream.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/)) {
|
||||
return 'number';
|
||||
}
|
||||
/* keywords */
|
||||
if (stream.match(keywordRegex)) {
|
||||
return 'keyword';
|
||||
}
|
||||
}
|
||||
|
||||
/* pairs (associative arrays) -> key */
|
||||
if (
|
||||
!state.pair
|
||||
&& stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^\s,\[\]{}#&*!|>'"%@`])[^#:]*(?=:($|\s))/)
|
||||
) {
|
||||
state.pair = true;
|
||||
state.keyCol = stream.indentation();
|
||||
return 'atom';
|
||||
}
|
||||
if (state.pair && stream.match(/^:\s*/)) {
|
||||
state.pairStart = true;
|
||||
return 'meta';
|
||||
}
|
||||
|
||||
/* nothing found, continue */
|
||||
state.pairStart = false;
|
||||
state.escaped = ch == '\\';
|
||||
stream.next();
|
||||
return null;
|
||||
},
|
||||
startState: function () {
|
||||
return {
|
||||
pair: false,
|
||||
pairStart: false,
|
||||
keyCol: 0,
|
||||
inlinePairs: 0,
|
||||
inlineList: 0,
|
||||
literal: false,
|
||||
escaped: false
|
||||
};
|
||||
},
|
||||
lineComment: '#',
|
||||
fold: 'indent'
|
||||
};
|
||||
});
|
||||
|
||||
cm.defineMIME('text/x-yaml', 'yaml');
|
||||
cm.defineMIME('text/yaml', 'yaml');
|
||||
};
|
||||
|
||||
export default yamlPlugin;
|
||||
@@ -0,0 +1,65 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
height: calc(100vh - 4rem);
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
font-family: ${(props) => (props.font ? props.font : 'default')};
|
||||
line-break: anywhere;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog {
|
||||
overflow: visible;
|
||||
input {
|
||||
background: transparent;
|
||||
border: 1px solid #d3d6db;
|
||||
outline: none;
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
.CodeMirror-overlayscroll-vertical div {
|
||||
background: #d2d7db;
|
||||
}
|
||||
|
||||
textarea.cm-editor {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Todo: dark mode temporary fix
|
||||
// Clean this
|
||||
.CodeMirror.cm-s-monokai {
|
||||
.CodeMirror-overlayscroll-horizontal div,
|
||||
.CodeMirror-overlayscroll-vertical div {
|
||||
background: #444444;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-property,
|
||||
.cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-string {
|
||||
color: #ce9178 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-number {
|
||||
color: #b5cea8 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-atom {
|
||||
color: #569cd6 !important;
|
||||
}
|
||||
|
||||
.cm-variable-valid {
|
||||
color: ${(props) => props.theme.codemirror.variable.valid};
|
||||
}
|
||||
.cm-variable-invalid {
|
||||
color: ${(props) => props.theme.codemirror.variable.invalid};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Copyright (c) 2021 GraphQL Contributors.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import yamlPlugin from './Plugins/Yaml/index';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
CodeMirror = require('codemirror');
|
||||
}
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.cachedValue = props.value || '';
|
||||
this.variables = {};
|
||||
this.lintOptions = {
|
||||
esversion: 11,
|
||||
expr: true,
|
||||
asi: true
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
switch (this.props.mode) {
|
||||
case 'yaml':
|
||||
// YAML linting and hightlighting plugin
|
||||
yamlPlugin(CodeMirror);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const editor = (this.editor = CodeMirror(this._node, {
|
||||
value: this.props.value || '',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
tabSize: 2,
|
||||
mode: this.props.mode || 'application/text',
|
||||
keyMap: 'sublime',
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
showCursorWhenSelecting: true,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
||||
lint: this.lintOptions,
|
||||
readOnly: this.props.readOnly,
|
||||
scrollbarStyle: 'overlay',
|
||||
theme: this.props.theme === 'dark' ? 'monokai' : 'default',
|
||||
extraKeys: {
|
||||
'Cmd-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Ctrl-S': () => {
|
||||
if (this.props.onSave) {
|
||||
this.props.onSave();
|
||||
}
|
||||
},
|
||||
'Cmd-F': 'findPersistent',
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-H': 'replace',
|
||||
'Ctrl-H': 'replace',
|
||||
'Tab': function (cm) {
|
||||
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
|
||||
? cm.execCommand('indentMore')
|
||||
: cm.replaceSelection(' ', 'end');
|
||||
},
|
||||
'Shift-Tab': 'indentLess',
|
||||
'Ctrl-Space': 'autocomplete',
|
||||
'Cmd-Space': 'autocomplete',
|
||||
'Ctrl-Y': 'foldAll',
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
'Cmd-I': 'unfoldAll'
|
||||
}
|
||||
}));
|
||||
if (editor) {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
editor.on('change', this._onEdit);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
this.ignoreChangeEvent = true;
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
}
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');
|
||||
}
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.editor) {
|
||||
this.editor.refresh();
|
||||
}
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full w-full graphiql-container"
|
||||
aria-label="Code Editor"
|
||||
font={this.props.font}
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
this.cachedValue = this.editor.getValue();
|
||||
if (this.props.onEdit) {
|
||||
this.props.onEdit(this.cachedValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from './CodeEditor/index';
|
||||
import { IconDeviceFloppy } from '@tabler/icons';
|
||||
import { saveApiSpecToFile } from 'providers/ReduxStore/slices/apiSpec';
|
||||
import { useState } from 'react';
|
||||
|
||||
const FileEditor = ({ apiSpec }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme, theme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const [content, setContent] = useState(apiSpec?.raw);
|
||||
|
||||
const onEdit = (value) => {
|
||||
setContent(value);
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
dispatch(saveApiSpecToFile({ uid: apiSpec?.uid, content }));
|
||||
};
|
||||
|
||||
const hasChanges = Boolean(content != apiSpec?.raw);
|
||||
|
||||
const editorMode = 'yaml';
|
||||
|
||||
return (
|
||||
<div className="flex flex-grow relative">
|
||||
<CodeEditor
|
||||
theme={displayedTheme}
|
||||
value={content}
|
||||
onEdit={onEdit}
|
||||
onSave={onSave}
|
||||
mode={editorMode}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
/>
|
||||
<IconDeviceFloppy
|
||||
onClick={onSave}
|
||||
color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
className={`absolute right-0 top-0 m-4 ${
|
||||
hasChanges ? 'cursor-pointer oapcity-100' : 'cursor-default opacity-50'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileEditor;
|
||||
@@ -0,0 +1,19 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.swagger-root {
|
||||
height: calc(100vh - 4rem);
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
|
||||
&.dark {
|
||||
.swagger-ui {
|
||||
filter: invert(88%) hue-rotate(180deg);
|
||||
}
|
||||
.swagger-ui .microlight {
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,19 @@
|
||||
import SwaggerUI from 'swagger-ui-react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const Swagger = ({ string }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
|
||||
console.log('string', string);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className={`swagger-root w-full overflow-y-scroll ${displayedTheme}`}>
|
||||
<SwaggerUI spec={string} />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Swagger;
|
||||
@@ -0,0 +1,22 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.menu-icon {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
}
|
||||
|
||||
div.dropdown-item.menu-item {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.colors.bg.danger};
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.react-tooltip {
|
||||
z-index: 10;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
97
packages/bruno-app/src/components/ApiSpecPanel/index.js
Normal file
97
packages/bruno-app/src/components/ApiSpecPanel/index.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import React, { forwardRef, useRef } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconFileCode, IconDots } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import FileEditor from './FileEditor';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { openApiSpec } from 'providers/ReduxStore/slices/apiSpec';
|
||||
import { useState } from 'react';
|
||||
import CreateApiSpec from 'components/Sidebar/ApiSpecs/CreateApiSpec';
|
||||
import { Suspense } from 'react';
|
||||
import Swagger from './Renderers/Swagger';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const ApiSpecPanel = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [createApiSpecModalOpen, setCreateApiSpecModalOpen] = useState(false);
|
||||
|
||||
const { apiSpecs, activeApiSpecUid } = useSelector((state) => state.apiSpec);
|
||||
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
let apiSpec = find(apiSpecs, (c) => c.uid === activeApiSpecUid);
|
||||
const { filename, pathname, raw, uid } = apiSpec || {};
|
||||
if (!uid) {
|
||||
return <div className="p-4 opacity-50">API Spec not found!</div>;
|
||||
}
|
||||
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<IconDots size={22} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleOpenApiSpec = () => {
|
||||
dispatch(openApiSpec()).catch(
|
||||
(err) => console.log(err) && toast.error('An error occurred while opening the API spec')
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col flex-grow relative">
|
||||
{createApiSpecModalOpen ? <CreateApiSpec onClose={() => setCreateApiSpecModalOpen(false)} /> : null}
|
||||
<div className="p-3 mb-2 w-full flex flex-row justify-between grid grid-cols-3">
|
||||
<div className="flex flex-row justify-start gap-x-4 col-span-1">
|
||||
<div className="flex w-fit items-center cursor-pointer">
|
||||
<IconFileCode size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2 mr-4 font-semibold">API Designer</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full col-span-1 flex justify-center" title={pathname}>
|
||||
{filename}
|
||||
</div>
|
||||
<div className="menu-icon pr-2 col-span-1 flex justify-end">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
setCreateApiSpecModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Create API Spec
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
dropdownTippyRef.current.hide();
|
||||
handleOpenApiSpec();
|
||||
}}
|
||||
>
|
||||
Open API Spec
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<section className="main flex flex-grow px-4 relative">
|
||||
<div className="w-full grid grid-cols-2">
|
||||
<div className="col-span-1">
|
||||
<FileEditor apiSpec={apiSpec} />
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Suspense fallback="">
|
||||
<Swagger string={raw} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiSpecPanel;
|
||||
251
packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js
Normal file
251
packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js
Normal file
@@ -0,0 +1,251 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
|
||||
.titlebar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 12px;
|
||||
padding-left: 70px; /* Space for macOS window controls */
|
||||
transition: padding-left 0.15s ease;
|
||||
}
|
||||
|
||||
/* When in full screen, no traffic lights so reduce padding */
|
||||
&.fullscreen .titlebar-content {
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
/* Remove drag region from interactive elements */
|
||||
.workspace-name-container,
|
||||
.dropdown-item,
|
||||
.home-button,
|
||||
.dropdown,
|
||||
button {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* Left section */
|
||||
.titlebar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: 10px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* When in full screen, no traffic lights so remove margin-left */
|
||||
&.fullscreen .titlebar-left {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
/* Workspace Name Dropdown Trigger */
|
||||
.workspace-name-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
.workspace-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.sidebar.color};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
flex-shrink: 0;
|
||||
color: ${(props) => props.theme.sidebar.muted};
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* Center section - Bruno branding */
|
||||
.titlebar-center {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
pointer-events: none;
|
||||
|
||||
.bruno-text {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.text};
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Right section */
|
||||
.titlebar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* App action buttons container */
|
||||
.titlebar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Workspace Dropdown Styles */
|
||||
.workspace-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 10px !important;
|
||||
margin: 0 !important;
|
||||
|
||||
&.active {
|
||||
.check-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.pin-btn:not(.pinned) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: ${(props) => props.theme.dropdown.color};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.workspace-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
flex-shrink: 0;
|
||||
pointer-events: none;
|
||||
|
||||
> * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: ${(props) => props.theme.workspace?.accent || props.theme.colors?.text?.yellow};
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pin-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;
|
||||
opacity: 0;
|
||||
|
||||
&.pinned {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.dropdown.hoverBg};
|
||||
color: ${(props) => props.theme.dropdown.mutedText};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Adjust for non-macOS platforms */
|
||||
&:not(.os-mac) .titlebar-content {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
/* Windows-specific styles */
|
||||
&.os-windows .titlebar-content {
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
&.os-windows .titlebar-left {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
&.os-linux .titlebar-content {
|
||||
padding-right: 0px;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
&.os-linux .titlebar-left {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* Custom window control buttons for Windows - always interactive, above modal overlay */
|
||||
.window-controls {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 36px;
|
||||
margin-left: 8px;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.window-control-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 46px;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.text};
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&:hover {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
|
||||
}
|
||||
|
||||
&.close:hover {
|
||||
background: #e81123;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
333
packages/bruno-app/src/components/AppTitleBar/index.js
Normal file
333
packages/bruno-app/src/components/AppTitleBar/index.js
Normal file
@@ -0,0 +1,333 @@
|
||||
import React from 'react';
|
||||
import { IconCheck, IconChevronDown, IconFolder, IconHome, IconPin, IconPinned, IconPlus, IconDownload, IconSettings, IconMinus, IconSquare, IconX, IconCopy } from '@tabler/icons';
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { savePreferences, showHomePage, showManageWorkspacePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';
|
||||
import { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs';
|
||||
import { openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces';
|
||||
|
||||
import Bruno from 'components/Bruno';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import ActionIcon from 'ui/ActionIcon';
|
||||
import IconSidebarToggle from 'components/Icons/IconSidebarToggle';
|
||||
import CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace';
|
||||
import ImportWorkspace from 'components/WorkspaceSidebar/ImportWorkspace';
|
||||
|
||||
import IconBottombarToggle from 'components/Icons/IconBottombarToggle/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { toTitleCase } from 'utils/common/index';
|
||||
import ResponseLayoutToggle from 'components/ResponsePane/ResponseLayoutToggle';
|
||||
import { isMacOS, isWindowsOS, isLinuxOS } from 'utils/common/platform';
|
||||
|
||||
const getOsClass = () => {
|
||||
if (isMacOS()) return 'os-mac';
|
||||
if (isWindowsOS()) return 'os-windows';
|
||||
if (isLinuxOS()) return 'os-linux';
|
||||
return 'os-other';
|
||||
};
|
||||
|
||||
const AppTitleBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
const osClass = getOsClass();
|
||||
const isWindows = osClass === 'os-windows';
|
||||
const isLinux = osClass === 'os-linux';
|
||||
const showWindowControls = isWindows || isLinux;
|
||||
|
||||
// Listen for fullscreen changes
|
||||
useEffect(() => {
|
||||
const { ipcRenderer } = window;
|
||||
if (!ipcRenderer) return;
|
||||
|
||||
const removeEnterFullScreenListener = ipcRenderer.on('main:enter-full-screen', () => {
|
||||
setIsFullScreen(true);
|
||||
});
|
||||
|
||||
const removeLeaveFullScreenListener = ipcRenderer.on('main:leave-full-screen', () => {
|
||||
setIsFullScreen(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeEnterFullScreenListener();
|
||||
removeLeaveFullScreenListener();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showWindowControls) return;
|
||||
const { ipcRenderer } = window;
|
||||
if (!ipcRenderer) return;
|
||||
|
||||
ipcRenderer.invoke('renderer:window-is-maximized')
|
||||
.then((maximized) => {
|
||||
setIsMaximized(maximized);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error getting initial maximized state:', error);
|
||||
});
|
||||
|
||||
const removeMaximizedListener = ipcRenderer.on('main:window-maximized', () => {
|
||||
setIsMaximized(true);
|
||||
});
|
||||
|
||||
const removeUnmaximizedListener = ipcRenderer.on('main:window-unmaximized', () => {
|
||||
setIsMaximized(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
removeMaximizedListener();
|
||||
removeUnmaximizedListener();
|
||||
};
|
||||
}, [showWindowControls]);
|
||||
|
||||
const handleMinimize = useCallback(() => {
|
||||
window.ipcRenderer?.send('renderer:window-minimize');
|
||||
}, []);
|
||||
|
||||
const handleMaximize = useCallback(() => {
|
||||
window.ipcRenderer?.send('renderer:window-maximize');
|
||||
// State will be updated via IPC events from main process (main:window-maximized/main:window-unmaximized)
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
window.ipcRenderer?.send('renderer:window-close');
|
||||
}, []);
|
||||
|
||||
// Get workspace info
|
||||
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);
|
||||
const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen);
|
||||
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);
|
||||
|
||||
// Sort workspaces according to preferences
|
||||
const sortedWorkspaces = useMemo(() => {
|
||||
return sortWorkspaces(workspaces, preferences);
|
||||
}, [workspaces, preferences]);
|
||||
|
||||
const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false);
|
||||
const [importWorkspaceModalOpen, setImportWorkspaceModalOpen] = useState(false);
|
||||
|
||||
const WorkspaceName = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="workspace-name-container" {...props}>
|
||||
<span className="workspace-name">{toTitleCase(activeWorkspace?.name) || 'Default Workspace'}</span>
|
||||
<IconChevronDown size={14} stroke={1.5} className="chevron-icon" />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const handleHomeClick = () => {
|
||||
dispatch(showHomePage());
|
||||
};
|
||||
|
||||
const handleWorkspaceSwitch = (workspaceUid) => {
|
||||
dispatch(switchWorkspace(workspaceUid));
|
||||
toast.success(`Switched to ${workspaces.find((w) => w.uid === workspaceUid)?.name}`);
|
||||
};
|
||||
|
||||
const handleOpenWorkspace = async () => {
|
||||
try {
|
||||
await dispatch(openWorkspaceDialog());
|
||||
toast.success('Workspace opened successfully');
|
||||
} catch (error) {
|
||||
toast.error(error.message || 'Failed to open workspace');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateWorkspace = () => {
|
||||
setCreateWorkspaceModalOpen(true);
|
||||
};
|
||||
|
||||
const handleManageWorkspaces = () => {
|
||||
dispatch(showManageWorkspacePage());
|
||||
};
|
||||
|
||||
const handleImportWorkspace = () => {
|
||||
setImportWorkspaceModalOpen(true);
|
||||
};
|
||||
|
||||
const handlePinWorkspace = useCallback((workspaceUid, e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const newPreferences = toggleWorkspacePin(workspaceUid, preferences);
|
||||
dispatch(savePreferences(newPreferences));
|
||||
}, [dispatch, preferences]);
|
||||
|
||||
const handleToggleSidebar = () => {
|
||||
dispatch(toggleSidebarCollapse());
|
||||
};
|
||||
|
||||
const handleToggleDevtools = () => {
|
||||
if (isConsoleOpen) {
|
||||
dispatch(closeConsole());
|
||||
} else {
|
||||
dispatch(openConsole());
|
||||
}
|
||||
};
|
||||
|
||||
// Build workspace menu items
|
||||
const workspaceMenuItems = useMemo(() => {
|
||||
const items = sortedWorkspaces.map((workspace) => {
|
||||
const isActive = workspace.uid === activeWorkspaceUid;
|
||||
const isPinned = preferences?.workspaces?.pinnedWorkspaceUids?.includes(workspace.uid);
|
||||
|
||||
return {
|
||||
id: workspace.uid,
|
||||
label: toTitleCase(workspace.name),
|
||||
onClick: () => handleWorkspaceSwitch(workspace.uid),
|
||||
className: `workspace-item ${isActive ? 'active' : ''}`,
|
||||
rightSection: (
|
||||
<div className="workspace-actions">
|
||||
{workspace.type !== 'default' && (
|
||||
<ActionIcon
|
||||
className={`pin-btn ${isPinned ? 'pinned' : ''}`}
|
||||
onClick={(e) => handlePinWorkspace(workspace.uid, e)}
|
||||
label={isPinned ? 'Unpin workspace' : 'Pin workspace'}
|
||||
size="sm"
|
||||
>
|
||||
{isPinned ? (
|
||||
<IconPinned size={14} stroke={1.5} />
|
||||
) : (
|
||||
<IconPin size={14} stroke={1.5} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
)}
|
||||
{isActive && <IconCheck size={16} stroke={1.5} className="check-icon" />}
|
||||
</div>
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
// Add label and action items
|
||||
items.push(
|
||||
{ type: 'label', label: 'Workspaces' },
|
||||
{
|
||||
id: 'create-workspace',
|
||||
leftSection: IconPlus,
|
||||
label: 'Create workspace',
|
||||
onClick: handleCreateWorkspace
|
||||
},
|
||||
{
|
||||
id: 'open-workspace',
|
||||
leftSection: IconFolder,
|
||||
label: 'Open workspace',
|
||||
onClick: handleOpenWorkspace
|
||||
},
|
||||
{
|
||||
id: 'import-workspace',
|
||||
leftSection: IconDownload,
|
||||
label: 'Import workspace',
|
||||
onClick: handleImportWorkspace
|
||||
},
|
||||
{
|
||||
id: 'manage-workspaces',
|
||||
leftSection: IconSettings,
|
||||
label: 'Manage workspaces',
|
||||
onClick: handleManageWorkspaces
|
||||
}
|
||||
);
|
||||
|
||||
return items;
|
||||
}, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace]);
|
||||
|
||||
return (
|
||||
<StyledWrapper className={`app-titlebar ${osClass} ${isFullScreen ? 'fullscreen' : ''}`}>
|
||||
{createWorkspaceModalOpen && (
|
||||
<CreateWorkspace onClose={() => setCreateWorkspaceModalOpen(false)} />
|
||||
)}
|
||||
{importWorkspaceModalOpen && (
|
||||
<ImportWorkspace onClose={() => setImportWorkspaceModalOpen(false)} />
|
||||
)}
|
||||
|
||||
<div className="titlebar-content">
|
||||
{/* Left section: Home + Workspace */}
|
||||
<div className="titlebar-left">
|
||||
<ActionIcon
|
||||
onClick={handleHomeClick}
|
||||
label="Home"
|
||||
size="lg"
|
||||
className="home-button"
|
||||
>
|
||||
<IconHome size={16} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
{/* Workspace Dropdown */}
|
||||
<MenuDropdown
|
||||
data-testid="workspace-menu"
|
||||
items={workspaceMenuItems}
|
||||
placement="bottom-start"
|
||||
selectedItemId={activeWorkspaceUid}
|
||||
>
|
||||
<WorkspaceName />
|
||||
</MenuDropdown>
|
||||
</div>
|
||||
|
||||
{/* Center section: Bruno logo + text */}
|
||||
<div className="titlebar-center">
|
||||
<Bruno width={18} />
|
||||
<span className="bruno-text">Bruno</span>
|
||||
</div>
|
||||
|
||||
{/* Right section: Action buttons */}
|
||||
<div className="titlebar-right">
|
||||
<div className="titlebar-actions">
|
||||
{/* Toggle sidebar */}
|
||||
<ActionIcon
|
||||
onClick={handleToggleSidebar}
|
||||
label={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}
|
||||
size="lg"
|
||||
data-testid="toggle-sidebar-button"
|
||||
>
|
||||
<IconSidebarToggle collapsed={sidebarCollapsed} size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
{/* Toggle devtools */}
|
||||
<ActionIcon
|
||||
onClick={handleToggleDevtools}
|
||||
label={isConsoleOpen ? 'Hide devtools' : 'Show devtools'}
|
||||
size="lg"
|
||||
data-testid="toggle-devtools-button"
|
||||
>
|
||||
<IconBottombarToggle collapsed={!isConsoleOpen} size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
|
||||
<ResponseLayoutToggle />
|
||||
</div>
|
||||
|
||||
{showWindowControls && (
|
||||
<div className="window-controls">
|
||||
<button
|
||||
className="window-control-btn minimize"
|
||||
onClick={handleMinimize}
|
||||
aria-label="Minimize"
|
||||
>
|
||||
<IconMinus size={16} stroke={1} />
|
||||
</button>
|
||||
<button
|
||||
className="window-control-btn maximize"
|
||||
onClick={handleMaximize}
|
||||
aria-label={isMaximized ? 'Restore' : 'Maximize'}
|
||||
>
|
||||
{isMaximized ? <IconCopy size={14} stroke={1} /> : <IconSquare size={14} stroke={1} />}
|
||||
</button>
|
||||
<button
|
||||
className="window-control-btn close"
|
||||
onClick={handleClose}
|
||||
aria-label="Close"
|
||||
>
|
||||
<IconX size={16} stroke={1} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppTitleBar;
|
||||
@@ -0,0 +1,38 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
|
||||
.body-mode-selector {
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
padding-left: 1.5rem !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
fill: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
76
packages/bruno-app/src/components/BodyModeSelector/index.js
Normal file
76
packages/bruno-app/src/components/BodyModeSelector/index.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { IconCaretDown, IconForms, IconBraces, IconCode, IconFileText, IconDatabase, IconFile, IconX } from '@tabler/icons';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import { humanizeRequestBodyMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DEFAULT_MODES = [
|
||||
{
|
||||
name: 'Form',
|
||||
options: [
|
||||
{ id: 'multipartForm', label: 'Multipart Form', leftSection: IconForms },
|
||||
{ id: 'formUrlEncoded', label: 'Form URL Encoded', leftSection: IconForms }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Raw',
|
||||
options: [
|
||||
{ id: 'json', label: 'JSON', leftSection: IconBraces },
|
||||
{ id: 'xml', label: 'XML', leftSection: IconCode },
|
||||
{ id: 'text', label: 'TEXT', leftSection: IconFileText },
|
||||
{ id: 'sparql', label: 'SPARQL', leftSection: IconDatabase }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Other',
|
||||
options: [
|
||||
{ id: 'file', label: 'File / Binary', leftSection: IconFile },
|
||||
{ id: 'none', label: 'No Body', leftSection: IconX }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const BodyModeSelector = ({
|
||||
currentMode,
|
||||
onModeChange,
|
||||
modes = DEFAULT_MODES,
|
||||
disabled = false,
|
||||
className = '',
|
||||
wrapperClassName = '',
|
||||
placement = 'bottom-end'
|
||||
}) => {
|
||||
// Add onClick handlers to mode options
|
||||
const menuItems = useMemo(() => {
|
||||
return modes.map((group) => ({
|
||||
...group,
|
||||
options: group.options.map((option) => ({
|
||||
...option,
|
||||
onClick: () => onModeChange(option.id)
|
||||
}))
|
||||
}));
|
||||
}, [modes, onModeChange]);
|
||||
|
||||
return (
|
||||
<StyledWrapper className={wrapperClassName}>
|
||||
<div className={`inline-flex items-center body-mode-selector ${disabled ? 'cursor-default' : 'cursor-pointer'}`}>
|
||||
<MenuDropdown
|
||||
items={menuItems}
|
||||
placement={placement}
|
||||
disabled={disabled}
|
||||
className={className}
|
||||
selectedItemId={currentMode}
|
||||
showGroupDividers={false}
|
||||
groupStyle="select"
|
||||
>
|
||||
<div className="flex items-center justify-center pl-3 py-1 select-none selected-body-mode">
|
||||
{humanizeRequestBodyMode(currentMode)}
|
||||
{' '}
|
||||
<IconCaretDown className="caret ml-2" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
</MenuDropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default BodyModeSelector;
|
||||
@@ -6,7 +6,7 @@ import StyledWrapper from './StyledWrapper';
|
||||
const BrunoSupport = ({ onClose }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal size="sm" title={'Support'} handleCancel={onClose} hideFooter={true}>
|
||||
<Modal size="sm" title="Support" handleCancel={onClose} hideFooter={true}>
|
||||
<div className="collection-options">
|
||||
<div className="mt-2">
|
||||
<a href="https://docs.usebruno.com" target="_blank" className="flex items-end">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -21,7 +22,8 @@ const BulkEditor = ({ params, onChange, onToggle, onSave, onRun }) => {
|
||||
<CodeEditor
|
||||
mode="text/plain"
|
||||
theme={displayedTheme}
|
||||
font={preferences.codeFont || 'default'}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
value={parsedParams}
|
||||
onEdit={handleEdit}
|
||||
onSave={onSave}
|
||||
|
||||
79
packages/bruno-app/src/components/Checkbox/StyledWrapper.js
Normal file
79
packages/bruno-app/src/components/Checkbox/StyledWrapper.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.checkbox-container {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-checkmark {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
visibility: ${(props) => props.checked ? 'visible' : 'hidden'};
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.checkbox-input {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid ${(props) => {
|
||||
if (props.checked && props.disabled) {
|
||||
return props.theme.colors.text.muted;
|
||||
}
|
||||
|
||||
if (props.checked && !props.disabled) {
|
||||
return props.theme.colors.text.yellow;
|
||||
}
|
||||
|
||||
return props.theme.colors.text.muted;
|
||||
}};
|
||||
border-radius: 4px;
|
||||
background-color: ${(props) => {
|
||||
if (props.checked && !props.disabled) {
|
||||
return props.theme.colors.text.yellow;
|
||||
}
|
||||
|
||||
if (props.checked && props.disabled) {
|
||||
return props.theme.colors.text.muted;
|
||||
}
|
||||
|
||||
return 'transparent';
|
||||
}};
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px ${(props) => props.theme.colors.text.yellow}40;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
45
packages/bruno-app/src/components/Checkbox/index.js
Normal file
45
packages/bruno-app/src/components/Checkbox/index.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import IconCheckMark from 'components/Icons/IconCheckMark';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
|
||||
const Checkbox = ({
|
||||
checked = false,
|
||||
disabled = false,
|
||||
onChange,
|
||||
className = '',
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
dataTestId = 'checkbox'
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const handleChange = (e) => {
|
||||
if (!disabled && onChange) {
|
||||
onChange(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper checked={checked} disabled={disabled} className={className}>
|
||||
<div className="checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
name={name}
|
||||
value={value}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
className="checkbox-input"
|
||||
data-testid={dataTestId}
|
||||
/>
|
||||
<IconCheckMark className="checkbox-checkmark" color={theme.examples.checkbox.color} size={14} />
|
||||
</div>
|
||||
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkbox;
|
||||
@@ -1,6 +1,12 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
&.read-only {
|
||||
div.CodeMirror .CodeMirror-cursor {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
div.CodeMirror {
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border: solid 1px ${(props) => props.theme.codemirror.border};
|
||||
@@ -12,6 +18,33 @@ const StyledWrapper = styled.div`
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.CodeMirror-placeholder {
|
||||
color: ${(props) => props.theme.text} !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
.CodeMirror-linenumber {
|
||||
text-align: left !important;
|
||||
padding-left: 3px !important;
|
||||
}
|
||||
|
||||
/* Override default lint highlight background when emphasizing the gutter */
|
||||
.CodeMirror-lint-line-error,
|
||||
.CodeMirror-lint-line-warning {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
/* Style line numbers when there's a lint issue */
|
||||
.CodeMirror-lint-line-error .CodeMirror-linenumber {
|
||||
color: ${(props) => props.theme.colors.text.danger} !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-line-warning .CodeMirror-linenumber {
|
||||
color: ${(props) => props.theme.colors.text.warning} !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Removes the glow outline around the folded json */
|
||||
.CodeMirror-foldmarker {
|
||||
text-shadow: none;
|
||||
@@ -67,41 +100,54 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-property,
|
||||
.cm-s-monokai span.cm-attribute {
|
||||
color: #9cdcfe !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-string {
|
||||
color: #ce9178 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-number {
|
||||
color: #b5cea8 !important;
|
||||
}
|
||||
|
||||
.cm-s-monokai span.cm-atom {
|
||||
color: #569cd6 !important;
|
||||
.cm-s-default, .cm-s-monokai {
|
||||
span.cm-def {
|
||||
color: ${(props) => props.theme.codemirror.tokens.definition} !important;
|
||||
}
|
||||
span.cm-property {
|
||||
color: ${(props) => props.theme.codemirror.tokens.property} !important;
|
||||
}
|
||||
span.cm-string {
|
||||
color: ${(props) => props.theme.codemirror.tokens.string} !important;
|
||||
}
|
||||
span.cm-number {
|
||||
color: ${(props) => props.theme.codemirror.tokens.number} !important;
|
||||
}
|
||||
span.cm-atom {
|
||||
color: ${(props) => props.theme.codemirror.tokens.atom} !important;
|
||||
}
|
||||
span.cm-variable, span.cm-variable-2 {
|
||||
color: ${(props) => props.theme.codemirror.tokens.variable} !important;
|
||||
}
|
||||
span.cm-keyword {
|
||||
color: ${(props) => props.theme.codemirror.tokens.keyword} !important;
|
||||
}
|
||||
span.cm-comment {
|
||||
color: ${(props) => props.theme.codemirror.tokens.comment} !important;
|
||||
}
|
||||
span.cm-operator {
|
||||
color: ${(props) => props.theme.codemirror.tokens.operator} !important;
|
||||
}
|
||||
span.cm-tag {
|
||||
color: ${(props) => props.theme.codemirror.tokens.tag} !important;
|
||||
}
|
||||
span.cm-tag.cm-bracket {
|
||||
color: ${(props) => props.theme.codemirror.tokens.tagBracket} !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Variable validation colors */
|
||||
.cm-variable-valid {
|
||||
color: green;
|
||||
color: ${(props) => props.theme.codemirror.variable.valid} !important;
|
||||
}
|
||||
.cm-variable-invalid {
|
||||
color: red;
|
||||
color: ${(props) => props.theme.codemirror.variable.invalid} !important;
|
||||
}
|
||||
|
||||
.CodeMirror-search-hint {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.cm-s-default span.cm-property {
|
||||
color: #1f61a0 !important;
|
||||
}
|
||||
|
||||
.cm-s-default span.cm-variable {
|
||||
color: #397d13 !important;
|
||||
}
|
||||
|
||||
|
||||
//matching bracket fix
|
||||
.CodeMirror-matchingbracket {
|
||||
@@ -109,6 +155,42 @@ const StyledWrapper = styled.div`
|
||||
text-decoration:unset;
|
||||
}
|
||||
|
||||
.cm-search-line-highlight {
|
||||
background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent};
|
||||
}
|
||||
|
||||
.cm-search-match {
|
||||
background: rgba(255, 193, 7, 0.25);
|
||||
}
|
||||
|
||||
.cm-search-current {
|
||||
background: rgba(255, 193, 7, 0.4);
|
||||
}
|
||||
|
||||
.lint-error-tooltip {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
background: ${(props) => props.theme.codemirror.bg};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
padding: 8px 12px;
|
||||
max-width: 400px;
|
||||
box-shadow: ${(props) => props.theme.shadow.sm};
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
line-height: 1.5;
|
||||
pointer-events: none;
|
||||
|
||||
.lint-tooltip-message {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.lint-tooltip-message.error {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
.lint-tooltip-message.warning {
|
||||
color: ${(props) => props.theme.colors.text.warning};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -14,6 +14,9 @@ import * as jsonlint from '@prantlf/jsonlint';
|
||||
import { JSHINT } from 'jshint';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
import { setupLinkAware } from 'utils/codemirror/linkAware';
|
||||
import { setupLintErrorTooltip } from 'utils/codemirror/lint-errors';
|
||||
import CodeMirrorSearch from 'components/CodeMirrorSearch';
|
||||
|
||||
const CodeMirror = require('codemirror');
|
||||
window.jsonlint = jsonlint;
|
||||
@@ -35,7 +38,12 @@ export default class CodeEditor extends React.Component {
|
||||
this.lintOptions = {
|
||||
esversion: 11,
|
||||
expr: true,
|
||||
asi: true
|
||||
asi: true,
|
||||
highlightLines: true
|
||||
};
|
||||
|
||||
this.state = {
|
||||
searchBarVisible: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,19 +52,22 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
const editor = (this.editor = CodeMirror(this._node, {
|
||||
value: this.props.value || '',
|
||||
placeholder: '...',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
lineWrapping: this.props.enableLineWrapping ?? true,
|
||||
tabSize: TAB_SIZE,
|
||||
mode: this.props.mode || 'application/ld+json',
|
||||
brunoVarInfo: {
|
||||
variables
|
||||
},
|
||||
brunoVarInfo: this.props.enableBrunoVarInfo !== false ? {
|
||||
variables,
|
||||
collection: this.props.collection,
|
||||
item: this.props.item
|
||||
} : false,
|
||||
keyMap: 'sublime',
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
showCursorWhenSelecting: true,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
lint: this.lintOptions,
|
||||
readOnly: this.props.readOnly,
|
||||
scrollbarStyle: 'overlay',
|
||||
@@ -83,28 +94,18 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
},
|
||||
'Cmd-F': (cm) => {
|
||||
if (this._isSearchOpen()) {
|
||||
// replace the older search component with the new one
|
||||
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
search && search.remove();
|
||||
if (!this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: true });
|
||||
}
|
||||
cm.execCommand('findPersistent');
|
||||
this._bindSearchHandler();
|
||||
this._appendSearchResultsCount();
|
||||
},
|
||||
'Ctrl-F': (cm) => {
|
||||
if (this._isSearchOpen()) {
|
||||
// replace the older search component with the new one
|
||||
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
search && search.remove();
|
||||
if (!this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: true });
|
||||
}
|
||||
cm.execCommand('findPersistent');
|
||||
this._bindSearchHandler();
|
||||
this._appendSearchResultsCount();
|
||||
},
|
||||
'Cmd-H': 'replace',
|
||||
'Ctrl-H': 'replace',
|
||||
Tab: function (cm) {
|
||||
'Tab': function (cm) {
|
||||
cm.getSelection().includes('\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()
|
||||
? cm.execCommand('indentMore')
|
||||
: cm.replaceSelection(' ', 'end');
|
||||
@@ -129,6 +130,11 @@ export default class CodeEditor extends React.Component {
|
||||
} else {
|
||||
this.editor.toggleComment();
|
||||
}
|
||||
},
|
||||
'Esc': () => {
|
||||
if (this.state.searchBarVisible) {
|
||||
this.setState({ searchBarVisible: false });
|
||||
}
|
||||
}
|
||||
},
|
||||
foldOptions: {
|
||||
@@ -145,7 +151,7 @@ export default class CodeEditor extends React.Component {
|
||||
} else if (this.props.mode == 'application/xml') {
|
||||
var doc = new DOMParser();
|
||||
try {
|
||||
//add header element and remove prefix namespaces for DOMParser
|
||||
// add header element and remove prefix namespaces for DOMParser
|
||||
var dcm = doc.parseFromString(
|
||||
'<a> ' + internal.replace(/(?<=\<|<\/)\w+:/g, '') + '</a>',
|
||||
'application/xml'
|
||||
@@ -182,16 +188,15 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
return found;
|
||||
});
|
||||
|
||||
|
||||
if (editor) {
|
||||
editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
editor.on('change', this._onEdit);
|
||||
editor.on('scroll', this.onScroll);
|
||||
editor.scrollTo(null, this.props.initialScroll);
|
||||
this.addOverlay();
|
||||
|
||||
const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);
|
||||
|
||||
|
||||
// Setup AutoComplete Helper for all modes
|
||||
const autoCompleteOptions = {
|
||||
showHintsFor: this.props.showHintsFor,
|
||||
@@ -202,6 +207,11 @@ export default class CodeEditor extends React.Component {
|
||||
editor,
|
||||
autoCompleteOptions
|
||||
);
|
||||
|
||||
setupLinkAware(editor);
|
||||
|
||||
// Setup lint error tooltip on line number hover
|
||||
this.cleanupLintErrorTooltip = setupLintErrorTooltip(editor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,8 +228,10 @@ export default class CodeEditor extends React.Component {
|
||||
CodeMirror.signal(this.editor, 'change', this.editor);
|
||||
}
|
||||
if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {
|
||||
const cursor = this.editor.getCursor();
|
||||
this.cachedValue = this.props.value;
|
||||
this.editor.setValue(this.props.value);
|
||||
this.editor.setCursor(cursor);
|
||||
}
|
||||
|
||||
if (this.editor) {
|
||||
@@ -227,6 +239,16 @@ export default class CodeEditor extends React.Component {
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.addOverlay();
|
||||
}
|
||||
|
||||
// Update collection and item when they change
|
||||
if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {
|
||||
if (!isEqual(this.props.collection, this.editor.options.brunoVarInfo.collection)) {
|
||||
this.editor.options.brunoVarInfo.collection = this.props.collection;
|
||||
}
|
||||
if (!isEqual(this.props.item, this.editor.options.brunoVarInfo.item)) {
|
||||
this.editor.options.brunoVarInfo.item = this.props.item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.theme !== prevProps.theme && this.editor) {
|
||||
@@ -237,19 +259,37 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.scrollTo(null, this.props.initialScroll);
|
||||
}
|
||||
|
||||
if (this.props.enableLineWrapping !== prevProps.enableLineWrapping) {
|
||||
this.editor.setOption('lineWrapping', this.props.enableLineWrapping);
|
||||
}
|
||||
|
||||
if (this.props.mode !== prevProps.mode) {
|
||||
this.editor.setOption('mode', this.props.mode);
|
||||
}
|
||||
|
||||
if (this.props.readOnly !== prevProps.readOnly && this.editor) {
|
||||
this.editor.setOption('readOnly', this.props.readOnly);
|
||||
}
|
||||
|
||||
this.ignoreChangeEvent = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) {
|
||||
this.editor.off('change', this._onEdit);
|
||||
this.editor.off('scroll', this.onScroll);
|
||||
this.editor = null;
|
||||
}
|
||||
if (this.props.onScroll) {
|
||||
this.props.onScroll(this.editor);
|
||||
}
|
||||
|
||||
this._unbindSearchHandler();
|
||||
if (this.brunoAutoCompleteCleanup) {
|
||||
this.brunoAutoCompleteCleanup();
|
||||
this.editor?._destroyLinkAware?.();
|
||||
this.editor.off('change', this._onEdit);
|
||||
|
||||
// Clean up lint error tooltip
|
||||
this.cleanupLintErrorTooltip?.();
|
||||
|
||||
const wrapper = this.editor.getWrapperElement();
|
||||
wrapper?.parentNode?.removeChild(wrapper);
|
||||
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,14 +299,22 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full w-full flex flex-col relative graphiql-container"
|
||||
className={`h-full w-full flex flex-col relative graphiql-container ${this.props.readOnly ? 'read-only' : ''}`}
|
||||
aria-label="Code Editor"
|
||||
font={this.props.font}
|
||||
fontSize={this.props.fontSize}
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<CodeMirrorSearch
|
||||
visible={this.state.searchBarVisible}
|
||||
editor={this.editor}
|
||||
onClose={() => this.setState({ searchBarVisible: false })}
|
||||
/>
|
||||
<div
|
||||
className={`editor-container${this.state.searchBarVisible ? ' search-bar-visible' : ''}`}
|
||||
ref={(node) => { this._node = node; }}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -275,12 +323,15 @@ export default class CodeEditor extends React.Component {
|
||||
let variables = getAllVariables(this.props.collection, this.props.item);
|
||||
this.variables = variables;
|
||||
|
||||
// Update brunoVarInfo with latest variables
|
||||
if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {
|
||||
this.editor.options.brunoVarInfo.variables = variables;
|
||||
}
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
onScroll = (event) => this.props.onScroll?.(event);
|
||||
|
||||
_onEdit = () => {
|
||||
if (!this.ignoreChangeEvent && this.editor) {
|
||||
this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);
|
||||
@@ -290,67 +341,4 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_isSearchOpen = () => {
|
||||
return document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
};
|
||||
|
||||
/**
|
||||
* Bind handler to search input to count number of search results
|
||||
*/
|
||||
_bindSearchHandler = () => {
|
||||
const searchInput = document.querySelector('.CodeMirror-search-field');
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', this._countSearchResults);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Unbind handler to search input to count number of search results
|
||||
*/
|
||||
_unbindSearchHandler = () => {
|
||||
const searchInput = document.querySelector('.CodeMirror-search-field');
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.removeEventListener('input', this._countSearchResults);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Append search results count to search dialog
|
||||
*/
|
||||
_appendSearchResultsCount = () => {
|
||||
const dialog = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
|
||||
if (dialog) {
|
||||
const searchResultsCount = document.createElement('span');
|
||||
searchResultsCount.id = this.searchResultsCountElementId;
|
||||
dialog.appendChild(searchResultsCount);
|
||||
|
||||
this._countSearchResults();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Count search results and update state
|
||||
*/
|
||||
_countSearchResults = () => {
|
||||
let count = 0;
|
||||
|
||||
const searchInput = document.querySelector('.CodeMirror-search-field');
|
||||
|
||||
if (searchInput && searchInput.value.length > 0) {
|
||||
// Escape special characters in search input to prevent RegExp crashes. Fixes #3051
|
||||
const text = new RegExp(escapeRegExp(searchInput.value), 'gi');
|
||||
const matches = this.editor.getValue().match(text);
|
||||
count = matches ? matches.length : 0;
|
||||
}
|
||||
|
||||
const searchResultsCountElement = document.querySelector(`#${this.searchResultsCountElementId}`);
|
||||
|
||||
if (searchResultsCountElement) {
|
||||
searchResultsCountElement.innerText = `${count} results`;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ jest.mock('codemirror', () => {
|
||||
|
||||
const MOCK_THEME = {
|
||||
codemirror: {
|
||||
bg: "#1e1e1e",
|
||||
border: "#333",
|
||||
bg: '#1e1e1e',
|
||||
border: '#333'
|
||||
},
|
||||
textLink: "#007acc",
|
||||
textLink: '#007acc'
|
||||
};
|
||||
|
||||
const setupEditorState = (editor, { value, cursorPosition }) => {
|
||||
@@ -27,8 +27,8 @@ const setupEditorState = (editor, { value, cursorPosition }) => {
|
||||
});
|
||||
|
||||
editor.state = {
|
||||
completionActive: null,
|
||||
}
|
||||
completionActive: null
|
||||
};
|
||||
};
|
||||
|
||||
const setupEditorWithRef = () => {
|
||||
@@ -47,5 +47,5 @@ describe('CodeEditor', () => {
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it("add CodeEditor related tests here", () => {});
|
||||
});
|
||||
it('add CodeEditor related tests here', () => {});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.bruno-search-bar {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0;
|
||||
padding: 1px 3px;
|
||||
width: auto;
|
||||
max-width: 320px;
|
||||
min-height: 22px;
|
||||
background: ${(props) => props.theme.background.base};
|
||||
color: ${(props) => props.theme.text.base};
|
||||
border: solid 1px ${(props) => props.theme.border.border2};
|
||||
border-radius: ${(props) => props.theme.border.radius.sm};
|
||||
}
|
||||
|
||||
.bruno-search-bar input {
|
||||
min-width: 80px;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 1px 2px;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
margin: 0 1px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.searchbar-icon-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0 1px;
|
||||
margin: 0 1px;
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
border-radius: 3px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.searchbar-result-count {
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
margin: 0 8px 0 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bruno-search-bar input {
|
||||
background: transparent;
|
||||
color: ${(props) => props.theme.colors.text.subtext2};
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
padding: 1px 2px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.searchbar-icon-btn:focus {
|
||||
outline: 1px solid ${(props) => props.theme.codemirror.border};
|
||||
}
|
||||
|
||||
.bruno-search-bar, .bruno-search-bar input {
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
||||
}
|
||||
|
||||
.cm-search-line-highlight {
|
||||
background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent};
|
||||
}
|
||||
|
||||
.searchbar-icon-btn.active {
|
||||
color: ${(props) => props.theme.brand};
|
||||
background-color: ${(props) => rgba(props.theme.brand, 0.1)};
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
201
packages/bruno-app/src/components/CodeMirrorSearch/index.js
Normal file
201
packages/bruno-app/src/components/CodeMirrorSearch/index.js
Normal file
@@ -0,0 +1,201 @@
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { IconRegex, IconArrowUp, IconArrowDown, IconX, IconLetterCase, IconLetterW } from '@tabler/icons';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');
|
||||
}
|
||||
|
||||
const CodeMirrorSearch = ({ visible, editor, onClose }) => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [regex, setRegex] = useState(false);
|
||||
const [caseSensitive, setCaseSensitive] = useState(false);
|
||||
const [wholeWord, setWholeWord] = useState(false);
|
||||
const [matchIndex, setMatchIndex] = useState(0);
|
||||
const [matchCount, setMatchCount] = useState(0);
|
||||
|
||||
const searchMarks = useRef([]);
|
||||
const searchLineHighlight = useRef(null);
|
||||
const searchMatches = useRef([]);
|
||||
|
||||
const debouncedSearchText = useDebounce(searchText, 150);
|
||||
|
||||
const memoizedMatches = useMemo(() => {
|
||||
if (!editor || !visible) return [];
|
||||
if (!debouncedSearchText) return [];
|
||||
|
||||
try {
|
||||
let query, options = {};
|
||||
if (regex) {
|
||||
try {
|
||||
query = new RegExp(debouncedSearchText, caseSensitive ? 'g' : 'gi');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
} else if (wholeWord) {
|
||||
const escaped = escapeRegExp(debouncedSearchText);
|
||||
query = new RegExp(`\\b${escaped}\\b`, caseSensitive ? 'g' : 'gi');
|
||||
} else {
|
||||
query = debouncedSearchText;
|
||||
options = { caseFold: !caseSensitive };
|
||||
}
|
||||
|
||||
const cursor = editor.getSearchCursor(query, { line: 0, ch: 0 }, options);
|
||||
const out = [];
|
||||
while (cursor.findNext()) {
|
||||
out.push({ from: cursor.from(), to: cursor.to() });
|
||||
}
|
||||
return out;
|
||||
} catch (e) {
|
||||
console.error('Search error:', e);
|
||||
return [];
|
||||
}
|
||||
}, [editor, visible, debouncedSearchText, regex, caseSensitive, wholeWord]);
|
||||
|
||||
const doSearch = useCallback((newIndex = 0) => {
|
||||
if (!editor) return;
|
||||
|
||||
// Clear previous marks
|
||||
searchMarks.current.forEach((mark) => mark.clear());
|
||||
searchMarks.current = [];
|
||||
// Clear previous line highlight
|
||||
if (searchLineHighlight.current !== null) {
|
||||
editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight');
|
||||
searchLineHighlight.current = null;
|
||||
}
|
||||
|
||||
if (!debouncedSearchText) {
|
||||
setMatchCount(0);
|
||||
setMatchIndex(0);
|
||||
searchMatches.current = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const matches = memoizedMatches;
|
||||
let matchIndex = matches.length ? Math.max(0, Math.min(newIndex, matches.length - 1)) : 0;
|
||||
matches.forEach((m, i) => {
|
||||
const mark = editor.markText(m.from, m.to, {
|
||||
className: i === matchIndex ? 'cm-search-current' : 'cm-search-match',
|
||||
clearOnEnter: true
|
||||
});
|
||||
searchMarks.current.push(mark);
|
||||
});
|
||||
|
||||
if (matches.length) {
|
||||
const currentLine = matches[matchIndex].from.line;
|
||||
editor.addLineClass(currentLine, 'wrap', 'cm-search-line-highlight');
|
||||
searchLineHighlight.current = currentLine;
|
||||
|
||||
editor.scrollIntoView(matches[matchIndex].from, 100);
|
||||
editor.setSelection(matches[matchIndex].from, matches[matchIndex].to);
|
||||
} else {
|
||||
searchLineHighlight.current = null;
|
||||
}
|
||||
|
||||
setMatchCount(matches.length);
|
||||
setMatchIndex(matchIndex);
|
||||
searchMatches.current = matches;
|
||||
} catch (e) {
|
||||
console.error('Search error:', e);
|
||||
setMatchCount(0);
|
||||
setMatchIndex(0);
|
||||
searchMatches.current = [];
|
||||
}
|
||||
}, [debouncedSearchText, regex, caseSensitive, wholeWord, editor, memoizedMatches]);
|
||||
|
||||
useEffect(() => {
|
||||
doSearch(0, debouncedSearchText);
|
||||
}, [debouncedSearchText, doSearch]);
|
||||
|
||||
const handleSearchBarClose = useCallback(() => {
|
||||
searchMarks.current.forEach((mark) => mark.clear());
|
||||
searchMarks.current = [];
|
||||
if (searchLineHighlight.current !== null && editor) {
|
||||
editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight');
|
||||
searchLineHighlight.current = null;
|
||||
}
|
||||
searchMatches.current = [];
|
||||
if (onClose) onClose();
|
||||
// Focus the editor after closing the search bar
|
||||
if (editor) {
|
||||
setTimeout(() => editor.focus(), 0);
|
||||
}
|
||||
}, [editor, onClose]);
|
||||
|
||||
const handleSearchTextChange = (text) => {
|
||||
setSearchText(text);
|
||||
setMatchIndex(0);
|
||||
};
|
||||
|
||||
const handleToggleRegex = () => {
|
||||
setRegex((prev) => !prev);
|
||||
setMatchIndex(0);
|
||||
doSearch(0);
|
||||
};
|
||||
|
||||
const handleToggleCase = () => {
|
||||
setCaseSensitive((prev) => !prev);
|
||||
setMatchIndex(0);
|
||||
doSearch(0);
|
||||
};
|
||||
|
||||
const handleToggleWholeWord = () => {
|
||||
setWholeWord((prev) => !prev);
|
||||
setMatchIndex(0);
|
||||
doSearch(0);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (!searchMatches.current || !searchMatches.current.length) return;
|
||||
let next = (matchIndex + 1) % searchMatches.current.length;
|
||||
setMatchIndex(next);
|
||||
doSearch(next);
|
||||
};
|
||||
|
||||
const handlePrev = () => {
|
||||
if (!searchMatches.current || !searchMatches.current.length) return;
|
||||
let prev = (matchIndex - 1 + searchMatches.current.length) % searchMatches.current.length;
|
||||
setMatchIndex(prev);
|
||||
doSearch(prev);
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="bruno-search-bar">
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
value={searchText}
|
||||
onChange={(e) => handleSearchTextChange(e.target.value)}
|
||||
placeholder="Search..."
|
||||
spellCheck={false}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) handleNext();
|
||||
if (e.key === 'Enter' && e.shiftKey) handlePrev();
|
||||
if (e.key === 'Escape') handleSearchBarClose();
|
||||
}}
|
||||
/>
|
||||
<span className="searchbar-result-count">{matchCount > 0 ? `${matchIndex + 1} / ${matchCount}` : '0 results'}</span>
|
||||
<ToolHint text="Regex search" toolhintId="searchbar-regex-toolhint" place="top">
|
||||
<button className={`searchbar-icon-btn ${regex ? 'active' : ''}`} onClick={handleToggleRegex}><IconRegex size={16} /></button>
|
||||
</ToolHint>
|
||||
<ToolHint text="Case sensitive" toolhintId="searchbar-case-toolhint" place="top">
|
||||
<button className={`searchbar-icon-btn ${caseSensitive ? 'active' : ''}`} onClick={handleToggleCase}><IconLetterCase size={14} /></button>
|
||||
</ToolHint>
|
||||
<ToolHint text="Whole word" toolhintId="searchbar-wholeword-toolhint" place="top">
|
||||
<button className={`searchbar-icon-btn ${wholeWord ? 'active' : ''}`} onClick={handleToggleWholeWord}><IconLetterW size={14} /></button>
|
||||
</ToolHint>
|
||||
<button className="searchbar-icon-btn" title="Previous" onClick={handlePrev}><IconArrowUp size={14} /></button>
|
||||
<button className="searchbar-icon-btn" title="Next" onClick={handleNext}><IconArrowDown size={14} /></button>
|
||||
<button className="searchbar-icon-btn" title="Close" onClick={handleSearchBarClose}><IconX size={14} /></button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeMirrorSearch;
|
||||
@@ -2,7 +2,8 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
@@ -13,7 +14,8 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.auth-placement-selector {
|
||||
padding: 0.5rem 0px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
padding: 0.2rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
@@ -37,7 +39,6 @@ const Wrapper = styled.div`
|
||||
|
||||
.auth-type-label {
|
||||
width: fit-content;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import Dropdown from 'components/Dropdown';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { humanizeRequestAPIKeyPlacement } from 'utils/collections';
|
||||
|
||||
@@ -16,9 +16,9 @@ const ApiKeyAuth = ({ collection }) => {
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const apikeyAuth = get(collection, 'root.request.auth.apikey', {});
|
||||
const apikeyAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.apikey', {}) : get(collection, 'root.request.auth.apikey', {});
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
@@ -43,43 +43,45 @@ const ApiKeyAuth = ({ collection }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
!apikeyAuth?.placement &&
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'apikey',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
placement: 'header'
|
||||
}
|
||||
})
|
||||
);
|
||||
!apikeyAuth?.placement
|
||||
&& dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'apikey',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
placement: 'header'
|
||||
}
|
||||
})
|
||||
);
|
||||
}, [apikeyAuth]);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Key</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={apikeyAuth.key || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAuthChange('key', val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Value</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Value</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={apikeyAuth.value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAuthChange('value', val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Add To</label>
|
||||
<label className="block mb-1">Add To</label>
|
||||
<div className="inline-flex items-center cursor-pointer auth-placement-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
|
||||
.auth-mode-selector {
|
||||
background: transparent;
|
||||
|
||||
.auth-mode-label {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
color: ${(props) => props.theme.primary.text};
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140, 140, 140);
|
||||
}
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import MenuDropdown from 'ui/MenuDropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateCollectionAuthMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
@@ -9,113 +9,77 @@ import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const AuthMode = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const authMode = get(collection, 'root.request.auth.mode');
|
||||
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<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) => {
|
||||
const onModeChange = useCallback((value) => {
|
||||
dispatch(
|
||||
updateCollectionAuthMode({
|
||||
collectionUid: collection.uid,
|
||||
mode: value
|
||||
})
|
||||
);
|
||||
};
|
||||
}, [dispatch, collection.uid]);
|
||||
|
||||
const menuItems = useMemo(() => [
|
||||
{
|
||||
id: 'awsv4',
|
||||
label: 'AWS Sig v4',
|
||||
onClick: () => onModeChange('awsv4')
|
||||
},
|
||||
{
|
||||
id: 'basic',
|
||||
label: 'Basic Auth',
|
||||
onClick: () => onModeChange('basic')
|
||||
},
|
||||
{
|
||||
id: 'wsse',
|
||||
label: 'WSSE Auth',
|
||||
onClick: () => onModeChange('wsse')
|
||||
},
|
||||
{
|
||||
id: 'bearer',
|
||||
label: 'Bearer Token',
|
||||
onClick: () => onModeChange('bearer')
|
||||
},
|
||||
{
|
||||
id: 'digest',
|
||||
label: 'Digest Auth',
|
||||
onClick: () => onModeChange('digest')
|
||||
},
|
||||
{
|
||||
id: 'ntlm',
|
||||
label: 'NTLM Auth',
|
||||
onClick: () => onModeChange('ntlm')
|
||||
},
|
||||
{
|
||||
id: 'oauth2',
|
||||
label: 'OAuth 2.0',
|
||||
onClick: () => onModeChange('oauth2')
|
||||
},
|
||||
{
|
||||
id: 'apikey',
|
||||
label: 'API Key',
|
||||
onClick: () => onModeChange('apikey')
|
||||
},
|
||||
{
|
||||
id: 'none',
|
||||
label: 'No Auth',
|
||||
onClick: () => onModeChange('none')
|
||||
}
|
||||
], [onModeChange]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer auth-mode-selector">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('awsv4');
|
||||
}}
|
||||
>
|
||||
AWS Sig v4
|
||||
<MenuDropdown
|
||||
items={menuItems}
|
||||
placement="bottom-end"
|
||||
selectedItemId={authMode}
|
||||
>
|
||||
<div className="flex items-center justify-center auth-mode-label select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('basic');
|
||||
}}
|
||||
>
|
||||
Basic Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('wsse');
|
||||
}}
|
||||
>
|
||||
WSSE 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('apikey');
|
||||
}}
|
||||
>
|
||||
API Key
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('none');
|
||||
}}
|
||||
>
|
||||
No Auth
|
||||
</div>
|
||||
</Dropdown>
|
||||
</MenuDropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,8 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const AwsV4Auth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = get(collection, 'root.request.auth.awsv4', {});
|
||||
const awsv4Auth = collection.draft?.root ? get(collection, 'draft.root.request.auth.awsv4', {}) : get(collection, 'root.request.auth.awsv4', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleAccessKeyIdChange = (accessKeyId) => {
|
||||
dispatch(
|
||||
@@ -123,19 +123,20 @@ const AwsV4Auth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Access Key ID</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Access Key ID</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.accessKeyId || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAccessKeyIdChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2 flex items-center">
|
||||
<label className="block mb-1">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-3 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.secretAccessKey || ''}
|
||||
theme={storedTheme}
|
||||
@@ -143,51 +144,56 @@ const AwsV4Auth = ({ collection }) => {
|
||||
onChange={(val) => handleSecretAccessKeyChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="awsv4-secret-access-key" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Session Token</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Session Token</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.sessionToken || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleSessionTokenChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Service</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Service</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.service || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleServiceChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Region</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Region</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.region || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleRegionChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Profile Name</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Profile Name</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.profileName || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleProfileNameChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -2,7 +2,8 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BasicAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const basicAuth = get(collection, 'root.request.auth.basic', {});
|
||||
const basicAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.basic', {}) : get(collection, 'root.request.auth.basic', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(basicAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
@@ -47,18 +47,19 @@ const BasicAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<label className="block mb-1">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.password || ''}
|
||||
@@ -67,6 +68,7 @@ const BasicAuth = ({ collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BearerAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const bearerToken = get(collection, 'root.request.auth.bearer.token', '');
|
||||
const bearerToken = collection.draft?.root ? get(collection, 'draft.root.request.auth.bearer.token', '') : get(collection, 'root.request.auth.bearer.token', '');
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(bearerToken);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleTokenChange = (token) => {
|
||||
dispatch(
|
||||
@@ -33,7 +33,7 @@ const BearerAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Token</label>
|
||||
<label className="block mb-1">Token</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={bearerToken}
|
||||
@@ -42,6 +42,7 @@ const BearerAuth = ({ collection }) => {
|
||||
onChange={(val) => handleTokenChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="bearer-token" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DigestAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const digestAuth = get(collection, 'root.request.auth.digest', {});
|
||||
const digestAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.digest', {}) : get(collection, 'root.request.auth.digest', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(digestAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
@@ -47,18 +47,19 @@ const DigestAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={digestAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<label className="block mb-1">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={digestAuth.password || ''}
|
||||
@@ -67,6 +68,7 @@ const DigestAuth = ({ collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="digest-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -6,25 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const NTLMAuth = ({ collection }) => {
|
||||
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const ntlmAuth = get(collection, 'root.request.auth.ntlm', {});
|
||||
const ntlmAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.ntlm', {}) : get(collection, 'root.request.auth.ntlm', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
@@ -67,26 +60,24 @@ const NTLMAuth = ({ collection }) => {
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<label className="block mb-1">Password</label>
|
||||
<div className="single-line-editor-wrapper mb-3 flex items-center">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
@@ -94,11 +85,12 @@ const NTLMAuth = ({ collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="ntlm-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Domain</label>
|
||||
<label className="block mb-1">Domain</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.domain || ''}
|
||||
@@ -106,6 +98,7 @@ const NTLMAuth = ({ collection }) => {
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleDomainChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
}
|
||||
.single-line-editor-wrapper {
|
||||
max-width: 400px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -10,14 +10,14 @@ import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCr
|
||||
import OAuth2Implicit from 'components/RequestPane/Auth/OAuth2/Implicit/index';
|
||||
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
|
||||
|
||||
const GrantTypeComponentMap = ({collection }) => {
|
||||
const GrantTypeComponentMap = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const save = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
dispatch(saveCollectionSettings(collection.uid));
|
||||
};
|
||||
|
||||
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
|
||||
let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', {});
|
||||
|
||||
switch (grantType) {
|
||||
@@ -40,7 +40,7 @@ const GrantTypeComponentMap = ({collection }) => {
|
||||
};
|
||||
|
||||
const OAuth2 = ({ collection }) => {
|
||||
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
|
||||
let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {});
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
|
||||
@@ -2,7 +2,8 @@ import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.subtext1};
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
|
||||
@@ -6,18 +6,18 @@ import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const WsseAuth = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const wsseAuth = get(collection, 'root.request.auth.wsse', {});
|
||||
const wsseAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.wsse', {}) : get(collection, 'root.request.auth.wsse', {});
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(wsseAuth?.password);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleUserChange = (username) => {
|
||||
dispatch(
|
||||
@@ -47,18 +47,19 @@ const WsseAuth = ({ collection }) => {
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<label className="block mb-1">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-3">
|
||||
<SingleLineEditor
|
||||
value={wsseAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUserChange(val)}
|
||||
collection={collection}
|
||||
isCompact
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<label className="block mb-1">Password</label>
|
||||
<div className="single-line-editor-wrapper flex items-center">
|
||||
<SingleLineEditor
|
||||
value={wsseAuth.password || ''}
|
||||
@@ -67,6 +68,7 @@ const WsseAuth = ({ collection }) => {
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
isCompact
|
||||
/>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="wsse-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
|
||||
@@ -8,17 +8,17 @@ import BasicAuth from './BasicAuth';
|
||||
import DigestAuth from './DigestAuth';
|
||||
import WsseAuth from './WsseAuth';
|
||||
import ApiKeyAuth from './ApiKeyAuth/';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import OAuth2 from './OAuth2';
|
||||
import NTLMAuth from './NTLMAuth';
|
||||
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Auth = ({ collection }) => {
|
||||
const authMode = get(collection, 'root.request.auth.mode');
|
||||
const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
@@ -36,7 +36,7 @@ const Auth = ({ collection }) => {
|
||||
}
|
||||
case 'ntlm': {
|
||||
return <NTLMAuth collection={collection} />;
|
||||
}
|
||||
}
|
||||
case 'oauth2': {
|
||||
return <OAuth2 collection={collection} />;
|
||||
}
|
||||
@@ -60,9 +60,9 @@ const Auth = ({ collection }) => {
|
||||
</div>
|
||||
{getAuthView()}
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -30,11 +30,11 @@ const StyledWrapper = styled.div`
|
||||
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};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
border: solid 1px ${(props) => props.theme.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
@@ -48,32 +48,37 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
.protocol-https,
|
||||
.protocol-grpcs {
|
||||
.protocol-grpcs,
|
||||
.protocol-wss {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
transition: transform 0.3s ease-in-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.protocol-https {
|
||||
animation: slideUpDown 6s infinite;
|
||||
animation: slideUpDown 9s infinite;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.protocol-grpcs {
|
||||
animation: slideUpDown 6s infinite 3s;
|
||||
animation: slideUpDown 9s infinite 3s;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.protocol-wss {
|
||||
animation: slideUpDown 9s infinite 6s;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
@keyframes slideUpDown {
|
||||
0%, 45% {
|
||||
0%, 30% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50%, 95% {
|
||||
33.33%, 97% {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
100% {
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
import React from 'react';
|
||||
import { IconCertificate, IconTrash, IconWorld } from '@tabler/icons';
|
||||
import { useFormik } from 'formik';
|
||||
import { uuid } from 'utils/common';
|
||||
import * as Yup from 'yup';
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons';
|
||||
import { useState } from 'react';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useRef } from 'react';
|
||||
import path from 'utils/common/path';
|
||||
import SensitiveFieldWarning from 'components/SensitiveFieldWarning/index';
|
||||
import SingleLineEditor from 'components/SingleLineEditor/index';
|
||||
import { useDetectSensitiveField } from 'hooks/useDetectSensitiveField/index';
|
||||
import { useTheme } from 'styled-components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateCollectionClientCertificates } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import get from 'lodash/get';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
const ClientCertSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// Get client certs from draft if exists, otherwise from brunoConfig
|
||||
const clientCertConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.clientCertificates.certs', [])
|
||||
: get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const certFilePathInputRef = useRef();
|
||||
const keyFilePathInputRef = useRef();
|
||||
const pfxFilePathInputRef = useRef();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
@@ -28,7 +40,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
domain: Yup.string()
|
||||
.required()
|
||||
.trim()
|
||||
.test('not-empty-after-trim', 'Domain is required', value => value && value.trim().length > 0),
|
||||
.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',
|
||||
@@ -62,28 +74,47 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
passphrase: values.passphrase
|
||||
};
|
||||
}
|
||||
onUpdate(relevantValues);
|
||||
|
||||
// Add the new cert to the existing certs in draft
|
||||
const updatedCerts = [...clientCertConfig, relevantValues];
|
||||
const clientCertificates = {
|
||||
enabled: true,
|
||||
certs: updatedCerts
|
||||
};
|
||||
|
||||
dispatch(updateCollectionClientCertificates({
|
||||
collectionUid: collection.uid,
|
||||
clientCertificates
|
||||
}));
|
||||
|
||||
formik.resetForm();
|
||||
resetFileInputFields();
|
||||
}
|
||||
});
|
||||
|
||||
const { isSensitive } = useDetectSensitiveField(collection);
|
||||
const { showWarning, warningMessage } = isSensitive(formik.values.passphrase);
|
||||
|
||||
const getFile = (e) => {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]);
|
||||
if (filePath) {
|
||||
let relativePath = path.relative(root, filePath);
|
||||
let relativePath = path.relative(collection.pathname, filePath);
|
||||
formik.setFieldValue(e.name, relativePath);
|
||||
}
|
||||
};
|
||||
|
||||
const resetFileInputFields = () => {
|
||||
certFilePathInputRef.current.value = '';
|
||||
keyFilePathInputRef.current.value = '';
|
||||
pfxFilePathInputRef.current.value = '';
|
||||
if (certFilePathInputRef.current) {
|
||||
certFilePathInputRef.current.value = '';
|
||||
}
|
||||
if (keyFilePathInputRef.current) {
|
||||
keyFilePathInputRef.current.value = '';
|
||||
}
|
||||
if (pfxFilePathInputRef.current) {
|
||||
pfxFilePathInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||
|
||||
const handleTypeChange = (e) => {
|
||||
formik.setFieldValue('type', e.target.value);
|
||||
if (e.target.value === 'cert') {
|
||||
@@ -97,34 +128,49 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (indexToRemove) => {
|
||||
const updatedCerts = clientCertConfig.filter((cert, index) => index !== indexToRemove);
|
||||
const clientCertificates = {
|
||||
enabled: true,
|
||||
certs: updatedCerts
|
||||
};
|
||||
|
||||
dispatch(updateCollectionClientCertificates({
|
||||
collectionUid: collection.uid,
|
||||
clientCertificates
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full h-full">
|
||||
<div className="text-xs mb-4 text-muted">Add client certificates to be used for specific domains.</div>
|
||||
|
||||
<h1 className="font-semibold">Client Certificates</h1>
|
||||
<h1 className="font-medium">Client Certificates</h1>
|
||||
<ul className="mt-4">
|
||||
{!clientCertConfig.length
|
||||
? 'No client certificates added'
|
||||
: 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}
|
||||
<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>
|
||||
<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={() => handleRemove(index)} className="remove-certificate ml-2">
|
||||
<IconTrash size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
</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>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h1 className="font-semibold mt-8 mb-2">Add Client Certificate</h1>
|
||||
<h1 className="font-medium mt-8 mb-2">Add Client Certificate</h1>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="domain">
|
||||
@@ -135,6 +181,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<span className="protocol-placeholder">
|
||||
<span className="protocol-https">https://</span>
|
||||
<span className="protocol-grpcs">grpcs://</span>
|
||||
<span className="protocol-wss">wss://</span>
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
@@ -314,30 +361,27 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
Passphrase
|
||||
</label>
|
||||
<div className="textbox flex flex-row items-center w-[300px] h-[1.70rem] relative">
|
||||
<input
|
||||
id="passphrase"
|
||||
type={passwordVisible ? 'text' : 'password'}
|
||||
name="passphrase"
|
||||
className="outline-none w-64 bg-transparent"
|
||||
onChange={formik.handleChange}
|
||||
<SingleLineEditor
|
||||
value={formik.values.passphrase || ''}
|
||||
theme={storedTheme}
|
||||
onChange={(val) => formik.setFieldValue('passphrase', val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm absolute right-0 l"
|
||||
onClick={() => setPasswordVisible(!passwordVisible)}
|
||||
>
|
||||
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
|
||||
</button>
|
||||
{showWarning && <SensitiveFieldWarning fieldName="basic-password" warningMessage={warningMessage} />}
|
||||
</div>
|
||||
{formik.touched.passphrase && formik.errors.passphrase ? (
|
||||
<div className="ml-1 text-red-500">{formik.errors.passphrase}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
<div className="mt-6 flex flex-row gap-2 items-center">
|
||||
<Button type="submit" size="sm" data-testid="add-client-cert">
|
||||
Add
|
||||
</button>
|
||||
</Button>
|
||||
<div className="h-4 border-l border-gray-600"></div>
|
||||
<Button type="button" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import 'github-markdown-css/github-markdown.css';
|
||||
import get from 'lodash/get';
|
||||
import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections';
|
||||
import { updateCollectionDocs, deleteCollectionDraft } from 'providers/ReduxStore/slices/collections';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Markdown from 'components/MarkDown';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconEdit, IconX, IconFileText } from '@tabler/icons';
|
||||
import Button from 'ui/Button/index';
|
||||
import ActionIcon from 'ui/ActionIcon/index';
|
||||
|
||||
const Docs = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { displayedTheme } = useTheme();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const docs = get(collection, 'root.docs', '');
|
||||
const docs = collection.draft?.root ? get(collection, 'draft.root.docs', '') : get(collection, 'root.docs', '');
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const toggleViewMode = () => {
|
||||
@@ -31,41 +33,41 @@ const Docs = ({ collection }) => {
|
||||
};
|
||||
|
||||
const handleDiscardChanges = () => {
|
||||
dispatch(
|
||||
dispatch((
|
||||
updateCollectionDocs({
|
||||
collectionUid: collection.uid,
|
||||
docs: docs
|
||||
})
|
||||
}))
|
||||
);
|
||||
toggleViewMode();
|
||||
}
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
dispatch(saveCollectionSettings(collection.uid));
|
||||
toggleViewMode();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full relative flex flex-col">
|
||||
<div className='flex flex-row w-full justify-between items-center mb-4'>
|
||||
<div className='text-lg font-medium flex items-center gap-2'>
|
||||
<div className="flex flex-row w-full justify-between items-center mb-4">
|
||||
<div className="text-lg font-medium flex items-center gap-2">
|
||||
<IconFileText size={20} strokeWidth={1.5} />
|
||||
Documentation
|
||||
</div>
|
||||
<div className='flex flex-row gap-2 items-center justify-center'>
|
||||
<div className="flex flex-row gap-2 items-center justify-center">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<div className="editing-mode" role="tab" onClick={handleDiscardChanges}>
|
||||
<IconX className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={onSave}>
|
||||
<Button type="button" color="secondary" onClick={handleDiscardChanges}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" onClick={onSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
<ActionIcon className="editing-mode" onClick={toggleViewMode}>
|
||||
<IconEdit className="cursor-pointer" size={16} strokeWidth={1.5} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,14 +83,13 @@ const Docs = ({ collection }) => {
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
) : (
|
||||
<div className='h-full overflow-auto pl-1'>
|
||||
<div className='h-[1px] min-h-[500px]'>
|
||||
<div className="h-full overflow-auto pl-1">
|
||||
<div className="h-[1px] min-h-[500px]">
|
||||
{
|
||||
docs?.length > 0 ?
|
||||
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
|
||||
:
|
||||
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={documentationPlaceholder} />
|
||||
}
|
||||
docs?.length > 0
|
||||
? <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
|
||||
: <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={documentationPlaceholder} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -98,7 +99,6 @@ const Docs = ({ collection }) => {
|
||||
|
||||
export default Docs;
|
||||
|
||||
|
||||
const documentationPlaceholder = `
|
||||
Welcome to your collection documentation! This space is designed to help you document your API collection effectively.
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.available-certificates {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
|
||||
button.remove-certificate {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,263 +0,0 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash, IconFile, IconFileImport, IconAlertCircle } from '@tabler/icons';
|
||||
import { getRelativePath, getBasename, getDirPath } from 'utils/common/path';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { existsSync, resolvePath } from '../../../utils/filesystem';
|
||||
|
||||
const GrpcSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
brunoConfig: { grpc: grpcConfig = {} }
|
||||
} = collection;
|
||||
|
||||
const fileInputRef = useRef(null);
|
||||
const [protoFileValidity, setProtoFileValidity] = useState({});
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
protoFiles: grpcConfig.protoFiles || []
|
||||
},
|
||||
onSubmit: (newGrpcConfig) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.grpc = newGrpcConfig;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
|
||||
toast.success('gRPC settings updated');
|
||||
}
|
||||
});
|
||||
|
||||
// Get file path using the ipcRenderer
|
||||
const getProtoFile = (event) => {
|
||||
const files = event?.files;
|
||||
if (files && files.length > 0) {
|
||||
const newProtoFiles = [...formik.values.protoFiles];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(files[i]);
|
||||
if (filePath) {
|
||||
const relativePath = getRelativePath(filePath, collection.pathname);
|
||||
const protoFileObj = {
|
||||
path: relativePath,
|
||||
type: 'file'
|
||||
};
|
||||
|
||||
// Check if this path already exists
|
||||
const exists = newProtoFiles.some(pf => pf.path === protoFileObj.path);
|
||||
if (!exists) {
|
||||
newProtoFiles.push(protoFileObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formik.setFieldValue('protoFiles', newProtoFiles);
|
||||
// Reset the file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for removing a proto file
|
||||
const handleRemoveProtoFile = (index) => {
|
||||
const updatedProtoFiles = [...formik.values.protoFiles];
|
||||
updatedProtoFiles.splice(index, 1);
|
||||
formik.setFieldValue('protoFiles', updatedProtoFiles);
|
||||
};
|
||||
|
||||
// Handle the browse button click
|
||||
const handleBrowseClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
// Check if a proto file path is valid
|
||||
const isProtoFileValid = async (protoFile) => {
|
||||
try {
|
||||
const absolutePath = await resolvePath(protoFile.path, collection.pathname);
|
||||
return await existsSync(absolutePath);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Validate all proto files and update state
|
||||
useEffect(() => {
|
||||
const validateProtoFiles = async () => {
|
||||
const validityMap = {};
|
||||
for (const file of formik.values.protoFiles) {
|
||||
validityMap[file.path] = await isProtoFileValid(file);
|
||||
}
|
||||
setProtoFileValidity(validityMap);
|
||||
};
|
||||
|
||||
validateProtoFiles();
|
||||
}, [formik.values.protoFiles, collection.pathname]);
|
||||
|
||||
// Handle replacing an invalid proto file
|
||||
const handleReplaceProtoFile = (index) => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
// Store the index to replace after file selection
|
||||
fileInputRef.current.dataset.replaceIndex = index;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle file input change
|
||||
const handleFileInputChange = (e) => {
|
||||
const replaceIndex = e.target.dataset.replaceIndex;
|
||||
if (replaceIndex !== undefined) {
|
||||
// Handle replacement
|
||||
const files = e.target.files;
|
||||
if (files && files.length > 0) {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(files[0]);
|
||||
if (filePath) {
|
||||
const relativePath = getRelativePath(filePath, collection.pathname);
|
||||
const updatedProtoFiles = [...formik.values.protoFiles];
|
||||
updatedProtoFiles[replaceIndex] = {
|
||||
path: relativePath,
|
||||
type: 'file'
|
||||
};
|
||||
formik.setFieldValue('protoFiles', updatedProtoFiles);
|
||||
}
|
||||
}
|
||||
delete e.target.dataset.replaceIndex;
|
||||
} else {
|
||||
getProtoFile(e.target);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label className="font-semibold text-sm mb-3 flex items-center" htmlFor="protoFiles">
|
||||
Add Proto Files
|
||||
<span id="proto-files-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="text-gray-500 cursor-pointer" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="proto-files-tooltip"
|
||||
className="tooltip-mod font-normal"
|
||||
html="Keep your proto files within the collection folder or the corresponding git repository to ensure paths remain valid when sharing the collection."
|
||||
/>
|
||||
</label>
|
||||
<div className="flex flex-col">
|
||||
{/* Hidden file input for file selection */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
accept=".proto"
|
||||
multiple
|
||||
onChange={handleFileInputChange}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* File selection options */}
|
||||
<div className="flex flex-col space-y-3">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary flex items-center"
|
||||
onClick={handleBrowseClick}
|
||||
>
|
||||
<IconFileImport size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Browse for proto files
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-t border-neutral-600 my-2"></div>
|
||||
|
||||
{/* List of added proto files */}
|
||||
<div>
|
||||
<div className="text-sm font-semibold mb-2 flex items-center">
|
||||
<IconFile size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Added Proto Files ({formik.values.protoFiles.length})
|
||||
</div>
|
||||
|
||||
{formik.values.protoFiles.length === 0 ? (
|
||||
<div className="text-neutral-500 text-sm italic">No proto files added yet</div>
|
||||
) : (
|
||||
<>
|
||||
{formik.values.protoFiles.some(file => !protoFileValidity[file.path]) && (
|
||||
<div className="text-xs text-red-500 mb-2 flex items-center bg-red-50 dark:bg-red-900/20 p-2 rounded">
|
||||
<IconAlertCircle size={14} className="mr-1" />
|
||||
Some proto files cannot be found at their specified paths. Use the "Replace" option to update their locations.
|
||||
</div>
|
||||
)}
|
||||
<ul className="mt-4">
|
||||
{formik.values.protoFiles.map((file, index) => {
|
||||
const isValid = protoFileValidity[file.path];
|
||||
return (
|
||||
<li key={index} className="flex items-center available-certificates p-2 rounded-lg mb-2">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<div className="flex w-full items-center">
|
||||
<IconFile className="mr-2" size={18} strokeWidth={1.5} />
|
||||
<div
|
||||
className="overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px] text-sm"
|
||||
title={file.path}
|
||||
>
|
||||
{getBasename(file.path)}
|
||||
<span className="text-xs text-neutral-500 ml-2">
|
||||
{getDirPath(file.path)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-end">
|
||||
{!isValid && (
|
||||
<div className="flex items-center mr-2">
|
||||
<IconAlertCircle
|
||||
size={16}
|
||||
className="text-red-500"
|
||||
title="Proto file not found. Click to replace."
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-red-500 ml-1 hover:underline"
|
||||
onClick={() => handleReplaceProtoFile(index)}
|
||||
>
|
||||
Replace
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="remove-certificate ml-2"
|
||||
onClick={() => handleRemoveProtoFile(index)}
|
||||
title="Remove file"
|
||||
>
|
||||
<IconTrash size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default GrpcSettings;
|
||||
@@ -6,7 +6,7 @@ const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
@@ -16,7 +16,7 @@ const Wrapper = styled.div`
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
@@ -33,7 +33,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.btn-add-header {
|
||||
font-size: 0.8125rem;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
|
||||
@@ -1,77 +1,78 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import {
|
||||
addCollectionHeader,
|
||||
updateCollectionHeader,
|
||||
deleteCollectionHeader,
|
||||
setCollectionHeaders
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { setCollectionHeaders } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import EditableTable from 'components/EditableTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
import { MimeTypes } from 'utils/codemirror/autocompleteConstants';
|
||||
import BulkEditor from 'components/BulkEditor/index';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = get(collection, 'root.request.headers', []);
|
||||
const headers = collection.draft?.root
|
||||
? get(collection, 'draft.root.request.headers', [])
|
||||
: get(collection, 'root.request.headers', []);
|
||||
const [isBulkEditMode, setIsBulkEditMode] = useState(false);
|
||||
|
||||
const toggleBulkEditMode = () => {
|
||||
setIsBulkEditMode(!isBulkEditMode);
|
||||
};
|
||||
|
||||
const handleBulkHeadersChange = (newHeaders) => {
|
||||
dispatch(setCollectionHeaders({ collectionUid: collection.uid, headers: newHeaders }));
|
||||
};
|
||||
const handleHeadersChange = useCallback((updatedHeaders) => {
|
||||
dispatch(setCollectionHeaders({ collectionUid: collection.uid, headers: updatedHeaders }));
|
||||
}, [dispatch, collection.uid]);
|
||||
|
||||
const addHeader = () => {
|
||||
dispatch(
|
||||
addCollectionHeader({
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleHeaderValueChange = (e, _header, type) => {
|
||||
const header = cloneDeep(_header);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
header.name = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
header.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
header.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
const columns = [
|
||||
{
|
||||
key: 'name',
|
||||
name: 'Name',
|
||||
isKeyField: true,
|
||||
placeholder: 'Name',
|
||||
width: '30%',
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
<SingleLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(newValue) => onChange(newValue.replace(/[\r\n]/g, ''))}
|
||||
autocomplete={headerAutoCompleteList}
|
||||
collection={collection}
|
||||
placeholder={isLastEmptyRow ? 'Name' : ''}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
name: 'Value',
|
||||
placeholder: 'Value',
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
<SingleLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={onChange}
|
||||
collection={collection}
|
||||
autocomplete={MimeTypes}
|
||||
placeholder={isLastEmptyRow ? 'Value' : ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
dispatch(
|
||||
updateCollectionHeader({
|
||||
header: header,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
];
|
||||
|
||||
const handleRemoveHeader = (header) => {
|
||||
dispatch(
|
||||
deleteCollectionHeader({
|
||||
headerUid: header.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
const defaultRow = {
|
||||
name: '',
|
||||
value: '',
|
||||
description: ''
|
||||
};
|
||||
|
||||
if (isBulkEditMode) {
|
||||
@@ -82,7 +83,7 @@ const Headers = ({ collection }) => {
|
||||
</div>
|
||||
<BulkEditor
|
||||
params={headers}
|
||||
onChange={handleBulkHeadersChange}
|
||||
onChange={handleHeadersChange}
|
||||
onToggle={toggleBulkEditMode}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
@@ -95,94 +96,24 @@ const Headers = ({ collection }) => {
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Add request headers that will be sent with every request in this collection.
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{headers && headers.length
|
||||
? headers.map((header) => {
|
||||
return (
|
||||
<tr key={header.uid}>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={header.name}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(newValue) =>
|
||||
handleHeaderValueChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
header,
|
||||
'name'
|
||||
)
|
||||
}
|
||||
autocomplete={headerAutoCompleteList}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={header.value}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(newValue) =>
|
||||
handleHeaderValueChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
header,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
autocomplete={MimeTypes}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={header.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
|
||||
/>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveHeader(header)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex justify-between mt-2">
|
||||
<button className="btn-add-header text-link pr-2 py-3 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
<EditableTable
|
||||
columns={columns}
|
||||
rows={headers}
|
||||
onChange={handleHeadersChange}
|
||||
defaultRow={defaultRow}
|
||||
/>
|
||||
<div className="flex justify-end mt-2">
|
||||
<button className="text-link select-none" onClick={toggleBulkEditMode}>
|
||||
Bulk Edit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Headers;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.icon-box {
|
||||
&.location {
|
||||
background-color: ${(props) => rgba(props.theme.textLink, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.textLink, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
&.environments {
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.green, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.colors.text.green, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
}
|
||||
}
|
||||
|
||||
&.requests {
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.purple, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.colors.text.purple, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.colors.text.purple};
|
||||
}
|
||||
}
|
||||
|
||||
&.share {
|
||||
background-color: ${(props) => rgba(props.theme.textLink, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.textLink, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
}
|
||||
}
|
||||
|
||||
&.generate-docs {
|
||||
background-color: ${(props) => rgba(props.theme.accents.primary, 0.08)};
|
||||
border: 1px solid ${(props) => rgba(props.theme.accents.primary, 0.09)};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.accents.primary};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -1,33 +1,44 @@
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
import { getTotalRequestCountInCollection } from 'utils/collections/';
|
||||
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";
|
||||
import { IconFolder, IconWorld, IconApi, IconShare, IconBook } from '@tabler/icons';
|
||||
import { areItemsLoading, getItemsLoadStats } from 'utils/collections/index';
|
||||
import { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import ShareCollection from 'components/ShareCollection/index';
|
||||
import GenerateDocumentation from 'components/Sidebar/Collections/Collection/GenerateDocumentation';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Info = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
|
||||
|
||||
const isCollectionLoading = areItemsLoading(collection);
|
||||
const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
|
||||
const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
|
||||
|
||||
const [showGenerateDocumentationModal, setShowGenerateDocumentationModal] = useState(false);
|
||||
|
||||
const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);
|
||||
|
||||
const collectionEnvironmentCount = collection.environments?.length || 0;
|
||||
const globalEnvironmentCount = globalEnvironments?.length || 0;
|
||||
|
||||
const handleToggleShowShareCollectionModal = (value) => (e) => {
|
||||
toggleShowShareCollectionModal(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col h-fit">
|
||||
<StyledWrapper className="w-full flex flex-col h-fit">
|
||||
<div className="rounded-lg py-6">
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-5">
|
||||
{/* Location Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<IconFolder className="w-5 h-5 text-blue-500" stroke={1.5} />
|
||||
<div className="icon-box location flex-shrink-0 p-3 rounded-lg">
|
||||
<IconFolder className="w-5 h-5" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-semibold text-sm">Location</div>
|
||||
<div className="mt-1 text-sm text-muted break-all">
|
||||
<div className="font-medium">Location</div>
|
||||
<div className="mt-1 text-muted break-all">
|
||||
{collection.pathname}
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,48 +46,90 @@ const Info = ({ collection }) => {
|
||||
|
||||
{/* Environments Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<IconWorld className="w-5 h-5 text-green-500" stroke={1.5} />
|
||||
<div className="icon-box environments flex-shrink-0 p-3 rounded-lg">
|
||||
<IconWorld className="w-5 h-5" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-semibold text-sm">Environments</div>
|
||||
<div className="mt-1 text-sm text-muted">
|
||||
{collection.environments?.length || 0} environment{collection.environments?.length !== 1 ? 's' : ''} configured
|
||||
<div className="font-medium">Environments</div>
|
||||
<div className="mt-1 flex flex-col gap-1">
|
||||
<button
|
||||
type="button"
|
||||
className="text-link cursor-pointer hover:underline text-left bg-transparent"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{collectionEnvironmentCount} collection environment{collectionEnvironmentCount !== 1 ? 's' : ''}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="text-link cursor-pointer hover:underline text-left bg-transparent"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: `${collection.uid}-global-environment-settings`,
|
||||
collectionUid: collection.uid,
|
||||
type: 'global-environment-settings'
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
{globalEnvironmentCount} global environment{globalEnvironmentCount !== 1 ? 's' : ''}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Requests Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
|
||||
<IconApi className="w-5 h-5 text-purple-500" stroke={1.5} />
|
||||
<div className="icon-box requests flex-shrink-0 p-3 rounded-lg">
|
||||
<IconApi className="w-5 h-5" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-semibold text-sm">Requests</div>
|
||||
<div className="mt-1 text-sm text-muted">
|
||||
<div className="font-medium">Requests</div>
|
||||
<div className="mt-1 text-muted">
|
||||
{
|
||||
isCollectionLoading? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
|
||||
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 className="icon-box share flex-shrink-0 p-3 rounded-lg">
|
||||
<IconShare className="w-5 h-5" 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">
|
||||
<div className="font-medium h-fit my-auto">Share</div>
|
||||
<div className="group-hover:underline text-link">
|
||||
Share Collection
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showShareCollectionModal && <ShareCollection collectionUid={collection.uid} onClose={handleToggleShowShareCollectionModal(false)} />}
|
||||
|
||||
<div className="flex items-start group cursor-pointer" onClick={() => setShowGenerateDocumentationModal(true)}>
|
||||
<div className="icon-box generate-docs flex-shrink-0 p-3 rounded-lg">
|
||||
<IconBook className="w-5 h-5" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4 h-full flex flex-col justify-start">
|
||||
<div className="font-medium h-fit my-auto">Documentation</div>
|
||||
<div className="group-hover:underline text-link">
|
||||
Generate Docs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showGenerateDocumentationModal && <GenerateDocumentation collectionUid={collection.uid} onClose={() => setShowGenerateDocumentationModal(false)} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Info;
|
||||
export default Info;
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
&.card {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.card.bg};
|
||||
|
||||
.title {
|
||||
border-top: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-left: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-right: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-top: 1px solid ${(props) => props.theme.table.border};
|
||||
border-left: 1px solid ${(props) => props.theme.table.border};
|
||||
border-right: 1px solid ${(props) => props.theme.table.border};
|
||||
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
|
||||
background-color: ${(props) => props.theme.status.warning.background};
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
color: ${(props) => props.theme.status.warning.text};
|
||||
}
|
||||
|
||||
.table {
|
||||
thead {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.bg};
|
||||
color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.color};
|
||||
color: ${(props) => props.theme.table.thead.color} !important;
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import React from 'react';
|
||||
import { flattenItems } from "utils/collections";
|
||||
import { flattenItems } from 'utils/collections';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
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);
|
||||
const itemsFailedLoading = flattenedItems?.filter((item) => item?.partial && !item?.loading);
|
||||
|
||||
if (!itemsFailedLoading?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleRequestClick = (item) => e => {
|
||||
const handleRequestClick = (item) => (e) => {
|
||||
e.preventDefault();
|
||||
if (isItemARequest(item)) {
|
||||
dispatch(hideHomePage());
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
focusTab({
|
||||
@@ -39,12 +37,12 @@ const RequestsNotLoaded = ({ collection }) => {
|
||||
);
|
||||
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">
|
||||
<IconAlertTriangle size={16} className="text-yellow-500" />
|
||||
<div className="flex items-center gap-2 px-3 py-2 title">
|
||||
<IconAlertTriangle size={16} className="warning-icon" />
|
||||
<span className="font-medium">Following requests were not loaded</span>
|
||||
</div>
|
||||
<table className="w-full border-collapse">
|
||||
@@ -61,7 +59,7 @@ const RequestsNotLoaded = ({ collection }) => {
|
||||
<tbody>
|
||||
{flattenedItems?.map((item, index) => (
|
||||
item?.partial && !item?.loading ? (
|
||||
<tr key={index} className='cursor-pointer' onClick={handleRequestClick(item)}>
|
||||
<tr key={index} className="cursor-pointer" onClick={handleRequestClick(item)}>
|
||||
<td className="py-1.5 px-3">
|
||||
{item?.pathname?.split(`${collection?.pathname}/`)?.[1]}
|
||||
</td>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import Docs from "../Docs";
|
||||
import Info from "./Info";
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Docs from '../Docs';
|
||||
import Info from './Info';
|
||||
import { IconBox } from '@tabler/icons';
|
||||
import RequestsNotLoaded from "./RequestsNotLoaded";
|
||||
import RequestsNotLoaded from './RequestsNotLoaded';
|
||||
|
||||
const Overview = ({ collection }) => {
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div className="grid grid-cols-5 gap-4 h-full">
|
||||
<div className="grid grid-cols-5 gap-5 h-full">
|
||||
<div className="col-span-2">
|
||||
<div className="text-xl font-semibold flex items-center gap-2">
|
||||
<IconBox size={24} stroke={1.5} />
|
||||
<div className="text-lg font-medium flex items-center gap-2">
|
||||
<IconBox size={20} stroke={1.5} />
|
||||
{collection?.name}
|
||||
</div>
|
||||
<Info collection={collection} />
|
||||
@@ -22,6 +22,6 @@ const Overview = ({ collection }) => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Overview;
|
||||
export default Overview;
|
||||
|
||||
@@ -16,11 +16,11 @@ const StyledWrapper = styled.div`
|
||||
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};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
border: solid 1px ${(props) => props.theme.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,47 @@
|
||||
import React from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
|
||||
import { updateCollectionPresets } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { get } from 'lodash';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const PresetsSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isGrpcEnabled = useBetaFeature(BETA_FEATURES.GRPC);
|
||||
const {
|
||||
brunoConfig: { presets: presets = {} }
|
||||
} = collection;
|
||||
const initialPresets = { requestType: 'http', requestUrl: '' };
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
requestType: presets.requestType === 'grpc' && !isGrpcEnabled ? 'http' : presets.requestType || 'http',
|
||||
requestUrl: presets.requestUrl || ''
|
||||
},
|
||||
onSubmit: (newPresets) => {
|
||||
// If gRPC is disabled but the preset is set to grpc, change it to http
|
||||
if (!isGrpcEnabled && newPresets.requestType === 'grpc') {
|
||||
newPresets.requestType = 'http';
|
||||
}
|
||||
// Get presets from draft.brunoConfig if it exists, otherwise from brunoConfig
|
||||
const currentPresets = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.presets', initialPresets)
|
||||
: get(collection, 'brunoConfig.presets', initialPresets);
|
||||
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.presets = newPresets;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid));
|
||||
toast.success('Collection presets updated');
|
||||
}
|
||||
});
|
||||
// Helper to update presets config
|
||||
const updatePresets = (updates) => {
|
||||
const updatedPresets = { ...currentPresets, ...updates };
|
||||
dispatch(updateCollectionPresets({
|
||||
collectionUid: collection.uid,
|
||||
presets: updatedPresets
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleRequestTypeChange = (e) => {
|
||||
updatePresets({ requestType: e.target.value });
|
||||
};
|
||||
|
||||
const handleRequestUrlChange = (e) => {
|
||||
updatePresets({ requestUrl: e.target.value });
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
These presets will be used as the default values for new requests in this collection.
|
||||
</div>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="bruno-form">
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
<label className="settings-label flex items-center" htmlFor="http">
|
||||
Request Type
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
@@ -49,9 +50,9 @@ const PresetsSettings = ({ collection }) => {
|
||||
className="cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
onChange={handleRequestTypeChange}
|
||||
value="http"
|
||||
checked={formik.values.requestType === 'http'}
|
||||
checked={(currentPresets.requestType || 'http') === 'http'}
|
||||
/>
|
||||
<label htmlFor="http" className="ml-1 cursor-pointer select-none">
|
||||
HTTP
|
||||
@@ -62,34 +63,43 @@ const PresetsSettings = ({ collection }) => {
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
onChange={handleRequestTypeChange}
|
||||
value="graphql"
|
||||
checked={formik.values.requestType === 'graphql'}
|
||||
checked={(currentPresets.requestType || 'http') === 'graphql'}
|
||||
/>
|
||||
<label htmlFor="graphql" className="ml-1 cursor-pointer select-none">
|
||||
GraphQL
|
||||
</label>
|
||||
|
||||
{isGrpcEnabled && (
|
||||
<>
|
||||
<input
|
||||
id="grpc"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={formik.handleChange}
|
||||
value="grpc"
|
||||
checked={formik.values.requestType === 'grpc'}
|
||||
/>
|
||||
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
|
||||
gRPC
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
<input
|
||||
id="grpc"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={handleRequestTypeChange}
|
||||
value="grpc"
|
||||
checked={(currentPresets.requestType || 'http') === 'grpc'}
|
||||
/>
|
||||
<label htmlFor="grpc" className="ml-1 cursor-pointer select-none">
|
||||
gRPC
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="ws"
|
||||
className="ml-4 cursor-pointer"
|
||||
type="radio"
|
||||
name="requestType"
|
||||
onChange={handleRequestTypeChange}
|
||||
value="ws"
|
||||
checked={(currentPresets.requestType || 'http') === 'ws'}
|
||||
/>
|
||||
<label htmlFor="ws" className="ml-1 cursor-pointer select-none">
|
||||
WebSocket
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="requestUrl">
|
||||
<label className="settings-label" htmlFor="request-url">
|
||||
Base URL
|
||||
</label>
|
||||
<div className="flex items-center w-full">
|
||||
@@ -104,8 +114,8 @@ const PresetsSettings = ({ collection }) => {
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.requestUrl || ''}
|
||||
onChange={handleRequestUrlChange}
|
||||
value={currentPresets.requestUrl || ''}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
@@ -113,11 +123,11 @@ const PresetsSettings = ({ collection }) => {
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
<Button type="button" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.available-certificates {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
|
||||
button.remove-certificate {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
|
||||
/* Section labels */
|
||||
label {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
/* Tooltip icon */
|
||||
.tooltip-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Error messages */
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
background-color: ${(props) => props.theme.bg};
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead {
|
||||
th {
|
||||
text-align: left;
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
&.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
&.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* File/Directory icons */
|
||||
.file-icon,
|
||||
.folder-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
/* File/Directory names */
|
||||
.file-name,
|
||||
.directory-name {
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
|
||||
/* Path text */
|
||||
.path-text {
|
||||
font-size: ${(props) => props.theme.font.size.xs};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
.empty-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
}
|
||||
|
||||
/* Invalid file indicator */
|
||||
.invalid-indicator {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.action-button {
|
||||
padding: 0.25rem;
|
||||
border-radius: ${(props) => props.theme.border.radius.base};
|
||||
transition: all 0.2s;
|
||||
|
||||
&.replace-button {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
background-color: ${(props) => props.theme.colors.bg.danger}20;
|
||||
}
|
||||
}
|
||||
|
||||
&.remove-button {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.text};
|
||||
background-color: ${(props) => props.theme.dropdown.hoverBg};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
accent-color: ${(props) => props.theme.colors.accent};
|
||||
border-color: ${(props) => props.theme.table.border};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: ${(props) => props.theme.primary.solid};
|
||||
}
|
||||
}
|
||||
|
||||
/* Add button */
|
||||
.btn-add-param {
|
||||
color: ${(props) => props.theme.textLink};
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.primary.solid};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,348 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import {
|
||||
IconTrash,
|
||||
IconFile,
|
||||
IconFileImport,
|
||||
IconAlertCircle,
|
||||
IconFolder
|
||||
} from '@tabler/icons';
|
||||
import { getBasename } from 'utils/common/path';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import useProtoFileManagement from '../../../hooks/useProtoFileManagement';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ProtobufSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
protoFiles,
|
||||
importPaths,
|
||||
addProtoFileToCollection,
|
||||
addImportPathToCollection,
|
||||
toggleImportPath,
|
||||
browseForProtoFile,
|
||||
browseForImportDirectory,
|
||||
removeProtoFileFromCollection,
|
||||
removeImportPathFromCollection,
|
||||
replaceImportPathInCollection,
|
||||
replaceProtoFileInCollection
|
||||
} = useProtoFileManagement(collection);
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
// Get file path using the ipcRenderer
|
||||
const getProtoFile = async (event) => {
|
||||
const files = event?.files;
|
||||
if (files && files.length > 0) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filePath = window?.ipcRenderer?.getFilePath(files[i]);
|
||||
if (filePath) {
|
||||
await addProtoFileToCollection(filePath);
|
||||
}
|
||||
}
|
||||
// Reset the file input
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveProtoFile = async (index) => {
|
||||
await removeProtoFileFromCollection(index);
|
||||
};
|
||||
|
||||
const handleBrowseClick = () => {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const handleReplaceProtoFile = async (index) => {
|
||||
const result = await browseForProtoFile();
|
||||
if (result.success) {
|
||||
await replaceProtoFileInCollection(index, result.filePath);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReplaceImportPath = async (index) => {
|
||||
const result = await browseForImportDirectory();
|
||||
if (result.success) {
|
||||
await replaceImportPathInCollection(index, result.directoryPath);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileInputChange = (e) => {
|
||||
getProtoFile(e.target);
|
||||
};
|
||||
|
||||
const getImportPath = async () => {
|
||||
const result = await browseForImportDirectory();
|
||||
if (result.success) {
|
||||
await addImportPathToCollection(result.directoryPath);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveImportPath = async (index) => {
|
||||
await removeImportPathFromCollection(index);
|
||||
};
|
||||
|
||||
const handleToggleImportPath = async (index) => {
|
||||
await toggleImportPath(index);
|
||||
};
|
||||
|
||||
const handleBrowseImportPathClick = () => {
|
||||
getImportPath();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
{/* Hidden file input for file selection */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
accept=".proto"
|
||||
multiple
|
||||
onChange={handleFileInputChange}
|
||||
/>
|
||||
|
||||
{/* Proto Files Section */}
|
||||
<div className="mb-6" data-testid="protobuf-proto-files-section">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center" htmlFor="protoFiles">
|
||||
Proto Files (
|
||||
{protoFiles.length}
|
||||
)
|
||||
<span id="proto-files-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="tooltip-icon" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="proto-files-tooltip"
|
||||
className="tooltip-mod font-normal"
|
||||
html="Keep your proto files within the collection folder or the corresponding git repository to ensure paths remain valid when sharing the collection."
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{protoFiles.some((file) => !file.exists) && (
|
||||
<div className="error-message text-xs mb-2 flex items-center p-2" data-testid="protobuf-invalid-files-message">
|
||||
<IconAlertCircle size={14} className="mr-1" />
|
||||
Some proto files cannot be found. Use the replace option to update their locations.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<table className="w-full border-collapse" data-testid="protobuf-proto-files-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
File
|
||||
</th>
|
||||
<th>
|
||||
Path
|
||||
</th>
|
||||
<th className="text-right">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{protoFiles.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="3" className="text-center">
|
||||
<div className="empty-state flex flex-col items-center">
|
||||
<IconFile size={24} className="empty-icon mb-2" />
|
||||
<span className="empty-text">No proto files added</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
protoFiles.map((file, index) => {
|
||||
const isValid = file.exists;
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<IconFile size={16} className="file-icon mr-2" />
|
||||
<span className="file-name" data-testid="protobuf-proto-file-name">
|
||||
{getBasename(collection.pathname, file.path)}
|
||||
</span>
|
||||
{!isValid && <IconAlertCircle size={12} className="invalid-indicator ml-2" />}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="path-text">
|
||||
{file.path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{!isValid && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReplaceProtoFile(index)}
|
||||
className="action-button replace-button"
|
||||
title="Replace file"
|
||||
>
|
||||
<IconFileImport size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveProtoFile(index)}
|
||||
className="action-button remove-button"
|
||||
title="Remove file"
|
||||
data-testid="protobuf-remove-file-button"
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleBrowseClick} data-testid="protobuf-add-file-button">
|
||||
+ Add Proto File
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Import Paths Section */}
|
||||
<div className="mb-6" data-testid="protobuf-import-paths-section">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center" htmlFor="importPaths">
|
||||
Import Paths (
|
||||
{importPaths.length}
|
||||
)
|
||||
<span id="import-paths-tooltip" className="ml-2">
|
||||
<IconAlertCircle size={16} className="tooltip-icon" />
|
||||
</span>
|
||||
<Tooltip
|
||||
anchorId="import-paths-tooltip"
|
||||
className="tooltip-mod font-normal"
|
||||
html="Add directories that contain proto files to be imported. These paths help resolve import statements in your proto files."
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{importPaths.some((path) => !path.exists) && (
|
||||
<div className="error-message text-xs mb-2 flex items-center p-2" data-testid="protobuf-invalid-import-paths-message">
|
||||
<IconAlertCircle size={14} className="mr-1" />
|
||||
Some import paths cannot be found at their specified locations.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<table className="w-full border-collapse" data-testid="protobuf-import-paths-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
</th>
|
||||
<th>
|
||||
Directory
|
||||
</th>
|
||||
<th>
|
||||
Path
|
||||
</th>
|
||||
<th className="text-right">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{importPaths.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan="4" className="text-center">
|
||||
<div className="empty-state flex flex-col items-center">
|
||||
<IconFolder size={24} className="empty-icon mb-2" />
|
||||
<span className="empty-text">No import paths added</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
importPaths.map((importPath, index) => {
|
||||
const isValid = importPath.exists;
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={importPath.enabled}
|
||||
onChange={() => handleToggleImportPath(index)}
|
||||
className="h-4 w-4"
|
||||
title={importPath.enabled ? 'Disable this import path' : 'Enable this import path'}
|
||||
data-testid="protobuf-import-path-checkbox"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<IconFolder size={16} className="folder-icon mr-2" />
|
||||
<span className="directory-name">
|
||||
{getBasename(collection.pathname, importPath.path)}
|
||||
</span>
|
||||
{!isValid && <IconAlertCircle size={12} className="invalid-indicator ml-2" />}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="path-text">
|
||||
{importPath.path}
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{!isValid && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReplaceImportPath(index)}
|
||||
className="action-button replace-button"
|
||||
title="Replace directory"
|
||||
>
|
||||
<IconFileImport size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveImportPath(index)}
|
||||
className="action-button remove-button"
|
||||
title="Remove import path"
|
||||
data-testid="protobuf-remove-import-path-button"
|
||||
>
|
||||
<IconTrash size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleBrowseImportPathClick} data-testid="protobuf-add-import-path-button">
|
||||
+ Add Import Path
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button type="button" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProtobufSettings;
|
||||
@@ -14,11 +14,11 @@ const StyledWrapper = styled.div`
|
||||
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};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
border: solid 1px ${(props) => props.theme.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +1,214 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import React from 'react';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons';
|
||||
import { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateCollectionProxy } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { get } from 'lodash';
|
||||
import toast from 'react-hot-toast';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
const proxySchema = Yup.object({
|
||||
enabled: Yup.string().oneOf(['global', 'true', 'false']),
|
||||
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
|
||||
hostname: Yup.string()
|
||||
.when('enabled', {
|
||||
is: 'true',
|
||||
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
|
||||
otherwise: (hostname) => hostname.nullable()
|
||||
})
|
||||
.max(1024),
|
||||
port: Yup.number()
|
||||
.min(1)
|
||||
.max(65535)
|
||||
.typeError('Specify port between 1 and 65535')
|
||||
.nullable()
|
||||
.transform((_, val) => (val ? Number(val) : null)),
|
||||
auth: Yup.object()
|
||||
.when('enabled', {
|
||||
is: 'true',
|
||||
then: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
username: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (username) => username.required('Specify username for proxy authentication.')
|
||||
})
|
||||
.max(1024),
|
||||
password: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (password) => password.required('Specify password for proxy authentication.')
|
||||
})
|
||||
.max(1024)
|
||||
})
|
||||
})
|
||||
.optional(),
|
||||
bypassProxy: Yup.string().optional().max(1024)
|
||||
});
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
enabled: proxyConfig.enabled || 'global',
|
||||
protocol: proxyConfig.protocol || 'http',
|
||||
hostname: proxyConfig.hostname || '',
|
||||
port: proxyConfig.port || '',
|
||||
const ProxySettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const initialProxyConfig = {
|
||||
inherit: true,
|
||||
config: {
|
||||
protocol: 'http',
|
||||
hostname: '',
|
||||
port: '',
|
||||
auth: {
|
||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
bypassProxy: proxyConfig.bypassProxy || ''
|
||||
},
|
||||
validationSchema: proxySchema,
|
||||
onSubmit: (values) => {
|
||||
proxySchema
|
||||
.validate(values, { abortEarly: true })
|
||||
.then((validatedProxy) => {
|
||||
// serialize 'enabled' to boolean
|
||||
if (validatedProxy.enabled === 'true') {
|
||||
validatedProxy.enabled = true;
|
||||
} else if (validatedProxy.enabled === 'false') {
|
||||
validatedProxy.enabled = false;
|
||||
}
|
||||
|
||||
onUpdate(validatedProxy);
|
||||
})
|
||||
.catch((error) => {
|
||||
let errMsg = error.message || 'Preferences validation error';
|
||||
toast.error(errMsg);
|
||||
});
|
||||
bypassProxy: ''
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Get proxy from draft.brunoConfig if it exists, otherwise from brunoConfig
|
||||
const currentProxyConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.proxy', initialProxyConfig)
|
||||
: get(collection, 'brunoConfig.proxy', initialProxyConfig);
|
||||
|
||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
formik.setValues({
|
||||
enabled: proxyConfig.enabled === true ? 'true' : proxyConfig.enabled === false ? 'false' : 'global',
|
||||
protocol: proxyConfig.protocol || 'http',
|
||||
hostname: proxyConfig.hostname || '',
|
||||
port: proxyConfig.port || '',
|
||||
auth: {
|
||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||
},
|
||||
bypassProxy: proxyConfig.bypassProxy || ''
|
||||
const validateHostnameOnChange = (hostname) => {
|
||||
if (hostname && hostname.length > 1024) {
|
||||
toast.error('Hostname must be less than 1024 characters');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validatePortOnChange = (port) => {
|
||||
if (!port || port === '') {
|
||||
return true; // Allow empty port during typing
|
||||
}
|
||||
const portNum = Number(port);
|
||||
if (isNaN(portNum)) {
|
||||
toast.error('Port must be a valid number');
|
||||
return false;
|
||||
}
|
||||
if (portNum < 1 || portNum > 65535) {
|
||||
toast.error('Port must be between 1 and 65535');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateAuthUsernameOnChange = (username) => {
|
||||
if (username && username.length > 1024) {
|
||||
toast.error('Username must be less than 1024 characters');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateAuthPasswordOnChange = (password) => {
|
||||
if (password && password.length > 1024) {
|
||||
toast.error('Password must be less than 1024 characters');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateBypassProxyOnChange = (bypassProxy) => {
|
||||
if (bypassProxy && bypassProxy.length > 1024) {
|
||||
toast.error('Bypass proxy must be less than 1024 characters');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Helper to update proxy config
|
||||
const updateProxy = (updates) => {
|
||||
const updatedProxy = { ...currentProxyConfig, ...updates };
|
||||
dispatch(updateCollectionProxy({
|
||||
collectionUid: collection.uid,
|
||||
proxy: updatedProxy
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const handleEnabledChange = (e) => {
|
||||
const value = e.target.value;
|
||||
// Map UI values to new format
|
||||
if (value === 'inherit') {
|
||||
updateProxy({ disabled: false, inherit: true });
|
||||
} else if (value === 'true') {
|
||||
updateProxy({ disabled: false, inherit: false });
|
||||
} else {
|
||||
updateProxy({ disabled: true, inherit: false });
|
||||
}
|
||||
};
|
||||
|
||||
const handleProtocolChange = (e) => {
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
protocol: e.target.value
|
||||
}
|
||||
});
|
||||
}, [proxyConfig]);
|
||||
};
|
||||
|
||||
const handleHostnameChange = (e) => {
|
||||
const hostname = e.target.value;
|
||||
if (validateHostnameOnChange(hostname)) {
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
hostname
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePortChange = (e) => {
|
||||
const port = e.target.value ? Number(e.target.value) : '';
|
||||
if (validatePortOnChange(port)) {
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
port
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthEnabledChange = (e) => {
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
auth: {
|
||||
...currentProxyConfig.config.auth,
|
||||
disabled: !e.target.checked
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAuthUsernameChange = (e) => {
|
||||
const username = e.target.value;
|
||||
if (validateAuthUsernameOnChange(username)) {
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
auth: {
|
||||
...currentProxyConfig.config.auth,
|
||||
username
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthPasswordChange = (e) => {
|
||||
const password = e.target.value;
|
||||
if (validateAuthPasswordOnChange(password)) {
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
auth: {
|
||||
...currentProxyConfig.config.auth,
|
||||
password
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleBypassProxyChange = (e) => {
|
||||
const bypassProxy = e.target.value;
|
||||
if (validateBypassProxyOnChange(bypassProxy)) {
|
||||
updateProxy({
|
||||
config: {
|
||||
...currentProxyConfig.config,
|
||||
bypassProxy
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Map new format to UI values
|
||||
const disabled = currentProxyConfig.disabled || false;
|
||||
const inherit = currentProxyConfig.inherit !== undefined ? currentProxyConfig.inherit : true;
|
||||
const enabledValue = disabled ? 'false' : (inherit ? 'inherit' : 'true');
|
||||
|
||||
return (
|
||||
<StyledWrapper className="h-full w-full">
|
||||
<div className="text-xs mb-4 text-muted">Configure proxy settings for this collection.</div>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="bruno-form">
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
Config
|
||||
<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' }}>inherit</span> - inherit from global preferences</li>
|
||||
<li><span style={{ width: '50px', display: 'inline-block' }}>enabled</span> - use collection-specific proxy config</li>
|
||||
<li><span style={{ width: '50px', display: 'inline-block' }}>disabled</span> - disable proxy for this collection</li>
|
||||
</ul>
|
||||
</div>
|
||||
</InfoTip>
|
||||
@@ -119,20 +218,20 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
<input
|
||||
type="radio"
|
||||
name="enabled"
|
||||
value="global"
|
||||
checked={formik.values.enabled === 'global'}
|
||||
onChange={formik.handleChange}
|
||||
value="inherit"
|
||||
checked={enabledValue === 'inherit'}
|
||||
onChange={handleEnabledChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
global
|
||||
inherit
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="enabled"
|
||||
value={'true'}
|
||||
checked={formik.values.enabled === 'true'}
|
||||
onChange={formik.handleChange}
|
||||
value="true"
|
||||
checked={enabledValue === 'true'}
|
||||
onChange={handleEnabledChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
enabled
|
||||
@@ -141,196 +240,185 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
<input
|
||||
type="radio"
|
||||
name="enabled"
|
||||
value={'false'}
|
||||
checked={formik.values.enabled === 'false'}
|
||||
onChange={formik.handleChange}
|
||||
value="false"
|
||||
checked={enabledValue === 'false'}
|
||||
onChange={handleEnabledChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
disabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="protocol">
|
||||
Protocol
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
{enabledValue === 'true' && (
|
||||
<>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="protocol">
|
||||
Protocol
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="http"
|
||||
checked={(currentProxyConfig.config?.protocol || 'http') === 'http'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTP
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="https"
|
||||
checked={(currentProxyConfig.config?.protocol || 'http') === 'https'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTPS
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks4"
|
||||
checked={(currentProxyConfig.config?.protocol || 'http') === 'socks4'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS4
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks5"
|
||||
checked={(currentProxyConfig.config?.protocol || 'http') === 'socks5'}
|
||||
onChange={handleProtocolChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS5
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="hostname">
|
||||
Hostname
|
||||
</label>
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="http"
|
||||
checked={formik.values.protocol === 'http'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTP
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="https"
|
||||
checked={formik.values.protocol === 'https'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
HTTPS
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks4"
|
||||
checked={formik.values.protocol === 'socks4'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS4
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks5"
|
||||
checked={formik.values.protocol === 'socks5'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
SOCKS5
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="hostname">
|
||||
Hostname
|
||||
</label>
|
||||
<input
|
||||
id="hostname"
|
||||
type="text"
|
||||
name="hostname"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.hostname || ''}
|
||||
/>
|
||||
{formik.touched.hostname && formik.errors.hostname ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="port">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
id="port"
|
||||
type="number"
|
||||
name="port"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.port}
|
||||
/>
|
||||
{formik.touched.port && formik.errors.port ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.port}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.enabled">
|
||||
Auth
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="auth.enabled"
|
||||
checked={formik.values.auth.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.username">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="auth.username"
|
||||
type="text"
|
||||
name="auth.username"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.username}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
{formik.touched.auth?.username && formik.errors.auth?.username ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.password">
|
||||
Password
|
||||
</label>
|
||||
<div className="textbox flex flex-row items-center w-[13.2rem] h-[1.70rem] relative">
|
||||
<input
|
||||
id="auth.password"
|
||||
type={passwordVisible ? 'text' : 'password'}
|
||||
name="auth.password"
|
||||
className="outline-none bg-transparent w-[10.5rem]"
|
||||
id="hostname"
|
||||
type="text"
|
||||
name="hostname"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.password}
|
||||
onChange={formik.handleChange}
|
||||
onChange={handleHostnameChange}
|
||||
value={currentProxyConfig.config?.hostname || ''}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm absolute right-0"
|
||||
onClick={() => setPasswordVisible(!passwordVisible)}
|
||||
>
|
||||
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
|
||||
</button>
|
||||
</div>
|
||||
{formik.touched.auth?.password && formik.errors.auth?.password ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.password}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="bypassProxy">
|
||||
Proxy Bypass
|
||||
</label>
|
||||
<input
|
||||
id="bypassProxy"
|
||||
type="text"
|
||||
name="bypassProxy"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.bypassProxy || ''}
|
||||
/>
|
||||
{formik.touched.bypassProxy && formik.errors.bypassProxy ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.bypassProxy}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="port">
|
||||
Port
|
||||
</label>
|
||||
<input
|
||||
id="port"
|
||||
type="number"
|
||||
name="port"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handlePortChange}
|
||||
value={currentProxyConfig.config?.port || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.disabled">
|
||||
Auth
|
||||
</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="auth.disabled"
|
||||
checked={!currentProxyConfig.config?.auth?.disabled}
|
||||
onChange={handleAuthEnabledChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.username">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="auth.username"
|
||||
type="text"
|
||||
name="auth.username"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={currentProxyConfig.config?.auth?.username || ''}
|
||||
onChange={handleAuthUsernameChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.password">
|
||||
Password
|
||||
</label>
|
||||
<div className="textbox flex flex-row items-center w-[13.2rem] h-[1.70rem] relative">
|
||||
<input
|
||||
id="auth.password"
|
||||
type={passwordVisible ? 'text' : 'password'}
|
||||
name="auth.password"
|
||||
className="outline-none bg-transparent w-[10.5rem]"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={currentProxyConfig.config?.auth?.password || ''}
|
||||
onChange={handleAuthPasswordChange}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm absolute right-0"
|
||||
onClick={() => setPasswordVisible(!passwordVisible)}
|
||||
>
|
||||
{passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="bypassProxy">
|
||||
Proxy Bypass
|
||||
</label>
|
||||
<input
|
||||
id="bypassProxy"
|
||||
type="text"
|
||||
name="bypassProxy"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handleBypassProxyChange}
|
||||
value={currentProxyConfig.config?.bypassProxy || ''}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProxySettings;
|
||||
export default ProxySettings;
|
||||
|
||||
@@ -8,7 +8,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,20 +1,38 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateCollectionRequestScript, updateCollectionResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Script = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestScript = get(collection, 'root.request.script.req', '');
|
||||
const responseScript = get(collection, 'root.request.script.res', '');
|
||||
const [activeTab, setActiveTab] = useState('pre-request');
|
||||
const preRequestEditorRef = useRef(null);
|
||||
const postResponseEditorRef = useRef(null);
|
||||
const requestScript = collection.draft?.root ? get(collection, 'draft.root.request.script.req', '') : get(collection, 'root.request.script.req', '');
|
||||
const responseScript = collection.draft?.root ? get(collection, 'draft.root.request.script.res', '') : get(collection, 'root.request.script.res', '');
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
// Refresh CodeMirror when tab becomes visible
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (activeTab === 'pre-request' && preRequestEditorRef.current?.editor) {
|
||||
preRequestEditorRef.current.editor.refresh();
|
||||
} else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) {
|
||||
postResponseEditorRef.current.editor.refresh();
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [activeTab]);
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateCollectionRequestScript({
|
||||
@@ -34,7 +52,7 @@ const Script = ({ collection }) => {
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
dispatch(saveCollectionSettings(collection.uid));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -42,39 +60,48 @@ const Script = ({ collection }) => {
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Write pre and post-request scripts that will run before and after any request in this collection is sent.
|
||||
</div>
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 mt-6">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="pre-request">Pre Request</TabsTrigger>
|
||||
<TabsTrigger value="post-response">Post Response</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="pre-request" className="mt-2">
|
||||
<CodeEditor
|
||||
ref={preRequestEditorRef}
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="post-response" className="mt-2">
|
||||
<CodeEditor
|
||||
ref={postResponseEditorRef}
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
theme={displayedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
showHintsFor={['req', 'res', 'bru']}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-12">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -6,8 +6,8 @@ const StyledWrapper = styled.div`
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
margin-right: ${(props) => props.theme.tabs.marginRight};
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
@@ -19,7 +19,12 @@ const StyledWrapper = styled.div`
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
@@ -39,6 +44,11 @@ const StyledWrapper = styled.div`
|
||||
.muted {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
input[type='radio'] {
|
||||
cursor: pointer;
|
||||
accent-color: ${(props) => props.theme.primary.solid};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -3,13 +3,14 @@ import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateCollectionTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Tests = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tests = get(collection, 'root.request.tests', '');
|
||||
const tests = collection.draft?.root ? get(collection, 'draft.root.request.tests', '') : get(collection, 'root.request.tests', '');
|
||||
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
@@ -23,7 +24,7 @@ const Tests = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
@@ -41,9 +42,9 @@ const Tests = ({ collection }) => {
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
color: ${(props) => props.theme.colors.text.subtext0};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
@@ -14,7 +14,7 @@ const Wrapper = styled.div`
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
@@ -31,7 +31,7 @@ const Wrapper = styled.div`
|
||||
}
|
||||
|
||||
.btn-add-var {
|
||||
font-size: 0.8125rem;
|
||||
font-size: ${(props) => props.theme.font.size.base};
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
|
||||
@@ -1,161 +1,81 @@
|
||||
import React from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import MultiLineEditor from 'components/MultiLineEditor';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import EditableTable from 'components/EditableTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
import {
|
||||
addCollectionVar,
|
||||
deleteCollectionVar,
|
||||
updateCollectionVar
|
||||
} from 'providers/ReduxStore/slices/collections/index';
|
||||
import { setCollectionVars } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const VarsTable = ({ collection, vars, varType }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const addVar = () => {
|
||||
dispatch(
|
||||
addCollectionVar({
|
||||
collectionUid: collection.uid,
|
||||
type: varType
|
||||
})
|
||||
);
|
||||
};
|
||||
const onSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
const onSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleVarChange = (e, v, type) => {
|
||||
const _var = cloneDeep(v);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
const value = e.target.value;
|
||||
const handleVarsChange = useCallback((updatedVars) => {
|
||||
dispatch(setCollectionVars({ collectionUid: collection.uid, vars: updatedVars, type: varType }));
|
||||
}, [dispatch, collection.uid, varType]);
|
||||
|
||||
if (variableNameRegex.test(value) === false) {
|
||||
toast.error(
|
||||
'Variable contains invalid characters! Variables must only contain alpha-numeric characters, "-", "_", "."'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_var.name = value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
_var.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
_var.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
const getRowError = useCallback((row, index, key) => {
|
||||
if (key !== 'name') return null;
|
||||
if (!row.name || row.name.trim() === '') return null;
|
||||
if (!variableNameRegex.test(row.name)) {
|
||||
return 'Variable contains invalid characters. Must only contain alphanumeric characters, "-", "_", "."';
|
||||
}
|
||||
dispatch(
|
||||
updateCollectionVar({
|
||||
type: varType,
|
||||
var: _var,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const handleRemoveVar = (_var) => {
|
||||
dispatch(
|
||||
deleteCollectionVar({
|
||||
type: varType,
|
||||
varUid: _var.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
const columns = [
|
||||
{
|
||||
key: 'name',
|
||||
name: 'Name',
|
||||
isKeyField: true,
|
||||
placeholder: 'Name',
|
||||
width: '40%'
|
||||
},
|
||||
{
|
||||
key: 'value',
|
||||
name: varType === 'request' ? 'Value' : (
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<InfoTip content="You can write any valid JS Template Literal here" infotipId={`collection-${varType}-var`} />
|
||||
</div>
|
||||
),
|
||||
placeholder: varType === 'request' ? 'Value' : 'Expr',
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
<MultiLineEditor
|
||||
value={value || ''}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={onChange}
|
||||
collection={collection}
|
||||
placeholder={isLastEmptyRow ? (varType === 'request' ? 'Value' : 'Expr') : ''}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const defaultRow = {
|
||||
name: '',
|
||||
value: '',
|
||||
...(varType === 'response' ? { local: false } : {})
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
{varType === 'request' ? (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Value</span>
|
||||
</div>
|
||||
</td>
|
||||
) : (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<InfoTip content="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{vars && vars.length
|
||||
? vars.map((_var) => {
|
||||
return (
|
||||
<tr key={_var.uid}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={_var.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={_var.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleVarChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
_var,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={_var.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'enabled')}
|
||||
/>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveVar(_var)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={addVar}>
|
||||
+ Add
|
||||
</button>
|
||||
<EditableTable
|
||||
columns={columns}
|
||||
rows={vars}
|
||||
onChange={handleVarsChange}
|
||||
defaultRow={defaultRow}
|
||||
getRowError={getRowError}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default VarsTable;
|
||||
|
||||
@@ -2,28 +2,30 @@ import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Button from 'ui/Button';
|
||||
|
||||
const Vars = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = get(collection, 'root.request.vars.res', []);
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []);
|
||||
const handleSave = () => dispatch(saveCollectionSettings(collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<div className="flex-1">
|
||||
<div className="mb-3 title text-xs">Pre Request</div>
|
||||
<VarsTable collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<div className="mt-3 mb-3 title text-xs">Post Response</div>
|
||||
<VarsTable collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
<Button type="submit" size="sm" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import toast from 'react-hot-toast';
|
||||
import { updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import ProxySettings from './ProxySettings';
|
||||
@@ -13,16 +10,14 @@ import Auth from './Auth';
|
||||
import Script from './Script';
|
||||
import Test from './Tests';
|
||||
import Presets from './Presets';
|
||||
import Grpc from './Grpc';
|
||||
import Protobuf from './Protobuf';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars/index';
|
||||
import StatusDot from 'components/StatusDot';
|
||||
import Overview from './Overview/index';
|
||||
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
|
||||
|
||||
const CollectionSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const isGrpcEnabled = useBetaFeature(BETA_FEATURES.GRPC);
|
||||
const tab = collection.settingsSelectedTab;
|
||||
const setTab = (tab) => {
|
||||
dispatch(
|
||||
@@ -33,61 +28,39 @@ const CollectionSettings = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const root = collection?.root;
|
||||
const root = collection?.draft?.root || collection?.root;
|
||||
const hasScripts = root?.request?.script?.res || root?.request?.script?.req;
|
||||
const hasTests = root?.request?.tests;
|
||||
const hasDocs = root?.docs;
|
||||
|
||||
const headers = get(collection, 'root.request.headers', []);
|
||||
const headers = collection.draft?.root
|
||||
? get(collection, 'draft.root.request.headers', [])
|
||||
: get(collection, 'root.request.headers', []);
|
||||
const activeHeadersCount = headers.filter((header) => header.enabled).length;
|
||||
|
||||
const requestVars = get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = get(collection, 'root.request.vars.res', []);
|
||||
const requestVars = collection.draft?.root
|
||||
? get(collection, 'draft.root.request.vars.req', [])
|
||||
: get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = collection.draft?.root
|
||||
? get(collection, 'draft.root.request.vars.res', [])
|
||||
: get(collection, 'root.request.vars.res', []);
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
|
||||
const authMode
|
||||
= (collection.draft?.root ? get(collection, 'draft.root.request.auth', {}) : get(collection, 'root.request.auth', {}))
|
||||
.mode || 'none';
|
||||
|
||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const grpcConfig = get(collection, 'brunoConfig.grpc', {});
|
||||
|
||||
const onProxySettingsUpdate = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.proxy = config;
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection settings updated successfully.');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
|
||||
const onClientCertSettingsUpdate = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
if (!brunoConfig.clientCertificates) {
|
||||
brunoConfig.clientCertificates = {
|
||||
enabled: true,
|
||||
certs: [config]
|
||||
};
|
||||
} else {
|
||||
brunoConfig.clientCertificates.certs.push(config);
|
||||
}
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection settings updated successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
|
||||
const onClientCertSettingsRemove = (config) => {
|
||||
const brunoConfig = cloneDeep(collection.brunoConfig);
|
||||
brunoConfig.clientCertificates.certs = brunoConfig.clientCertificates.certs.filter(
|
||||
(item) => item.domain != config.domain
|
||||
);
|
||||
dispatch(updateBrunoConfig(brunoConfig, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Collection settings updated successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
const proxyConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.proxy', {})
|
||||
: get(collection, 'brunoConfig.proxy', {});
|
||||
const proxyEnabled = proxyConfig.hostname ? true : false;
|
||||
const clientCertConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.clientCertificates.certs', [])
|
||||
: get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
const protobufConfig = collection.draft?.brunoConfig
|
||||
? get(collection, 'draft.brunoConfig.protobuf', {})
|
||||
: get(collection, 'brunoConfig.protobuf', {});
|
||||
const presets = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.presets', {}) : get(collection, 'brunoConfig.presets', {});
|
||||
const hasPresets = presets && presets.requestUrl !== '';
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
@@ -113,20 +86,13 @@ const CollectionSettings = ({ collection }) => {
|
||||
return <Presets collection={collection} />;
|
||||
}
|
||||
case 'proxy': {
|
||||
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
|
||||
return <ProxySettings collection={collection} />;
|
||||
}
|
||||
case 'clientCert': {
|
||||
return (
|
||||
<ClientCertSettings
|
||||
root={collection.pathname}
|
||||
clientCertConfig={clientCertConfig}
|
||||
onUpdate={onClientCertSettingsUpdate}
|
||||
onRemove={onClientCertSettingsRemove}
|
||||
/>
|
||||
);
|
||||
return <ClientCertSettings collection={collection} />;
|
||||
}
|
||||
case 'grpc': {
|
||||
return <Grpc collection={collection} />;
|
||||
case 'protobuf': {
|
||||
return <Protobuf collection={collection} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -165,21 +131,20 @@ const CollectionSettings = ({ collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('presets')} role="tab" onClick={() => setTab('presets')}>
|
||||
Presets
|
||||
{hasPresets && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||
Proxy
|
||||
{Object.keys(proxyConfig).length > 0 && <StatusDot />}
|
||||
{Object.keys(proxyConfig).length > 0 && proxyEnabled && <StatusDot />}
|
||||
</div>
|
||||
<div className={getTabClassname('clientCert')} role="tab" onClick={() => setTab('clientCert')}>
|
||||
Client Certificates
|
||||
{clientCertConfig.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
{isGrpcEnabled && (
|
||||
<div className={getTabClassname('grpc')} role="tab" onClick={() => setTab('grpc')}>
|
||||
gRPC
|
||||
{grpcConfig.protoFiles && grpcConfig.protoFiles.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
)}
|
||||
<div className={getTabClassname('protobuf')} role="tab" onClick={() => setTab('protobuf')}>
|
||||
Protobuf
|
||||
{protobufConfig.protoFiles && protobufConfig.protoFiles.length > 0 && <StatusDot />}
|
||||
</div>
|
||||
</div>
|
||||
<section className="mt-4 h-full overflow-auto">{getTabPanel(tab)}</section>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div``;
|
||||
const StyledWrapper = styled.div`
|
||||
/* Info icon */
|
||||
.info-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
/* Required field asterisk */
|
||||
.required-asterisk {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
/* Error messages */
|
||||
.error-message {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
/* Checkbox */
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
accent-color: ${(props) => props.theme.primary.solid};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user