mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 01:18:32 +00:00
Compare commits
571 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8f88d8a13 | ||
|
|
c1f5d474b1 | ||
|
|
c04b83bcaa | ||
|
|
f8b5a48076 | ||
|
|
6fd84fcec3 | ||
|
|
6687a20835 | ||
|
|
68a508e26a | ||
|
|
05bf40fa82 | ||
|
|
49f29fa2af | ||
|
|
89e0d3ae7e | ||
|
|
2d4f337f03 | ||
|
|
fb4ad848d6 | ||
|
|
31ecc281a8 | ||
|
|
745cb85f95 | ||
|
|
aac4a33c92 | ||
|
|
32d9c4833e | ||
|
|
65f688dbc2 | ||
|
|
3f6f4ccd12 | ||
|
|
fa8ec4267f | ||
|
|
99b25fc5b2 | ||
|
|
a0fe80604e | ||
|
|
7ac39bcf5c | ||
|
|
5df9981e20 | ||
|
|
03184fa1e8 | ||
|
|
e4b2b651f5 | ||
|
|
fd6b083ae5 | ||
|
|
91d9e81da4 | ||
|
|
1e435b8e10 | ||
|
|
834014b5d1 | ||
|
|
85b22b53aa | ||
|
|
24f92d2b4e | ||
|
|
5f5ad288dc | ||
|
|
e274aedb9a | ||
|
|
7fa68731d2 | ||
|
|
c6440ab849 | ||
|
|
d52163ceb3 | ||
|
|
827df18c62 | ||
|
|
3c9e913349 | ||
|
|
7f7b3f479a | ||
|
|
3a5a213242 | ||
|
|
1f0bde4a12 | ||
|
|
2e38300a67 | ||
|
|
b731aa28be | ||
|
|
ddfd3bdf15 | ||
|
|
32b696da57 | ||
|
|
36f19ec7bc | ||
|
|
53e85f4568 | ||
|
|
4607814c09 | ||
|
|
c40f8e654c | ||
|
|
7fbd8f0a8a | ||
|
|
c5a47ba7ec | ||
|
|
13a7e4538e | ||
|
|
36443edca3 | ||
|
|
5e94ea0e83 | ||
|
|
fbeaa96419 | ||
|
|
8e5234d76a | ||
|
|
f4b9b53922 | ||
|
|
3f1b4dc628 | ||
|
|
7cd46c3a6c | ||
|
|
3300e67900 | ||
|
|
abd7edf1db | ||
|
|
7b1223d2c5 | ||
|
|
a3e6ecddbb | ||
|
|
276c9ce1b0 | ||
|
|
3687594cc4 | ||
|
|
5274d77660 | ||
|
|
ba3db06ac6 | ||
|
|
2f9762da0e | ||
|
|
766d103e20 | ||
|
|
8f398bacb2 | ||
|
|
e0afbdbb74 | ||
|
|
d90cb91a19 | ||
|
|
1026addc8f | ||
|
|
ecf849b207 | ||
|
|
98a87dcc00 | ||
|
|
973f8e036d | ||
|
|
da9afcabe8 | ||
|
|
177f23e19f | ||
|
|
97b634f608 | ||
|
|
c572ba574f | ||
|
|
84c8676804 | ||
|
|
586e26fa2e | ||
|
|
83779dd2e1 | ||
|
|
e77a9adebd | ||
|
|
e8156c5611 | ||
|
|
471e1780a0 | ||
|
|
89c5dae881 | ||
|
|
6c52263c3c | ||
|
|
76787b4080 | ||
|
|
34ea7f0673 | ||
|
|
73062472b7 | ||
|
|
eb043fb465 | ||
|
|
a46e8851c6 | ||
|
|
4d23b4b372 | ||
|
|
45126c99ab | ||
|
|
5ba3903c31 | ||
|
|
07dd409acd | ||
|
|
ddc2740b31 | ||
|
|
d06d28b7fc | ||
|
|
b35596bb59 | ||
|
|
a35b04ad48 | ||
|
|
9d9e4b5065 | ||
|
|
6b07cc6e8f | ||
|
|
296e067222 | ||
|
|
49ea7f33e6 | ||
|
|
7267ba6485 | ||
|
|
59c5c24752 | ||
|
|
64b46caacd | ||
|
|
213f21bd01 | ||
|
|
861604075a | ||
|
|
d2de19f31c | ||
|
|
ffa4708ea8 | ||
|
|
6b85323d1f | ||
|
|
f22e975d90 | ||
|
|
1d58bdab59 | ||
|
|
564825868e | ||
|
|
e8199f102a | ||
|
|
82c6e83d56 | ||
|
|
6bfdd58b59 | ||
|
|
9fd937bfbc | ||
|
|
bbb904437f | ||
|
|
fe59cd3223 | ||
|
|
a6ddf12ee1 | ||
|
|
7e8073cbdb | ||
|
|
846ac66ca3 | ||
|
|
c2bdddaa59 | ||
|
|
55c52971d6 | ||
|
|
3ed7dd3893 | ||
|
|
16cf654e57 | ||
|
|
89fbb42512 | ||
|
|
b00d35f007 | ||
|
|
ddddab8112 | ||
|
|
f8f38802a9 | ||
|
|
fc25ea597c | ||
|
|
72df4419ff | ||
|
|
e9a8763ca0 | ||
|
|
a1d54eacf6 | ||
|
|
f75121d426 | ||
|
|
16137a8189 | ||
|
|
922c1ff253 | ||
|
|
d3e625ef84 | ||
|
|
623ac0bac6 | ||
|
|
7dc09faa9b | ||
|
|
21c49093a5 | ||
|
|
f2c32a8812 | ||
|
|
232027ff4e | ||
|
|
f08871d491 | ||
|
|
31c4d7ecfd | ||
|
|
0dcd50f546 | ||
|
|
ece5ad3175 | ||
|
|
1f6366f262 | ||
|
|
3f7ace8bdf | ||
|
|
0ca035f492 | ||
|
|
79be625fb5 | ||
|
|
2d29eaa31a | ||
|
|
2c9b1b801a | ||
|
|
82abda85fe | ||
|
|
2ce5a6eacb | ||
|
|
7484a8a72b | ||
|
|
5f77ee342b | ||
|
|
e715a47eb6 | ||
|
|
ae36bdfe7b | ||
|
|
1aa91cd2be | ||
|
|
95419413b5 | ||
|
|
b6da0b2113 | ||
|
|
d5a72b1a5d | ||
|
|
ca8b2a58d4 | ||
|
|
9bd7723c48 | ||
|
|
d809a58deb | ||
|
|
edbe9ee4f0 | ||
|
|
924f415176 | ||
|
|
c3decadf30 | ||
|
|
b150694a5c | ||
|
|
b3ee0af226 | ||
|
|
187e482feb | ||
|
|
d034d599c5 | ||
|
|
d767a144f2 | ||
|
|
ad8e9c2561 | ||
|
|
0cf4f09608 | ||
|
|
b05d8d7da0 | ||
|
|
5f94fa1174 | ||
|
|
b31d7044a0 | ||
|
|
2acf8cae86 | ||
|
|
4ab4f09987 | ||
|
|
7e654a81bf | ||
|
|
8c101a8684 | ||
|
|
95190fa424 | ||
|
|
544edfa7d7 | ||
|
|
ba9766fbf0 | ||
|
|
0cd4a4ed94 | ||
|
|
67218f5bb4 | ||
|
|
b28f7625e4 | ||
|
|
5b00f7a8b3 | ||
|
|
fe1e260e92 | ||
|
|
ee3ef0b68c | ||
|
|
63e0df1640 | ||
|
|
1b9ad34f3f | ||
|
|
19463cd0cf | ||
|
|
3fa87fe99a | ||
|
|
658a47e03e | ||
|
|
6e7fc2a9aa | ||
|
|
1244716b9b | ||
|
|
ddd479ed45 | ||
|
|
a9459bc236 | ||
|
|
cdfa839cf3 | ||
|
|
4c8ebe7765 | ||
|
|
15f3276663 | ||
|
|
1f8c4431e0 | ||
|
|
6c92ad22ef | ||
|
|
4dd17247d1 | ||
|
|
58fbe2e64b | ||
|
|
333564f687 | ||
|
|
faa7cdb1d5 | ||
|
|
3302e2e910 | ||
|
|
0e1869139b | ||
|
|
353be75d9c | ||
|
|
43c5b4cf53 | ||
|
|
6dd6879e8f | ||
|
|
955fcb795d | ||
|
|
ad3f1b2331 | ||
|
|
f2bdf2eaf6 | ||
|
|
470e9d0442 | ||
|
|
4230bf8e41 | ||
|
|
7bf049a1f0 | ||
|
|
074cbf0c03 | ||
|
|
211f941b9f | ||
|
|
363cbe75d2 | ||
|
|
ead1225bc4 | ||
|
|
61b705112e | ||
|
|
0f1fb72e21 | ||
|
|
7808a6db05 | ||
|
|
b07ccbdfab | ||
|
|
a2b6bc5970 | ||
|
|
3cc2e3d4fa | ||
|
|
a0be0e10ac | ||
|
|
78eec9ea5c | ||
|
|
97a300fbfc | ||
|
|
e898ebdfa9 | ||
|
|
d6628d960e | ||
|
|
e0266f0a69 | ||
|
|
ec89acde48 | ||
|
|
102f7a5ecb | ||
|
|
19498363e1 | ||
|
|
be857d7812 | ||
|
|
e95d9fe611 | ||
|
|
e7f130f8a1 | ||
|
|
d71e69ac3c | ||
|
|
442e68283d | ||
|
|
e218b8276d | ||
|
|
36ee1f542a | ||
|
|
97871c25cf | ||
|
|
35db296c1f | ||
|
|
e90fc466b4 | ||
|
|
b11113b529 | ||
|
|
79072785e2 | ||
|
|
ce59170752 | ||
|
|
60d3d07459 | ||
|
|
4eb74c8ed3 | ||
|
|
9e4ff465af | ||
|
|
7b55d52c11 | ||
|
|
6b668aaebe | ||
|
|
ad02ab8f36 | ||
|
|
5d1be2f18a | ||
|
|
6c3dc7bbd2 | ||
|
|
30adea3146 | ||
|
|
eed952d43f | ||
|
|
09f67abad6 | ||
|
|
d595032f46 | ||
|
|
671fcecb38 | ||
|
|
ca615e8662 | ||
|
|
d3ab2f28c6 | ||
|
|
a3cdc871d2 | ||
|
|
774ce327a9 | ||
|
|
625a19e86c | ||
|
|
1087cacdb0 | ||
|
|
faccca3921 | ||
|
|
3810152c6b | ||
|
|
816496246b | ||
|
|
fcbecfb480 | ||
|
|
dc425b067b | ||
|
|
03011a41a8 | ||
|
|
46e59eac4b | ||
|
|
cf96527ffb | ||
|
|
e4df7e3111 | ||
|
|
3ca32d4199 | ||
|
|
1298df384d | ||
|
|
fbfdfdca04 | ||
|
|
36bf2c8648 | ||
|
|
ba8bfc6d17 | ||
|
|
f781bd7d8a | ||
|
|
6e246410d5 | ||
|
|
d4e07d2028 | ||
|
|
542735e220 | ||
|
|
062adf019c | ||
|
|
7a864e0054 | ||
|
|
72521a6007 | ||
|
|
0466cd6233 | ||
|
|
8a0f7c6b8f | ||
|
|
d145275172 | ||
|
|
6cbdbad09d | ||
|
|
6d7f397d7b | ||
|
|
5ea9799c49 | ||
|
|
608d606f64 | ||
|
|
d922376d57 | ||
|
|
33cd30315a | ||
|
|
01c298c58e | ||
|
|
250227a134 | ||
|
|
21c9fda04f | ||
|
|
36caa5c2d3 | ||
|
|
9beb422674 | ||
|
|
93f89698fd | ||
|
|
cf8eb7e4ac | ||
|
|
8d4e5bc732 | ||
|
|
de223acb19 | ||
|
|
037793c113 | ||
|
|
5682c47d00 | ||
|
|
32629cdcca | ||
|
|
1979be6c4b | ||
|
|
84ed2aff72 | ||
|
|
f602306069 | ||
|
|
9e8488d417 | ||
|
|
4b297f32e0 | ||
|
|
9c3c28be42 | ||
|
|
bd5bc4acb2 | ||
|
|
e20dc985ef | ||
|
|
79379b23e2 | ||
|
|
41abdbb7fa | ||
|
|
d637334fd7 | ||
|
|
9ca22cd1d1 | ||
|
|
ff3c666f48 | ||
|
|
0a2835a543 | ||
|
|
f9f3b3ebc0 | ||
|
|
dfefb0a314 | ||
|
|
12f8ae37a6 | ||
|
|
d5c24d417d | ||
|
|
d64f4d3740 | ||
|
|
571b4459ac | ||
|
|
d4683ab961 | ||
|
|
91b1814c55 | ||
|
|
ff3321d643 | ||
|
|
659a71491d | ||
|
|
3d366477d0 | ||
|
|
beaa20c134 | ||
|
|
99fe4432c4 | ||
|
|
71e8ea457c | ||
|
|
53e49ffdd3 | ||
|
|
9ee398dc3b | ||
|
|
c4a21e1089 | ||
|
|
2b25a0be18 | ||
|
|
524c3f8445 | ||
|
|
c67cf6cca2 | ||
|
|
33da3a1303 | ||
|
|
c586deeb30 | ||
|
|
051dac02cd | ||
|
|
0668331822 | ||
|
|
1affe0056b | ||
|
|
0ad2186c43 | ||
|
|
5db50339e0 | ||
|
|
2df7f63ba9 | ||
|
|
0e1c4008e7 | ||
|
|
fdca86ffd2 | ||
|
|
b9291201d9 | ||
|
|
239caa1763 | ||
|
|
362289b7cd | ||
|
|
f9f1ca0640 | ||
|
|
53eb53e062 | ||
|
|
2d149d94ef | ||
|
|
f0c3b8a877 | ||
|
|
3a222458d1 | ||
|
|
46bc097733 | ||
|
|
1ce8d707f1 | ||
|
|
b854e66a24 | ||
|
|
1a366893ec | ||
|
|
bf90ee96e5 | ||
|
|
ce2e997c06 | ||
|
|
7617ae12cb | ||
|
|
905e993a13 | ||
|
|
be6abf421f | ||
|
|
b47b0565e5 | ||
|
|
094bf7bc60 | ||
|
|
159dd90b03 | ||
|
|
c222bf47c3 | ||
|
|
c91bc0d1c6 | ||
|
|
64ac763a13 | ||
|
|
bca3d5749b | ||
|
|
2ee6759282 | ||
|
|
c479c2e786 | ||
|
|
6d8bdfa276 | ||
|
|
024f61a95e | ||
|
|
856236c918 | ||
|
|
0179c58cd2 | ||
|
|
515381b930 | ||
|
|
5ef0de2e4d | ||
|
|
c6fef2f1be | ||
|
|
21536f9a27 | ||
|
|
faf8581ddf | ||
|
|
47992288c4 | ||
|
|
640692abc7 | ||
|
|
ed95b8349e | ||
|
|
81b1982d4f | ||
|
|
e381d0c00b | ||
|
|
50f2f54335 | ||
|
|
fab350a32d | ||
|
|
4600217606 | ||
|
|
6421148dc1 | ||
|
|
12f3f802a7 | ||
|
|
ae78ed36ef | ||
|
|
1e27427d09 | ||
|
|
f2a8bd5d10 | ||
|
|
860a3b16ad | ||
|
|
8fadaf98ae | ||
|
|
8f1f41374c | ||
|
|
e3679c9ee9 | ||
|
|
7f17999486 | ||
|
|
d2b35beb6f | ||
|
|
0f3a8a87bb | ||
|
|
3c0770b792 | ||
|
|
5a89d12716 | ||
|
|
8730c5a85b | ||
|
|
24fcb8caf2 | ||
|
|
5a0e68073f | ||
|
|
945f1eb74a | ||
|
|
de74edb50f | ||
|
|
80414d751d | ||
|
|
295d82dca9 | ||
|
|
9474918853 | ||
|
|
6db36513c5 | ||
|
|
7be38bcfe0 | ||
|
|
289625c851 | ||
|
|
1f5aa25d5e | ||
|
|
e2e3895a58 | ||
|
|
65e448b1eb | ||
|
|
d5a6522563 | ||
|
|
be72d24ecb | ||
|
|
c25542bbdf | ||
|
|
a838185ddf | ||
|
|
ac3637fcfa | ||
|
|
e3f32dfc87 | ||
|
|
a204e3814b | ||
|
|
be8db1876a | ||
|
|
64d9413777 | ||
|
|
a71afc8f73 | ||
|
|
4e4130acf5 | ||
|
|
bb7d13d2d9 | ||
|
|
c08be14d87 | ||
|
|
90f47e5877 | ||
|
|
8a19189dcd | ||
|
|
efbc119e7c | ||
|
|
d36956e0a4 | ||
|
|
efbdde8252 | ||
|
|
ae2f170fe6 | ||
|
|
4f0d87fba4 | ||
|
|
8703124007 | ||
|
|
f9f99adbf9 | ||
|
|
da9a6c434a | ||
|
|
c8764f6555 | ||
|
|
e90718f47b | ||
|
|
af130bac17 | ||
|
|
04481a26e1 | ||
|
|
86c2a60184 | ||
|
|
0cbf803ed9 | ||
|
|
c91819c072 | ||
|
|
2c4a3a5eb4 | ||
|
|
99fc980a48 | ||
|
|
960f62ac8e | ||
|
|
57e0f0c0c4 | ||
|
|
8216bf5eec | ||
|
|
9f11cfc836 | ||
|
|
51cb930b6a | ||
|
|
1c8712b6eb | ||
|
|
6c1f8c78b2 | ||
|
|
b35b814561 | ||
|
|
80eaaad658 | ||
|
|
51a73d864e | ||
|
|
87e29db545 | ||
|
|
0cf18e6804 | ||
|
|
3a6dacc1f4 | ||
|
|
8d89496304 | ||
|
|
6ef7891f8a | ||
|
|
e1bf475f1f | ||
|
|
f7eb325e11 | ||
|
|
c50b88d60d | ||
|
|
90fedc8ec6 | ||
|
|
9c46bc79d9 | ||
|
|
9ecfb3a640 | ||
|
|
da4e96d1ef | ||
|
|
bf2d1220a1 | ||
|
|
744abe585c | ||
|
|
5e2640fe73 | ||
|
|
7513314179 | ||
|
|
d4b663bfa8 | ||
|
|
9b2e2ed3d8 | ||
|
|
e4ea6b9109 | ||
|
|
1a8feb8029 | ||
|
|
72a5f05009 | ||
|
|
d5ef240de6 | ||
|
|
1932d98b57 | ||
|
|
bdb7ff9947 | ||
|
|
3f4f7fb24c | ||
|
|
494b484cd9 | ||
|
|
48e4e60696 | ||
|
|
2e600838b2 | ||
|
|
52428c6b35 | ||
|
|
2734f26c78 | ||
|
|
73c62010b7 | ||
|
|
26853787da | ||
|
|
712319ff34 | ||
|
|
7a80247fc5 | ||
|
|
ea9e294c54 | ||
|
|
55315b5b88 | ||
|
|
d63e7371fe | ||
|
|
dce11d1bd5 | ||
|
|
e720fed63b | ||
|
|
43f7c2ab86 | ||
|
|
95532102ba | ||
|
|
4603ec4d5e | ||
|
|
cbdd56e577 | ||
|
|
ce77d08408 | ||
|
|
3a5071412e | ||
|
|
336ad38cb9 | ||
|
|
636e14a385 | ||
|
|
bfc03f5ae4 | ||
|
|
97200961eb | ||
|
|
7297bb184e | ||
|
|
b74922c8f3 | ||
|
|
ce0827308f | ||
|
|
3d0c9cc0ae | ||
|
|
1804454ff0 | ||
|
|
3c710120b9 | ||
|
|
fcc12fb089 | ||
|
|
3d8dee944f | ||
|
|
78e5cd3c03 | ||
|
|
26d99c7aee | ||
|
|
77a7318dfb | ||
|
|
b83da46f12 | ||
|
|
cf6ec4e84f | ||
|
|
978d810473 | ||
|
|
cedcd2cf35 | ||
|
|
ce9cdc5293 | ||
|
|
e83c2da798 | ||
|
|
39f148267e | ||
|
|
f4f093d4db | ||
|
|
3c4ef2f2df | ||
|
|
e39975cb3c | ||
|
|
b767ccd063 | ||
|
|
c5adfd8975 | ||
|
|
6695d90609 | ||
|
|
5c79282a1b | ||
|
|
64019f8ecf | ||
|
|
4c89f31934 | ||
|
|
1c53ce91f0 | ||
|
|
0b83fbb7ce | ||
|
|
08ceed86a8 | ||
|
|
f99918d725 | ||
|
|
1877dd858e | ||
|
|
acff0c379e | ||
|
|
a849e4fb7b | ||
|
|
613699fb69 | ||
|
|
0c4ba71922 | ||
|
|
d346bb00eb | ||
|
|
8bb57aa41d | ||
|
|
f378f04fc3 | ||
|
|
742d69b03c | ||
|
|
0e3bc62d9d | ||
|
|
a02d2b9c58 | ||
|
|
21edfbc25a | ||
|
|
4ba4d8fc27 | ||
|
|
5f32924ddb | ||
|
|
9bcf56d689 | ||
|
|
0f1e330dc5 | ||
|
|
51ee37cf96 |
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Description
|
||||
|
||||
<!-- Explain here the changes your PR introduces and text to help us understand the context of this change. -->
|
||||
|
||||
### Contribution Checklist:
|
||||
|
||||
- [ ] **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.**
|
||||
- [ ] **I have read the [contribution guidelines](https://github.com/usebruno/bruno/blob/main/contributing.md).**
|
||||
- [ ] **Create an issue and link to the pull request.**
|
||||
|
||||
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
|
||||
|
||||
Please see [here](../publishing.md) for more information.
|
||||
50
.github/workflows/release-snap.yml
vendored
Normal file
50
.github/workflows/release-snap.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: Publish to Snapcraft
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build:
|
||||
description: 'Build and publish to Snapcraft'
|
||||
required: true
|
||||
default: 'true'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- name: Check package-lock.json
|
||||
run: npm ci
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install --legacy-peer-deps
|
||||
|
||||
- name: Build Electron app
|
||||
run: |
|
||||
npm run build:bruno-query
|
||||
npm run build:graphql-docs
|
||||
npm run build:web
|
||||
npm run build:electron:snap
|
||||
|
||||
- name: Install Snapcraft
|
||||
run: |
|
||||
sudo snap install snapcraft --classic
|
||||
continue-on-error: true
|
||||
|
||||
- name: Configure Snapcraft
|
||||
run: snapcraft whoami
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_API_KEY }}
|
||||
|
||||
- name: Publish to Snapcraft
|
||||
run: |
|
||||
snapcraft upload --release=stable packages/bruno-electron/out/*.snap
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_API_KEY }}
|
||||
22
.github/workflows/unit-tests.yml
vendored
22
.github/workflows/unit-tests.yml
vendored
@@ -5,16 +5,16 @@ on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
jobs:
|
||||
test:
|
||||
tests:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Install dependencies
|
||||
run: npm i --legacy-peer-deps
|
||||
run: npm ci --legacy-peer-deps
|
||||
- name: Test Package bruno-query
|
||||
run: npm run test --workspace=packages/bruno-query
|
||||
- name: Build Package bruno-query
|
||||
@@ -27,5 +27,19 @@ jobs:
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
- name: Test Package bruno-js
|
||||
run: npm run test --workspace=packages/bruno-js
|
||||
- name: Test Package bruno-cli
|
||||
run: npm run test --workspace=packages/bruno-cli
|
||||
- name: Test Package bruno-electron
|
||||
run: npm run test --workspace=packages/bruno-electron
|
||||
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Install dependencies
|
||||
run: npm ci --legacy-peer-deps
|
||||
- name: Run Prettier
|
||||
run: npm run test:prettier:web
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,7 +4,6 @@
|
||||
node_modules
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
## Lets make bruno better, together !!
|
||||
**English** | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md)
|
||||
|
||||
I am happy that you are looking to improve bruno. Below are the guidelines to get started bringing up bruno on your computer.
|
||||
## Let's make bruno better, together !!
|
||||
|
||||
We are happy that you are looking to improve bruno. Below are the guidelines to get started bringing up bruno on your computer.
|
||||
|
||||
### Technology Stack
|
||||
|
||||
Bruno is built using NextJs and React. We also use electron to ship a desktop version (that supports local collections)
|
||||
Bruno is built using Next.js and React. We also use electron to ship a desktop version (that supports local collections)
|
||||
|
||||
Libraries we use
|
||||
|
||||
@@ -19,11 +21,61 @@ Libraries we use
|
||||
|
||||
### Dependencies
|
||||
|
||||
You would need [Node v14.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
|
||||
You would need [Node v18.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
|
||||
|
||||
### Lets start coding
|
||||
## Development
|
||||
|
||||
Please reference [development.md](docs/development.md) for instructions on running the local development environment.
|
||||
Bruno is being developed as a desktop app. You need to load the app by running the Next.js app in one terminal and then run the electron app in another terminal.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# build graphql docs
|
||||
npm run build:graphql-docs
|
||||
|
||||
# build bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# run next app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# run electron app (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
|
||||
|
||||
```shell
|
||||
# Delete node_modules in sub-directories
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Delete package-lock in sub-directories
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
|
||||
### Raising Pull Request
|
||||
|
||||
@@ -31,5 +83,5 @@ Please reference [development.md](docs/development.md) for instructions on runni
|
||||
- Please follow the format of creating branches
|
||||
- feature/[feature name]: This branch should contain changes for a specific feature
|
||||
- Example: feature/dark-mode
|
||||
- bugfix/[bug name]: This branch should container only bug fixes for a specific bug
|
||||
- bugfix/[bug name]: This branch should contain only bug fixes for a specific bug
|
||||
- Example bugfix/bug-1
|
||||
|
||||
89
docs/contributing/contributing_de.md
Normal file
89
docs/contributing/contributing_de.md
Normal file
@@ -0,0 +1,89 @@
|
||||
## Lass uns Bruno noch besser machen, gemeinsam !!
|
||||
|
||||
Ich freue mich, dass Du Bruno verbessern möchtest. Hier findest Du eine Anleitung, mit der Du Bruno auf Deinem Computer einrichten kannst.
|
||||
|
||||
### Technologie Stack
|
||||
|
||||
Bruno ist mit Next.js und React erstellt. Außerdem benötigen wir electron für die Desktop Version (die lokale Sammlungen unterstützt).
|
||||
|
||||
Bibliotheken die wir benutzen
|
||||
|
||||
- CSS - Tailwind
|
||||
- Code Editoren - Codemirror
|
||||
- State Management - Redux
|
||||
- Icons - Tabler Icons
|
||||
- Formulare - formik
|
||||
- Schema Validierung - Yup
|
||||
- Request Client - axios
|
||||
- Dateisystem Watcher - chokidar
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
Du benötigst [Node v18.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
|
||||
|
||||
### Lass uns coden
|
||||
|
||||
Eine Anleitung zum Ausführen einer lokalen Entwicklungsumgebung findest Du in [development.md](docs/development_de.md).
|
||||
|
||||
### Pull Request erstellen
|
||||
|
||||
- Bitte halte die PRs klein und begrenzt auf eine Sache
|
||||
- Bitte halte Dich beim Erstellen eines Branches an das folgende Format
|
||||
- feature/[feature name]: Dieser Branch soll Änderungen für ein bestimmtes Feature enthalten
|
||||
- Beispiel: feature/dark-mode
|
||||
- bugfix/[bug name]: Dieser Branch soll ausschließlich Bugfixes für einen bestimmten Bug enthalten
|
||||
- Beispiel: bugfix/bug-1
|
||||
|
||||
## Entwicklung
|
||||
|
||||
Bruno wird als Desktop-Anwendung entwickelt. Um die App zu starten, musst Du zuerst die Next.js App in einem Terminal ausführen und anschließend in einem anderen Terminal die Electron-App.
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### Lokales Entwickeln
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# build graphql docs
|
||||
npm run build:graphql-docs
|
||||
|
||||
# build bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# run next app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# run electron app (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
Es kann sein, dass Du einen `Unsupported platform`-Fehler bekommst, wenn Du `npm install` ausführst. Um dies zu beheben, musst Du `node_modules` und `package-lock.json` löschen und `npm install` erneut ausführen. Dies sollte alle notwendigen Pakete installieren, die zum Ausführen der Anwendung benötigt werden.
|
||||
|
||||
```shell
|
||||
# Delete node_modules in sub-directories
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Delete package-lock in sub-directories
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Testen
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
89
docs/contributing/contributing_fr.md
Normal file
89
docs/contributing/contributing_fr.md
Normal file
@@ -0,0 +1,89 @@
|
||||
## Ensemble, améliorons Bruno !
|
||||
|
||||
Je suis content de voir que vous envisagez améliorer Bruno. Ci-dessous, vous trouverez les règles et guides pour récupérer Bruno sur votre ordinateur.
|
||||
|
||||
### Technologies utilisées
|
||||
|
||||
Bruno est construit en utilisant NextJs et React. Nous utilisons aussi Electron pour embarquer la version ordinateur (qui permet les collections locales).
|
||||
|
||||
Les bibliothèques que nous utilisons :
|
||||
|
||||
- CSS - Tailwind
|
||||
- Code Editors - Codemirror
|
||||
- State Management - Redux
|
||||
- Icons - Tabler Icons
|
||||
- Forms - formik
|
||||
- Schema Validation - Yup
|
||||
- Request Client - axios
|
||||
- Filesystem Watcher - chokidar
|
||||
|
||||
### Dépendances
|
||||
|
||||
Vous aurez besoin de [Node v18.x ou la dernière version LTS](https://nodejs.org/en/) et npm 8.x. Nous utilisons aussi les espaces de travail npm (_npm workspaces_) dans ce projet.
|
||||
|
||||
### Commençons à coder
|
||||
|
||||
Veuillez vous référez à la [documentation de développement](docs/development_fr.md) pour les instructions de démarrage de l'environnement de développement local.
|
||||
|
||||
### Ouvrir une Pull Request
|
||||
|
||||
- Merci de conserver les PR petites et focalisées sur un seul objectif
|
||||
- Merci de suivre le format de nom des branches
|
||||
- feature/[feature name]: Cette branche devrait contenir une fonctionnalité spécifique
|
||||
- Exemple: feature/dark-mode
|
||||
- bugfix/[bug name]: Cette branche devrait contenir seulement une solution pour pour une bogue spécifique
|
||||
- Exemple: bugfix/bug-1
|
||||
|
||||
## Développement
|
||||
|
||||
Bruno est développé comme une application de _lourde_. Vous devez charger l'application en démarrant nextjs dans un terminal, puis démarre l'application Electron dans un autre terminal.
|
||||
|
||||
### Dépendances
|
||||
|
||||
- NodeJS v18
|
||||
|
||||
### Développement local
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# build graphql docs
|
||||
npm run build:graphql-docs
|
||||
|
||||
# build bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# run next app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# run electron app (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Dépannage
|
||||
|
||||
Vous pourriez rencontrer une error `Unsupported platform` pendant le lancement de `npm install`. Pour résoudre cela, veuillez supprimer le répertoire `node_modules`, le fichier `package-lock.json` et lancer à nouveau `npm install`. Cela devrait isntaller tous les paquets nécessaires pour lancer l'application.
|
||||
|
||||
```shell
|
||||
# Delete node_modules in sub-directories
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Delete package-lock in sub-directories
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
89
docs/contributing/contributing_ru.md
Normal file
89
docs/contributing/contributing_ru.md
Normal file
@@ -0,0 +1,89 @@
|
||||
## Давайте вместе сделаем Бруно лучше!!!
|
||||
|
||||
Я рад, что Вы хотите усовершенствовать bruno. Ниже приведены рекомендации по запуску bruno на вашем компьютере.
|
||||
|
||||
### Стек
|
||||
|
||||
Bruno построен с использованием Next.js и React. Мы также используем electron для поставки десктопной версии ( которая поддерживает локальные коллекции )
|
||||
|
||||
Библиотеки, которые мы используем
|
||||
|
||||
- CSS - Tailwind
|
||||
- Редакторы кода - Codemirror
|
||||
- Управление состоянием - Redux
|
||||
- Иконки - Tabler Icons
|
||||
- Формы - formik
|
||||
- Валидация схем - Yup
|
||||
- Запросы клиента - axios
|
||||
- Наблюдатель за файловой системой - chokidar
|
||||
|
||||
### Зависимости
|
||||
|
||||
Вам потребуется [Node v18.x или последняя версия LTS](https://nodejs.org/en/) и npm 8.x. В проекте мы используем рабочие пространства npm
|
||||
|
||||
### Приступим к коду
|
||||
|
||||
Пожалуйста, обратитесь к [development_ru.md](docs/development_ru.md) для получения инструкций по запуску локальной среды разработки.
|
||||
|
||||
### Создание Pull Request
|
||||
|
||||
- Пожалуйста, пусть PR будет небольшим и сфокусированным на одной вещи
|
||||
- Пожалуйста, соблюдайте формат создания веток
|
||||
- feature/[название функции]: Эта ветка должна содержать изменения для конкретной функции
|
||||
- Пример: feature/dark-mode
|
||||
- bugfix/[название ошибки]: Эта ветка должна содержать только исправления для конкретной ошибки
|
||||
- Пример 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 в подкаталогах
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Тестирование
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
35
docs/contributing/contributing_tr.md
Normal file
35
docs/contributing/contributing_tr.md
Normal file
@@ -0,0 +1,35 @@
|
||||
## Bruno'yu birlikte daha iyi hale getirelim !!
|
||||
|
||||
Bruno'yu geliştirmek istemenizden mutluluk duyuyorum. Aşağıda, bruno'yu bilgisayarınıza getirmeye başlamak için yönergeler bulunmaktadır.
|
||||
|
||||
### Kullanılan Teknolojiler
|
||||
|
||||
Bruno, Next.js ve React kullanılarak oluşturulmuştur. Ayrıca bir masaüstü sürümü (yerel koleksiyonları destekleyen) göndermek için electron kullanıyoruz
|
||||
|
||||
Kullandığımız kütüphaneler
|
||||
|
||||
- CSS - Tailwind
|
||||
- Kod Düzenleyiciler - Codemirror
|
||||
- Durum Yönetimi - Redux
|
||||
- Iconlar - Tabler Simgeleri
|
||||
- Formlar - formik
|
||||
- Şema Doğrulama - Yup
|
||||
- İstek İstemcisi - axios
|
||||
- Dosya Sistemi İzleyicisi - chokidar
|
||||
|
||||
### Bağımlılıklar
|
||||
|
||||
[Node v18.x veya en son LTS sürümüne](https://nodejs.org/en/) ve npm 8.x'e ihtiyacınız olacaktır. Projede npm çalışma alanlarını kullanıyoruz
|
||||
|
||||
### Kodlamaya başlayalım
|
||||
|
||||
Yerel geliştirme ortamının çalıştırılmasına ilişkin talimatlar için lütfen [development.md](docs/development.md) adresine başvurun.
|
||||
|
||||
### Pull Request Oluşturma
|
||||
|
||||
- Lütfen PR'ları küçük tutun ve tek bir şeye odaklanın
|
||||
- Lütfen şube oluşturma formatını takip edin
|
||||
- feature/[özellik adı]: Bu dal belirli bir özellik için değişiklikler içermelidir
|
||||
- Örnek: feature/dark-mode
|
||||
- bugfix/[hata adı]: Bu dal yalnızca belirli bir hata için hata düzeltmelerini içermelidir
|
||||
- Örnek bugfix/bug-1
|
||||
89
docs/contributing/contributing_ua.md
Normal file
89
docs/contributing/contributing_ua.md
Normal file
@@ -0,0 +1,89 @@
|
||||
## Давайте зробимо Bruno краще, разом !!
|
||||
|
||||
Я дуже радий що Ви бажаєте покращити Bruno. Нижче наведені вказівки як розпочати розробку Bruno на Вашому комп'ютері.
|
||||
|
||||
### Стек технологій
|
||||
|
||||
Bruno побудований на Next.js та React. Також для десктопної версії (яка підтримує локальні колекції) використовується Electron
|
||||
|
||||
Бібліотеки, які ми використовуємо
|
||||
|
||||
- CSS - Tailwind
|
||||
- Редактори коду - Codemirror
|
||||
- Керування станом - Redux
|
||||
- Іконки - Tabler Icons
|
||||
- Форми - formik
|
||||
- Валідація по схемі - Yup
|
||||
- Клієнт запитів - axios
|
||||
- Спостерігач за файловою системою - chokidar
|
||||
|
||||
### Залежності
|
||||
|
||||
Вам знадобиться [Node v18.x або остання LTS версія](https://nodejs.org/en/) та npm 8.x. Ми використовуєм npm workspaces в цьому проекті
|
||||
|
||||
### Починаєм писати код
|
||||
|
||||
Будь ласка, зверніться до [development_ua.md](docs/development_ua.md) за інструкціями щодо запуску локального середовища розробки.
|
||||
|
||||
### Створення Pull Request-ів
|
||||
|
||||
- Будь ласка, робіть PR-и маленькими і сфокусованими на одній речі
|
||||
- Будь ласка, слідуйте формату назв гілок
|
||||
- feature/[назва feature]: Така гілка має містити зміни лише щодо конкретної feature
|
||||
- Приклад: feature/dark-mode
|
||||
- bugfix/[назва баґу]: Така гілка має містити лише виправлення конкретного багу
|
||||
- Приклад: 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
|
||||
|
||||
# запустіть додаток електрон (термінал 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Усунення несправностей
|
||||
|
||||
Ви можете зтикнутись із помилкою `Unsupported platform` коли запускаєте `npm install`. Щоб усунути цю проблему, вам потрібно видалити `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 в піддиректоріях
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Тестування
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
@@ -1,53 +0,0 @@
|
||||
## Development
|
||||
|
||||
Bruno is being developed as a desktop app. You need to load the app by running the nextjs app in one terminal and then run the electron app in another terminal.
|
||||
|
||||
### Dependencies
|
||||
* NodeJS v18
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
npm i --legacy-peer-deps
|
||||
|
||||
# build graphql docs
|
||||
# note: you can for now ignore the error thrown while building the graphql docs
|
||||
npm run build:graphql-docs
|
||||
|
||||
# build bruno query
|
||||
npm run build:bruno-query
|
||||
|
||||
# run next app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# run electron app (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
|
||||
|
||||
```shell
|
||||
# Delete node_modules in sub-directories
|
||||
find ./ -type d -name "node_modules" -print0 | while read -d $'\0' dir; do
|
||||
rm -rf "$dir"
|
||||
done
|
||||
|
||||
# Delete package-lock in sub-directories
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
```
|
||||
93
docs/readme/readme_de.md
Normal file
93
docs/readme/readme_de.md
Normal file
@@ -0,0 +1,93 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### Bruno - Opensource IDE zum Erkunden und Testen von APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
Bruno ist ein neuer und innovativer API-Client, der den Status Quo von Postman und ähnlichen Tools revolutionieren soll.
|
||||
|
||||
Bruno speichert Deine Sammlungen direkt in einem Ordner in Deinem Dateisystem. Wir verwenden eine einfache Textauszeichnungssprache - Bru - um Informationen über API-Anfragen zu speichern.
|
||||
|
||||
Du kannst Git oder eine andere Versionskontrolle deiner Wahl verwenden, um an deinen API-Sammlungen gemeinsam mit anderen zu arbeiten.
|
||||
|
||||
Bruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno eine Cloud-Synchronisation hinzuzufügen. Wir schätzen den Schutz Deiner Daten und glauben, dass sie auf Deinem Gerät bleiben sollten. Lies unsere Langzeit-Vision [hier](https://github.com/usebruno/bruno/discussions/269).
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Einsatz auf verschiedensten Plattformen 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Zusammenarbeiten mit Git 👩💻🧑💻
|
||||
|
||||
oder eine Versionskontrolle Deiner Wahl
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Wichtige Links 📌
|
||||
|
||||
- [Unsere Langzeit-Vision](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [Dokumentation](https://docs.usebruno.com)
|
||||
- [Webseite](https://www.usebruno.com)
|
||||
- [Preise](https://www.usebruno.com/pricing)
|
||||
- [Download](https://www.usebruno.com/downloads)
|
||||
|
||||
### Showcase 🎥
|
||||
|
||||
- [Erfahrungsberichte](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Wissenswertes](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### Unterstützung ❤️
|
||||
|
||||
Wuff! Wenn Du dieses Projekt magst, klick den ⭐ Button !!
|
||||
|
||||
### Teile Erfahrungsberichte 📣
|
||||
|
||||
Wenn Bruno Dir bei Deiner Arbeit und in Deinen Teams geholfen hat, vergiss bitte nicht, Deine [Erfahrungsberichte auf unserer GitHub-Diskussion](https://github.com/usebruno/bruno/discussions/343) zu teilen.
|
||||
|
||||
### Veröffentlichung in neuen Paketmanagern
|
||||
|
||||
Bitte [hier](/publishing.md) für mehr Informationen lesen.
|
||||
|
||||
### Mitmachen 👩💻🧑💻
|
||||
|
||||
Ich freue mich, dass Du Bruno verbessern willst. Bitte schau Dir den [Leitfaden zum Mitmachen](../contributing/contributing_de.md) an.
|
||||
|
||||
Auch wenn Du nicht in der Lage bist, einen Beitrag in Form von Code zu leisten, zögere bitte nicht, uns Fehler und Funktionswünsche mitzuteilen, die implementiert werden müssen, um Deinen Anwendungsfall zu unterstützen.
|
||||
|
||||
### Autoren
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### In Verbindung bleiben 🌐
|
||||
|
||||
[Twitter](https://twitter.com/use_bruno) <br />
|
||||
[Webseite](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[LinkedIn](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### Markenzeichen
|
||||
|
||||
**Name**
|
||||
|
||||
`Bruno` ist ein Markenzeichen von [Anoop M D](https://www.helloanoop.com/)
|
||||
|
||||
**Logo**
|
||||
|
||||
Das Logo stammt von [OpenMoji](https://openmoji.org/library/emoji-1F436/). Lizenz: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### Lizenz 📄
|
||||
|
||||
[MIT](/license.md)
|
||||
94
docs/readme/readme_fr.md
Normal file
94
docs/readme/readme_fr.md
Normal file
@@ -0,0 +1,94 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### Bruno - IDE Opensource pour explorer et tester des APIs.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
Bruno est un nouveau client API, innovant, qui a pour but de révolutionner le _status quo_ que représente Postman et les autres outils.
|
||||
|
||||
Bruno sauvegarde vos collections directement sur votre système de fichiers. Nous utilisons un langage de balise de type texte pour décrire les requêtes API.
|
||||
|
||||
Vous pouvez utiliser git ou tout autre gestionnaire de version pour travailler de manière collaborative sur vos collections d'APIs.
|
||||
|
||||
Bruno ne fonctionne qu'en mode déconnecté. Il n'y a pas de d'abonnement ou de synchronisation avec le cloud Bruno, il n'y en aura jamais. Nous sommes conscients de la confidentialité de vos données et nous sommes convaincus qu'elles doivent rester sur vos appareils. Vous pouvez lire notre vision à long terme [ici (en anglais)](https://github.com/usebruno/bruno/discussions/269).
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Fonctionne sur de multiples platformes 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Collaborer via Git 👩💻🧑💻
|
||||
|
||||
Ou n'importe quel système de gestion de sources
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Liens importants 📌
|
||||
|
||||
- [Notre vision à long terme (en anglais)](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [Documentation](https://docs.usebruno.com)
|
||||
- [Site web](https://www.usebruno.com)
|
||||
- [Prix](https://www.usebruno.com/pricing)
|
||||
- [Téléchargement](https://www.usebruno.com/downloads)
|
||||
|
||||
### Showcase 🎥
|
||||
|
||||
- [Témoignages](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Centre de connaissance](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### Soutien ❤️
|
||||
|
||||
Ouaf! Si vous aimez le projet, cliquez sur le bouton ⭐ !!
|
||||
|
||||
### Partage de témoignages 📣
|
||||
|
||||
Si Bruno vous a aidé dans votre travail, au sein de votre équipe, merci de penser à partager votre témoignage sur la [page discussion Github dédiée](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
### Publier Bruno sur un nouveau gestionnaire de paquets
|
||||
|
||||
Veuillez regarder [ici](/publishing.md) pour plus d'information.
|
||||
|
||||
### Contribuer 👩💻🧑💻
|
||||
|
||||
Je suis heureux de voir que vous cherchez à améliorer Bruno. Merci de consulter le [guide de contribution](../contributing/contributing_fr.md)
|
||||
|
||||
Même si vous n'êtes pas en mesure de contribuer directement via du code, n'hésitez pas à consigner les bogues et les demandes de nouvelles fonctionnalités pour résoudre vos cas d'usage !
|
||||
|
||||
### Auteurs
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Restons en contact 🌐
|
||||
|
||||
[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)
|
||||
|
||||
### Marque
|
||||
|
||||
**Nom**
|
||||
|
||||
`Bruno` est une marque appartenant à [Anoop M D](https://www.helloanoop.com/)
|
||||
|
||||
**Logo**
|
||||
|
||||
Le logo est issu de [OpenMoji](https://openmoji.org/library/emoji-1F436/).
|
||||
Licence: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)
|
||||
|
||||
### Licence 📄
|
||||
|
||||
[MIT](/license.md)
|
||||
77
docs/readme/readme_ru.md
Normal file
77
docs/readme/readme_ru.md
Normal file
@@ -0,0 +1,77 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### Bruno - IDE с открытым исходным кодом для изучения и тестирования API.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
Bruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами.
|
||||
|
||||
Bruno хранит ваши коллекции непосредственно в папке в вашей файловой системе. Для сохранения информации об API-запросах мы используем язык Bru.
|
||||
|
||||
Для совместной работы над коллекциями API можно использовать git или любой другой контроль версий по вашему выбору.
|
||||
|
||||
Bruno работает только в автономном режиме. Добавление облачной синхронизации в Bruno не планируется. Мы ценим конфиденциальность ваших данных и считаем, что они должны оставаться на вашем устройстве. Ознакомьтесь с нашим долгосрочным видением [здесь](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Работа на нескольких платформах 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Совместная работа через Git 👩💻🧑💻
|
||||
|
||||
Или другая система контроля версий по вашему выбору
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Важные ссылки 📌
|
||||
|
||||
- [Наше долгосрочное видение](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [Roadmap](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [Документация](https://docs.usebruno.com)
|
||||
- [Сайт](https://www.usebruno.com)
|
||||
- [Скачать Bruno](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)
|
||||
|
||||
### Поддержка ❤️
|
||||
|
||||
Гав! Если вам нравится проект, нажмите на звездочку ⭐ !!!
|
||||
|
||||
### Поделись отзывами 📣
|
||||
|
||||
Если Бруно помог вам в работе и в ваших командах, пожалуйста, не забудьте поделиться своим [отзывом на нашем обсуждении в github](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
### Внести вклад 👩💻🧑💻
|
||||
|
||||
Я рад, что Вы хотите улучшить Бруно. Пожалуйста, ознакомьтесь с [этим гайдом](../contributing/contributing_ru.md)
|
||||
|
||||
Даже если вы не можете внести свой вклад с помощью кода, пожалуйста, не стесняйтесь сообщать об ошибках и пожеланиях к функциям, которые необходимо реализовать для решения вашей задачи.
|
||||
|
||||
### Авторы
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Оставайтесь на связи 🌐
|
||||
|
||||
[X ( Twitter )](https://twitter.com/use_bruno) <br />
|
||||
[Наш сайт](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq)
|
||||
|
||||
### Лицензия 📄
|
||||
|
||||
[MIT](/license.md)
|
||||
78
docs/readme/readme_tr.md
Normal file
78
docs/readme/readme_tr.md
Normal file
@@ -0,0 +1,78 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### Bruno - API'leri keşfetmek ve test etmek için açık kaynaklı IDE.
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
Bruno, Postman ve benzeri araçlar tarafından temsil edilen statükoda devrim yaratmayı amaçlayan yeni ve yenilikçi bir API istemcisidir.
|
||||
|
||||
Bruno koleksiyonlarınızı doğrudan dosya sisteminizdeki bir klasörde saklar. API istekleri hakkındaki bilgileri kaydetmek için düz bir metin biçimlendirme dili olan Bru kullanıyoruz.
|
||||
|
||||
API koleksiyonlarınız üzerinde işbirliği yapmak için git veya seçtiğiniz herhangi bir sürüm kontrolünü kullanabilirsiniz.
|
||||
|
||||
Bruno yalnızca çevrimdışıdır. Bruno'ya bulut senkronizasyonu eklemek gibi bir planımız yok. Veri gizliliğinize değer veriyoruz ve cihazınızda kalması gerektiğine inanıyoruz. Uzun vadeli vizyonumuzu okuyun [burada](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Birden fazla platformda çalıştırın 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Git üzerinden işbirliği yapın 👩💻🧑💻
|
||||
|
||||
Veya seçtiğiniz herhangi bir sürüm kontrol sistemi
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Önemli Bağlantılar 📌
|
||||
|
||||
- [Uzun Vadeli Vizyonumuz](https://github.com/usebruno/bruno/discussions/269)
|
||||
- [Yol Haritası](https://github.com/usebruno/bruno/discussions/384)
|
||||
- [Dokümantasyon](https://docs.usebruno.com)
|
||||
- [Web sitesi](https://www.usebruno.com)
|
||||
- [İndir](https://www.usebruno.com/downloads)
|
||||
|
||||
### Vitrin 🎥
|
||||
|
||||
- [Görüşler](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Bilgi Merkezi](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### Destek ❤️
|
||||
|
||||
Woof! Projeyi beğendiyseniz, şu ⭐ düğmesine basın!
|
||||
|
||||
### Referansları Paylaşın 📣
|
||||
|
||||
Bruno işinizde ve ekiplerinizde size yardımcı olduysa, lütfen [github tartışmamızdaki referanslarınızı](https://github.com/usebruno/bruno/discussions/343) paylaşmayı unutmayın
|
||||
|
||||
### Katkıda Bulunun 👩💻🧑💻
|
||||
|
||||
Bruno'yu geliştirmek istemenize sevindim. Lütfen [katkıda bulunma kılavuzu](../contributing/contributing.md)'na göz atın
|
||||
|
||||
Kod yoluyla katkıda bulunamasanız bile, lütfen kullanım durumunuzu çözmek için uygulanması gereken hataları ve özellik isteklerini bildirmekten çekinmeyin.
|
||||
|
||||
### Katkıda Bulunanlar
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/usebruno/bruno/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=usebruno/bruno" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### İletişimde Kalın 🌐
|
||||
|
||||
[Twitter](https://twitter.com/use_bruno) <br />
|
||||
[Website](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq)
|
||||
[LinkedIn](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### Lisans 📄
|
||||
|
||||
[MIT](/license.md)
|
||||
78
docs/readme/readme_ua.md
Normal file
78
docs/readme/readme_ua.md
Normal file
@@ -0,0 +1,78 @@
|
||||
<br />
|
||||
<img src="../../assets/images/logo-transparent.png" width="80"/>
|
||||
|
||||
### Bruno - IDE із відкритим кодом для тестування та дослідження API
|
||||
|
||||
[](https://badge.fury.io/gh/usebruno%bruno)
|
||||
[](https://github.com/usebruno/bruno/workflows/unit-tests.yml)
|
||||
[](https://github.com/usebruno/bruno/pulse)
|
||||
[](https://twitter.com/use_bruno)
|
||||
[](https://www.usebruno.com)
|
||||
[](https://www.usebruno.com/downloads)
|
||||
|
||||
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman.
|
||||
|
||||
Bruno зберігає ваші колекції напряму у теці на вашому диску. Він використовує текстову мову розмітки Bru для збереження інформації про ваші API запити.
|
||||
|
||||
Ви можете використовувати git або будь-яку іншу систему контролю версій щоб спільно працювати над вашими колекціями API запитів.
|
||||
|
||||
Bruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Взнати більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Кросплатформенність 🖥️
|
||||
|
||||
 <br /><br />
|
||||
|
||||
### Спільна робота через Git 👩💻🧑💻
|
||||
|
||||
Або будь-яку іншу систему контролю версій на ваш вибір
|
||||
|
||||
 <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/downloads)
|
||||
|
||||
### Вітрина 🎥
|
||||
|
||||
- [Відгуки](https://github.com/usebruno/bruno/discussions/343)
|
||||
- [Хаб знань](https://github.com/usebruno/bruno/discussions/386)
|
||||
- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)
|
||||
|
||||
### Підтримка ❤️
|
||||
|
||||
Гав! Якщо вам сподобався проект, тисніть на ⭐ !!
|
||||
|
||||
### Поділитись відгуками 📣
|
||||
|
||||
Якщо Bruno допоміг вам у вашій роботі і вашим командам, будь ласка не забудьте поділитись вашими [відгуками у github дискусії](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
### Зробити свій внесок 👩💻🧑💻
|
||||
|
||||
Я радий що ви бажаєте покращити Bruno. Будь ласка переглянте [інструкцію по контрибуції](../contributing/contributing_ua.md)
|
||||
|
||||
Навіть якщо ви не можете зробити свій внесок пишучи програмний код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі.
|
||||
|
||||
### Автори
|
||||
|
||||
<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 />
|
||||
[Сайт](https://www.usebruno.com) <br />
|
||||
[Discord](https://discord.com/invite/KgcZUncpjq) <br />
|
||||
[LinkedIn](https://www.linkedin.com/company/usebruno)
|
||||
|
||||
### Ліцензія 📄
|
||||
|
||||
[MIT](/license.md)
|
||||
28264
package-lock.json
generated
Normal file
28264
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -22,7 +22,8 @@
|
||||
"jest": "^29.2.0",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"ts-jest": "^29.0.5"
|
||||
"ts-jest": "^29.0.5",
|
||||
"fs-extra": "^11.1.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
@@ -31,13 +32,19 @@
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
"build:electron": "./scripts/build-electron.sh",
|
||||
"build:electron": "node ./scripts/build-electron.js",
|
||||
"build:electron:mac": "./scripts/build-electron.sh mac",
|
||||
"build:electron:win": "./scripts/build-electron.sh win",
|
||||
"build:electron:linux": "./scripts/build-electron.sh linux",
|
||||
"build:electron:deb": "./scripts/build-electron.sh deb",
|
||||
"build:electron:rpm": "./scripts/build-electron.sh rpm",
|
||||
"build:electron:snap": "./scripts/build-electron.sh snap",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report",
|
||||
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup": "3.2.5"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cross-env ENV=dev next dev",
|
||||
"dev": "cross-env ENV=dev next dev -p 3000",
|
||||
"build": "next build && next export",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "jest",
|
||||
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -18,7 +19,7 @@
|
||||
"@tabler/icons": "^1.46.0",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.5.0",
|
||||
"@usebruno/schema": "0.6.0",
|
||||
"axios": "^0.26.0",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
@@ -27,13 +28,17 @@
|
||||
"file-dialog": "^0.0.8",
|
||||
"file-saver": "^2.0.5",
|
||||
"formik": "^2.2.9",
|
||||
"github-markdown-css": "^5.2.0",
|
||||
"graphiql": "^1.5.9",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"httpsnippet": "^3.0.1",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it": "^13.0.2",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "12.3.3",
|
||||
|
||||
@@ -4,6 +4,8 @@ const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
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-overlayscroll-horizontal div,
|
||||
|
||||
@@ -70,6 +70,35 @@ export default class CodeEditor extends React.Component {
|
||||
'Ctrl-F': 'findPersistent',
|
||||
Tab: function (cm) {
|
||||
cm.replaceSelection(' ', 'end');
|
||||
},
|
||||
'Ctrl-Y': 'foldAll',
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
'Cmd-I': 'unfoldAll'
|
||||
},
|
||||
foldOptions: {
|
||||
widget: (from, to) => {
|
||||
var count = undefined;
|
||||
var internal = this.editor.getRange(from, to);
|
||||
if (this.props.mode == 'application/ld+json') {
|
||||
if (this.editor.getLine(from.line).endsWith('[')) {
|
||||
var toParse = '[' + internal + ']';
|
||||
} else var toParse = '{' + internal + '}';
|
||||
try {
|
||||
count = Object.keys(JSON.parse(toParse)).length;
|
||||
} catch (e) {}
|
||||
} else if (this.props.mode == 'application/xml') {
|
||||
var doc = new DOMParser();
|
||||
try {
|
||||
//add header element and remove prefix namespaces for DOMParser
|
||||
var dcm = doc.parseFromString(
|
||||
'<a> ' + internal.replace(/(?<=\<|<\/)\w+:/g, '') + '</a>',
|
||||
'application/xml'
|
||||
);
|
||||
count = dcm.documentElement.children.length;
|
||||
} catch (e) {}
|
||||
}
|
||||
return count ? `\u21A4${count}\u21A6` : '\u2194';
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -117,10 +146,14 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.editor) {
|
||||
this.editor.refresh();
|
||||
}
|
||||
return (
|
||||
<StyledWrapper
|
||||
className="h-full"
|
||||
className="h-full w-full"
|
||||
aria-label="Code Editor"
|
||||
font={this.props.font}
|
||||
ref={(node) => {
|
||||
this._node = node;
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.auth-mode-selector {
|
||||
background: transparent;
|
||||
|
||||
.auth-mode-label {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,78 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateCollectionAuthMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
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 Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onModeChange = (value) => {
|
||||
dispatch(
|
||||
updateCollectionAuthMode({
|
||||
collectionUid: collection.uid,
|
||||
mode: value
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
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
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('basic');
|
||||
}}
|
||||
>
|
||||
Basic Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('bearer');
|
||||
}}
|
||||
>
|
||||
Bearer Token
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('none');
|
||||
}}
|
||||
>
|
||||
No Auth
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default AuthMode;
|
||||
@@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,191 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } 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 handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const handleAccessKeyIdChange = (accessKeyId) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSecretAccessKeyChange = (secretAccessKey) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSessionTokenChange = (sessionToken) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleServiceChange = (service) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRegionChange = (region) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleProfileNameChange = (profileName) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
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">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.accessKeyId || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAccessKeyIdChange(val)}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.secretAccessKey || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleSecretAccessKeyChange(val)}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Session Token</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.sessionToken || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleSessionTokenChange(val)}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Service</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.service || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleServiceChange(val)}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Region</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.region || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleRegionChange(val)}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Profile Name</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.profileName || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleProfileNameChange(val)}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AwsV4Auth;
|
||||
@@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } 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 handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'basic',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: basicAuth.password
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (password) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'basic',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: basicAuth.username,
|
||||
password: password
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicAuth;
|
||||
@@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } 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 handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const handleTokenChange = (token) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'bearer',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
token: token
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Token</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={bearerToken}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleTokenChange(val)}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default BearerAuth;
|
||||
@@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div``;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import AuthMode from './AuthMode';
|
||||
import AwsV4Auth from './AwsV4Auth';
|
||||
import BearerAuth from './BearerAuth';
|
||||
import BasicAuth from './BasicAuth';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Auth = ({ collection }) => {
|
||||
const authMode = get(collection, 'root.request.auth.mode');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'awsv4': {
|
||||
return <AwsV4Auth collection={collection} />;
|
||||
}
|
||||
case 'basic': {
|
||||
return <BasicAuth collection={collection} />;
|
||||
}
|
||||
case 'bearer': {
|
||||
return <BearerAuth collection={collection} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-2">
|
||||
<div className="flex flex-grow justify-start items-center">
|
||||
<AuthMode collection={collection} />
|
||||
</div>
|
||||
{getAuthView()}
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default Auth;
|
||||
@@ -0,0 +1,43 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.settings-label {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.certificate-icon {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
|
||||
input {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.available-certificates {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.url.bg};
|
||||
|
||||
button.remove-certificate {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
|
||||
.textbox {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.15rem 0.45rem;
|
||||
box-shadow: none;
|
||||
border-radius: 0px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,130 @@
|
||||
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 StyledWrapper from './StyledWrapper';
|
||||
|
||||
const ClientCertSettings = ({ clientCertConfig, onUpdate, onRemove }) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
domain: '',
|
||||
certFilePath: '',
|
||||
keyFilePath: '',
|
||||
passphrase: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
domain: Yup.string().required(),
|
||||
certFilePath: Yup.string().required(),
|
||||
keyFilePath: Yup.string().required(),
|
||||
passphrase: Yup.string()
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
onUpdate(values);
|
||||
}
|
||||
});
|
||||
|
||||
const getFile = (e) => {
|
||||
formik.values[e.name] = e.files[0].path;
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center font-semibold mt-4 mb-2">
|
||||
<IconCertificate className="mr-1 certificate-icon" size={24} strokeWidth={1.5} /> Client Certificates
|
||||
</div>
|
||||
<ul className="mt-4">
|
||||
{!clientCertConfig.length
|
||||
? 'None'
|
||||
: clientCertConfig.map((clientCert) => (
|
||||
<li key={uuid()} className="flex items-center available-certificates p-2 rounded-lg mb-2">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<div className="flex items-center">
|
||||
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
|
||||
{clientCert.domain}
|
||||
</div>
|
||||
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
|
||||
<IconTrash size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h1 className="font-semibold mt-8 mb-2">Add Client Certicate</h1>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="domain">
|
||||
Domain
|
||||
</label>
|
||||
<input
|
||||
id="domain"
|
||||
type="text"
|
||||
name="domain"
|
||||
placeholder="*.example.org"
|
||||
className="block textbox"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.domain || ''}
|
||||
/>
|
||||
{formik.touched.domain && formik.errors.domain ? (
|
||||
<div className="ml-1 text-red-500">{formik.errors.domain}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="certFilePath">
|
||||
Cert file
|
||||
</label>
|
||||
<input
|
||||
id="certFilePath"
|
||||
type="file"
|
||||
name="certFilePath"
|
||||
className="block"
|
||||
onChange={(e) => getFile(e.target)}
|
||||
/>
|
||||
{formik.touched.certFilePath && formik.errors.certFilePath ? (
|
||||
<div className="ml-1 text-red-500">{formik.errors.certFilePath}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="keyFilePath">
|
||||
Key file
|
||||
</label>
|
||||
<input
|
||||
id="keyFilePath"
|
||||
type="file"
|
||||
name="keyFilePath"
|
||||
className="block"
|
||||
onChange={(e) => getFile(e.target)}
|
||||
/>
|
||||
{formik.touched.keyFilePath && formik.errors.keyFilePath ? (
|
||||
<div className="ml-1 text-red-500">{formik.errors.keyFilePath}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="passphrase">
|
||||
Passphrase
|
||||
</label>
|
||||
<input
|
||||
id="passphrase"
|
||||
type="text"
|
||||
name="passphrase"
|
||||
className="block textbox"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.passphrase || ''}
|
||||
/>
|
||||
{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">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientCertSettings;
|
||||
@@ -0,0 +1,18 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 240px);
|
||||
|
||||
.CodeMirror-scroll {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
.editing-mode {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,55 @@
|
||||
import 'github-markdown-css/github-markdown.css';
|
||||
import get from 'lodash/get';
|
||||
import { updateCollectionDocs } from 'providers/ReduxStore/slices/collections';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Markdown from 'components/MarkDown';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Docs = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const docs = get(collection, 'root.docs', '');
|
||||
|
||||
const toggleViewMode = () => {
|
||||
setIsEditing((prev) => !prev);
|
||||
};
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateCollectionDocs({
|
||||
collectionUid: collection.uid,
|
||||
docs: value
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-1 h-full w-full relative">
|
||||
<div className="editing-mode mb-2" role="tab" onClick={toggleViewMode}>
|
||||
{isEditing ? 'Preview' : 'Edit'}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={storedTheme}
|
||||
value={docs || ''}
|
||||
onEdit={onEdit}
|
||||
onSave={onSave}
|
||||
mode="application/text"
|
||||
/>
|
||||
) : (
|
||||
<Markdown onDoubleClick={toggleViewMode} content={docs} />
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Docs;
|
||||
@@ -0,0 +1,56 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-header {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,151 @@
|
||||
import React 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
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const Headers = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const headers = get(collection, 'root.request.headers', []);
|
||||
|
||||
const addHeader = () => {
|
||||
dispatch(
|
||||
addCollectionHeader({
|
||||
collectionUid: 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;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateCollectionHeader({
|
||||
header: header,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveHeader = (header) => {
|
||||
dispatch(
|
||||
deleteCollectionHeader({
|
||||
headerUid: header.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<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}
|
||||
/>
|
||||
</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>
|
||||
<button className="btn-add-header text-link pr-2 py-3 mt-2 select-none" onClick={addHeader}>
|
||||
+ Add Header
|
||||
</button>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default Headers;
|
||||
@@ -1,13 +1,55 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
const proxySchema = Yup.object({
|
||||
enabled: Yup.string().oneOf(['global', 'true', 'false']),
|
||||
protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),
|
||||
hostname: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (hostname) => hostname.required('Specify the hostname for your proxy.'),
|
||||
otherwise: (hostname) => hostname.nullable()
|
||||
})
|
||||
.max(1024),
|
||||
port: Yup.number()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (port) => port.required('Specify port between 1 and 65535').typeError('Specify port between 1 and 65535'),
|
||||
otherwise: (port) => port.nullable().transform((_, val) => (val ? Number(val) : null))
|
||||
})
|
||||
.min(1)
|
||||
.max(65535),
|
||||
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 || false,
|
||||
enabled: proxyConfig.enabled || 'global',
|
||||
protocol: proxyConfig.protocol || 'http',
|
||||
hostname: proxyConfig.hostname || '',
|
||||
port: proxyConfig.port || '',
|
||||
@@ -15,27 +57,33 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||
}
|
||||
},
|
||||
bypassProxy: proxyConfig.bypassProxy || ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
protocol: Yup.string().oneOf(['http', 'https']),
|
||||
hostname: Yup.string().max(1024),
|
||||
port: Yup.number().min(0).max(65535),
|
||||
auth: Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
username: Yup.string().max(1024),
|
||||
password: Yup.string().max(1024)
|
||||
})
|
||||
}),
|
||||
validationSchema: proxySchema,
|
||||
onSubmit: (values) => {
|
||||
onUpdate(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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
formik.setValues({
|
||||
enabled: proxyConfig.enabled || false,
|
||||
enabled: proxyConfig.enabled === true ? 'true' : proxyConfig.enabled === false ? 'false' : 'global',
|
||||
protocol: proxyConfig.protocol || 'http',
|
||||
hostname: proxyConfig.hostname || '',
|
||||
port: proxyConfig.port || '',
|
||||
@@ -43,7 +91,8 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
enabled: proxyConfig.auth ? proxyConfig.auth.enabled || false : false,
|
||||
username: proxyConfig.auth ? proxyConfig.auth.username || '' : '',
|
||||
password: proxyConfig.auth ? proxyConfig.auth.password || '' : ''
|
||||
}
|
||||
},
|
||||
bypassProxy: proxyConfig.bypassProxy || ''
|
||||
});
|
||||
}, [proxyConfig]);
|
||||
|
||||
@@ -51,18 +100,64 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
<StyledWrapper>
|
||||
<h1 className="font-medium mb-3">Proxy Settings</h1>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="ml-4 mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="enabled">
|
||||
Enabled
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
Config
|
||||
<Tooltip
|
||||
text={`
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
`}
|
||||
tooltipId="request-var"
|
||||
/>
|
||||
</label>
|
||||
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="enabled"
|
||||
value="global"
|
||||
checked={formik.values.enabled === 'global'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
global
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="enabled"
|
||||
value={'true'}
|
||||
checked={formik.values.enabled === 'true'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
enabled
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="enabled"
|
||||
value={'false'}
|
||||
checked={formik.values.enabled === 'false'}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-1"
|
||||
/>
|
||||
disabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4 mb-3 flex items-center">
|
||||
<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 mr-4">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
@@ -73,7 +168,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
/>
|
||||
http
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
@@ -84,9 +179,31 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
/>
|
||||
https
|
||||
</label>
|
||||
<label className="flex items-center ml-4">
|
||||
<input
|
||||
type="radio"
|
||||
name="protocol"
|
||||
value="socks5"
|
||||
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="ml-4 mb-3 flex items-center">
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="hostname">
|
||||
Hostname
|
||||
</label>
|
||||
@@ -103,10 +220,10 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
value={formik.values.hostname || ''}
|
||||
/>
|
||||
{formik.touched.hostname && formik.errors.hostname ? (
|
||||
<div className="text-red-500">{formik.errors.hostname}</div>
|
||||
<div className="ml-3 text-red-500">{formik.errors.hostname}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="ml-4 mb-3 flex items-center">
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="port">
|
||||
Port
|
||||
</label>
|
||||
@@ -122,9 +239,11 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.port}
|
||||
/>
|
||||
{formik.touched.port && formik.errors.port ? <div className="text-red-500">{formik.errors.port}</div> : null}
|
||||
{formik.touched.port && formik.errors.port ? (
|
||||
<div className="ml-3 text-red-500">{formik.errors.port}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="ml-4 mb-3 flex items-center">
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.enabled">
|
||||
Auth
|
||||
</label>
|
||||
@@ -136,7 +255,7 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="ml-4 mb-3 flex items-center">
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.username">
|
||||
Username
|
||||
</label>
|
||||
@@ -153,10 +272,10 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
{formik.touched.auth?.username && formik.errors.auth?.username ? (
|
||||
<div className="text-red-500">{formik.errors.auth.username}</div>
|
||||
<div className="ml-3 text-red-500">{formik.errors.auth.username}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="ml-4 mb-3 flex items-center">
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="auth.password">
|
||||
Password
|
||||
</label>
|
||||
@@ -173,12 +292,32 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
{formik.touched.auth?.password && formik.errors.auth?.password ? (
|
||||
<div className="text-red-500">{formik.errors.auth.password}</div>
|
||||
<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="mt-6">
|
||||
<button type="submit" className="submit btn btn-md btn-secondary">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } 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 { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Script = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestScript = get(collection, 'root.request.script.req', '');
|
||||
const responseScript = get(collection, 'root.request.script.res', '');
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateCollectionRequestScript({
|
||||
script: value,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onResponseScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateCollectionResponseScript({
|
||||
script: value,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveCollectionRoot(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>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</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={storedTheme}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-12">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Script;
|
||||
@@ -1,6 +1,32 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
border: none;
|
||||
border-bottom: solid 2px transparent;
|
||||
margin-right: 1.25rem;
|
||||
color: var(--color-tab-inactive);
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:active,
|
||||
&:focus-within,
|
||||
&:focus-visible,
|
||||
&:target {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: ${(props) => props.theme.tabs.active.color} !important;
|
||||
border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
table {
|
||||
thead,
|
||||
td {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div``;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateCollectionTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Tests = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tests = get(collection, 'root.request.tests', '');
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateCollectionTests({
|
||||
tests: value,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={tests || ''}
|
||||
theme={storedTheme}
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
onSave={handleSave}
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tests;
|
||||
@@ -1,20 +1,56 @@
|
||||
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';
|
||||
import ClientCertSettings from './ClientCertSettings';
|
||||
import Headers from './Headers';
|
||||
import Auth from './Auth';
|
||||
import Script from './Script';
|
||||
import Test from './Tests';
|
||||
import Docs from './Docs';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CollectionSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tab = collection.settingsSelectedTab;
|
||||
const setTab = (tab) => {
|
||||
dispatch(
|
||||
updateSettingsSelectedTab({
|
||||
collectionUid: collection.uid,
|
||||
tab
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||
|
||||
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
|
||||
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');
|
||||
@@ -22,11 +58,84 @@ const CollectionSettings = ({ collection }) => {
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update collection settings'));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="px-4 py-4">
|
||||
<h1 className="font-semibold mb-4">Collection Settings</h1>
|
||||
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'));
|
||||
};
|
||||
|
||||
<ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'headers': {
|
||||
return <Headers collection={collection} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth collection={collection} />;
|
||||
}
|
||||
case 'script': {
|
||||
return <Script collection={collection} />;
|
||||
}
|
||||
case 'tests': {
|
||||
return <Test collection={collection} />;
|
||||
}
|
||||
case 'proxy': {
|
||||
return <ProxySettings proxyConfig={proxyConfig} onUpdate={onProxySettingsUpdate} />;
|
||||
}
|
||||
case 'clientCert': {
|
||||
return (
|
||||
<ClientCertSettings
|
||||
clientCertConfig={clientCertConfig}
|
||||
onUpdate={onClientCertSettingsUpdate}
|
||||
onRemove={onClientCertSettingsRemove}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'docs': {
|
||||
return <Docs collection={collection} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getTabClassname = (tabName) => {
|
||||
return classnames(`tab select-none ${tabName}`, {
|
||||
active: tabName === tab
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative px-4 py-4">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => setTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||
Proxy
|
||||
</div>
|
||||
<div className={getTabClassname('clientCert')} role="tab" onClick={() => setTab('clientCert')}>
|
||||
Client Certificates
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
</div>
|
||||
<section className={`flex ${['auth', 'script', 'docs', 'clientCert'].includes(tab) ? '' : 'mt-4'}`}>
|
||||
{getTabPanel(tab)}
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.CodeMirror {
|
||||
/* todo: find a better way */
|
||||
height: calc(100vh - 240px);
|
||||
|
||||
.CodeMirror-scroll {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
.editing-mode {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
60
packages/bruno-app/src/components/Documentation/index.js
Normal file
60
packages/bruno-app/src/components/Documentation/index.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import 'github-markdown-css/github-markdown.css';
|
||||
import get from 'lodash/get';
|
||||
import { updateRequestDocs } from 'providers/ReduxStore/slices/collections';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import { useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import Markdown from 'components/MarkDown';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Documentation = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const docs = item.draft ? get(item, 'draft.request.docs') : get(item, 'request.docs');
|
||||
|
||||
const toggleViewMode = () => {
|
||||
setIsEditing((prev) => !prev);
|
||||
};
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestDocs({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
docs: value
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-1 h-full w-full relative">
|
||||
<div className="editing-mode mb-2" role="tab" onClick={toggleViewMode}>
|
||||
{isEditing ? 'Preview' : 'Edit'}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={storedTheme}
|
||||
value={docs || ''}
|
||||
onEdit={onEdit}
|
||||
onSave={onSave}
|
||||
mode="application/text"
|
||||
/>
|
||||
) : (
|
||||
<Markdown onDoubleClick={toggleViewMode} content={docs} />
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Documentation;
|
||||
@@ -14,6 +14,8 @@ const Wrapper = styled.div`
|
||||
background-color: ${(props) => props.theme.dropdown.bg};
|
||||
box-shadow: ${(props) => props.theme.dropdown.shadow};
|
||||
border-radius: 3px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
|
||||
.tippy-content {
|
||||
padding-left: 0;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import Modal from 'components/Modal/index';
|
||||
import Portal from 'components/Portal/index';
|
||||
import { useFormik } from 'formik';
|
||||
import { copyEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const CopyEnvironment = ({ collection, environment, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
name: environment.name + ' - Copy'
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(copyEnvironment(values.name, environment.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment created in collection');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while created the environment'));
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onSubmit = () => {
|
||||
formik.handleSubmit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title={'Copy Environment'} confirmText="Copy" handleConfirm={onSubmit} handleCancel={onClose}>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div>
|
||||
<label htmlFor="name" className="block font-semibold">
|
||||
New Environment Name
|
||||
</label>
|
||||
<input
|
||||
id="environment-name"
|
||||
type="text"
|
||||
name="name"
|
||||
ref={inputRef}
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.name || ''}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name ? (
|
||||
<div className="text-red-500">{formik.errors.name}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyEnvironment;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Portal from 'components/Portal/index';
|
||||
import Modal from 'components/Modal/index';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useFormik } from 'formik';
|
||||
import { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -17,7 +17,7 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
|
||||
@@ -13,10 +13,12 @@ const Wrapper = styled.div`
|
||||
padding: 4px 10px;
|
||||
|
||||
&:nth-child(1),
|
||||
&:nth-child(4),
|
||||
&:nth-child(5) {
|
||||
&:nth-child(4) {
|
||||
width: 70px;
|
||||
}
|
||||
&:nth-child(5) {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 25%;
|
||||
|
||||
@@ -1,68 +1,79 @@
|
||||
import React, { useReducer } from 'react';
|
||||
import React from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import reducer from './reducer';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { uuid } from 'utils/common';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const [state, reducerDispatch] = useReducer(reducer, { hasChanges: false, variables: environment.variables || [] });
|
||||
const { variables, hasChanges } = state;
|
||||
|
||||
const saveChanges = () => {
|
||||
dispatch(saveEnvironment(cloneDeep(variables), environment.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Changes saved successfully');
|
||||
reducerDispatch({
|
||||
type: 'CHANGES_SAVED'
|
||||
});
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: environment.variables || [],
|
||||
validationSchema: Yup.array().of(
|
||||
Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
name: Yup.string()
|
||||
.required('Name cannot be empty')
|
||||
.matches(/^(?!\d)\w*$/, 'Name contains invalid characters')
|
||||
.trim(),
|
||||
secret: Yup.boolean(),
|
||||
type: Yup.string(),
|
||||
uid: Yup.string(),
|
||||
value: Yup.string().trim()
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while saving the changes'));
|
||||
),
|
||||
onSubmit: (values) => {
|
||||
if (!formik.dirty) {
|
||||
toast.error('Nothing to save');
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(saveEnvironment(cloneDeep(values), environment.uid, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Changes saved successfully');
|
||||
formik.resetForm({ values });
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while saving the changes'));
|
||||
}
|
||||
});
|
||||
|
||||
const ErrorMessage = ({ name }) => {
|
||||
const meta = formik.getFieldMeta(name);
|
||||
console.log(name, meta);
|
||||
if (!meta.error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<label htmlFor={name} className="text-red-500">
|
||||
{meta.error}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
const addVariable = () => {
|
||||
reducerDispatch({
|
||||
type: 'ADD_VAR'
|
||||
});
|
||||
const newVariable = {
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
};
|
||||
formik.setFieldValue(formik.values.length, newVariable, false);
|
||||
};
|
||||
|
||||
const handleVarChange = (e, _variable, type) => {
|
||||
const variable = cloneDeep(_variable);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
variable.name = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
variable.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
variable.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
case 'secret': {
|
||||
variable.secret = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
reducerDispatch({
|
||||
type: 'UPDATE_VAR',
|
||||
variable
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveVars = (variable) => {
|
||||
reducerDispatch({
|
||||
type: 'DELETE_VAR',
|
||||
variable
|
||||
});
|
||||
const handleRemoveVar = (id) => {
|
||||
formik.setValues(formik.values.filter((variable) => variable.uid !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -78,55 +89,57 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{variables && variables.length
|
||||
? variables.map((variable, index) => {
|
||||
return (
|
||||
<tr key={variable.uid}>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={variable.enabled}
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, variable, 'enabled')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={variable.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleVarChange(e, variable, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={variable.value}
|
||||
theme={storedTheme}
|
||||
onChange={(newValue) => handleVarChange({ target: { value: newValue } }, variable, 'value')}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={variable.secret}
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, variable, 'secret')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button onClick={() => handleRemoveVars(variable)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
{formik.values.map((variable, index) => (
|
||||
<tr key={variable.uid}>
|
||||
<td className="text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-3 mousetrap"
|
||||
name={`${index}.enabled`}
|
||||
checked={variable.enabled}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
className="mousetrap"
|
||||
id={`${index}.name`}
|
||||
name={`${index}.name`}
|
||||
value={formik.values[index].name}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
<ErrorMessage name={`${index}.name`} />
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
collection={collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
onChange={(newValue) => formik.setFieldValue(`${index}.value`, newValue, true)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-3 mousetrap"
|
||||
name={`${index}.secret`}
|
||||
checked={variable.secret}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button onClick={() => handleRemoveVar(variable.uid)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -137,12 +150,7 @@ const EnvironmentVariables = ({ environment, collection }) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-md btn-secondary mt-2"
|
||||
disabled={!hasChanges}
|
||||
onClick={saveChanges}
|
||||
>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary mt-2" onClick={formik.handleSubmit}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import produce from 'immer';
|
||||
import find from 'lodash/find';
|
||||
import filter from 'lodash/filter';
|
||||
import { uuid } from 'utils/common';
|
||||
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'ADD_VAR': {
|
||||
return produce(state, (draft) => {
|
||||
draft.variables.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
type: 'text',
|
||||
secret: false,
|
||||
enabled: true
|
||||
});
|
||||
draft.hasChanges = true;
|
||||
});
|
||||
}
|
||||
|
||||
case 'UPDATE_VAR': {
|
||||
return produce(state, (draft) => {
|
||||
const variable = find(draft.variables, (v) => v.uid === action.variable.uid);
|
||||
variable.name = action.variable.name;
|
||||
variable.value = action.variable.value;
|
||||
variable.enabled = action.variable.enabled;
|
||||
variable.secret = action.variable.secret;
|
||||
draft.hasChanges = true;
|
||||
});
|
||||
}
|
||||
|
||||
case 'DELETE_VAR': {
|
||||
return produce(state, (draft) => {
|
||||
draft.variables = filter(draft.variables, (v) => v.uid !== action.variable.uid);
|
||||
draft.hasChanges = true;
|
||||
});
|
||||
}
|
||||
|
||||
case 'CHANGES_SAVED': {
|
||||
return produce(state, (draft) => {
|
||||
draft.hasChanges = false;
|
||||
});
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IconEdit, IconTrash, IconDatabase } from '@tabler/icons';
|
||||
import EnvironmentVariables from './EnvironmentVariables';
|
||||
import RenameEnvironment from '../../RenameEnvironment';
|
||||
import { IconCopy, IconDatabase, IconEdit, IconTrash } from '@tabler/icons';
|
||||
import { useState } from 'react';
|
||||
import CopyEnvironment from '../../CopyEnvironment';
|
||||
import DeleteEnvironment from '../../DeleteEnvironment';
|
||||
import RenameEnvironment from '../../RenameEnvironment';
|
||||
import EnvironmentVariables from './EnvironmentVariables';
|
||||
|
||||
const EnvironmentDetails = ({ environment, collection }) => {
|
||||
const [openEditModal, setOpenEditModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
const [openCopyModal, setOpenCopyModal] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="px-6 flex-grow flex flex-col pt-6" style={{ maxWidth: '700px' }}>
|
||||
@@ -20,6 +22,9 @@ const EnvironmentDetails = ({ environment, collection }) => {
|
||||
collection={collection}
|
||||
/>
|
||||
)}
|
||||
{openCopyModal && (
|
||||
<CopyEnvironment onClose={() => setOpenCopyModal(false)} environment={environment} collection={collection} />
|
||||
)}
|
||||
<div className="flex">
|
||||
<div className="flex flex-grow items-center">
|
||||
<IconDatabase className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
@@ -27,6 +32,7 @@ const EnvironmentDetails = ({ environment, collection }) => {
|
||||
</div>
|
||||
<div className="flex gap-x-4 pl-4">
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenEditModal(true)} />
|
||||
<IconCopy className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenCopyModal(true)} />
|
||||
<IconTrash className="cursor-pointer" size={20} strokeWidth={1.5} onClick={() => setOpenDeleteModal(true)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,9 @@ const StyledWrapper = styled.div`
|
||||
background-color: ${(props) => props.theme.collection.environment.settings.sidebar.bg};
|
||||
border-right: solid 1px ${(props) => props.theme.collection.environment.settings.sidebar.borderRight};
|
||||
min-height: 400px;
|
||||
height: 100%;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.environment-item {
|
||||
@@ -35,18 +38,21 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
|
||||
.btn-create-environment {
|
||||
.btn-create-environment,
|
||||
.btn-import-environment {
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: none;
|
||||
color: ${(props) => props.theme.textLink};
|
||||
|
||||
&:hover {
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
span:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-import-environment {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -2,13 +2,18 @@ import React, { useEffect, useState, forwardRef, useRef } from 'react';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import usePrevious from 'hooks/usePrevious';
|
||||
import EnvironmentDetails from './EnvironmentDetails';
|
||||
import CreateEnvironment from '../CreateEnvironment/index';
|
||||
import CreateEnvironment from '../CreateEnvironment';
|
||||
import { IconDownload, IconShieldLock } from '@tabler/icons';
|
||||
import ImportEnvironment from '../ImportEnvironment';
|
||||
import ManageSecrets from '../ManageSecrets';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const EnvironmentList = ({ collection }) => {
|
||||
const { environments } = collection;
|
||||
const [selectedEnvironment, setSelectedEnvironment] = useState(null);
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [openImportModal, setOpenImportModal] = useState(false);
|
||||
const [openManageSecretsModal, setOpenManageSecretsModal] = useState(false);
|
||||
|
||||
const envUids = environments ? environments.map((env) => env.uid) : [];
|
||||
const prevEnvUids = usePrevious(envUids);
|
||||
@@ -48,9 +53,11 @@ const EnvironmentList = ({ collection }) => {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
|
||||
{openImportModal && <ImportEnvironment collection={collection} onClose={() => setOpenImportModal(false)} />}
|
||||
{openManageSecretsModal && <ManageSecrets onClose={() => setOpenManageSecretsModal(false)} />}
|
||||
<div className="flex">
|
||||
<div>
|
||||
<div className="environments-sidebar">
|
||||
<div className="environments-sidebar flex flex-col">
|
||||
{environments &&
|
||||
environments.length &&
|
||||
environments.map((env) => (
|
||||
@@ -65,6 +72,17 @@ const EnvironmentList = ({ collection }) => {
|
||||
<div className="btn-create-environment" onClick={() => setOpenCreateModal(true)}>
|
||||
+ <span>Create</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto btn-import-environment">
|
||||
<div className="flex items-center" onClick={() => setOpenImportModal(true)}>
|
||||
<IconDownload size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Import</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-2" onClick={() => setOpenManageSecretsModal(true)}>
|
||||
<IconShieldLock size={12} strokeWidth={2} />
|
||||
<span className="label ml-1 text-xs">Managing Secrets</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EnvironmentDetails environment={selectedEnvironment} collection={collection} />
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import importPostmanEnvironment from 'utils/importers/postman-environment';
|
||||
import { importEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { toastError } from 'utils/common/error';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ImportEnvironment = ({ onClose, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleImportPostmanEnvironment = () => {
|
||||
importPostmanEnvironment()
|
||||
.then((environment) => {
|
||||
dispatch(importEnvironment(environment.name, environment.variables, collection.uid))
|
||||
.then(() => {
|
||||
toast.success('Environment imported successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while importing the environment'));
|
||||
})
|
||||
.catch((err) => toastError(err, 'Postman Import environment failed'));
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title="Import Environment" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<div>
|
||||
<div className="text-link hover:underline cursor-pointer" onClick={handleImportPostmanEnvironment}>
|
||||
Postman Environment
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportEnvironment;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
const ManageSecrets = ({ onClose }) => {
|
||||
return (
|
||||
<Portal>
|
||||
<Modal size="sm" title="Manage Secrets" hideFooter={true} handleConfirm={onClose} handleCancel={onClose}>
|
||||
<div>
|
||||
<p>In any collection, there are secrets that need to be managed.</p>
|
||||
<p className="mt-2">These secrets can be anything such as API keys, passwords, or tokens.</p>
|
||||
<p className="mt-4">Bruno offers two approaches to manage secrets in collections.</p>
|
||||
<p className="mt-2">
|
||||
Read more about it in our{' '}
|
||||
<a
|
||||
href="https://docs.usebruno.com/secrets-management/overview.html"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-link hover:underline"
|
||||
>
|
||||
docs
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageSecrets;
|
||||
@@ -17,7 +17,7 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be atleast 1 characters')
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
}),
|
||||
|
||||
@@ -3,10 +3,12 @@ import React, { useState } from 'react';
|
||||
import CreateEnvironment from './CreateEnvironment';
|
||||
import EnvironmentList from './EnvironmentList';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ImportEnvironment from './ImportEnvironment';
|
||||
|
||||
const EnvironmentSettings = ({ collection, onClose }) => {
|
||||
const { environments } = collection;
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [openImportModal, setOpenImportModal] = useState(false);
|
||||
|
||||
if (!environments || !environments.length) {
|
||||
return (
|
||||
@@ -20,13 +22,23 @@ const EnvironmentSettings = ({ collection, onClose }) => {
|
||||
hideCancel={true}
|
||||
>
|
||||
{openCreateModal && <CreateEnvironment collection={collection} onClose={() => setOpenCreateModal(false)} />}
|
||||
<div className="text-center">
|
||||
{openImportModal && <ImportEnvironment collection={collection} onClose={() => setOpenImportModal(false)} />}
|
||||
<div className="text-center flex flex-col">
|
||||
<p>No environments found!</p>
|
||||
<button
|
||||
className="btn-create-environment text-link pr-2 py-3 mt-2 select-none"
|
||||
onClick={() => setOpenCreateModal(true)}
|
||||
>
|
||||
+ <span>Create Environment</span>
|
||||
<span>Create Environment</span>
|
||||
</button>
|
||||
|
||||
<span>Or</span>
|
||||
|
||||
<button
|
||||
className="btn-import-environment text-link pl-2 pr-2 py-3 select-none"
|
||||
onClick={() => setOpenImportModal(true)}
|
||||
>
|
||||
<span>Import Environment</span>
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
83
packages/bruno-app/src/components/MarkDown/StyledWrapper.js
Normal file
83
packages/bruno-app/src/components/MarkDown/StyledWrapper.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledMarkdownBodyWrapper = styled.div`
|
||||
background: transparent;
|
||||
height: inherit;
|
||||
.markdown-body {
|
||||
background: transparent;
|
||||
overflow-y: auto;
|
||||
color: ${(props) => props.theme.text};
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
padding-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
|
||||
h1 {
|
||||
margin: 0.67em 0;
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
padding-bottom: 0.3em;
|
||||
font-size: 1.3;
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
padding-bottom: 0.3em;
|
||||
font-size: 1.2;
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-weight: var(--base-text-weight-semibold, 600);
|
||||
font-size: 0.9em;
|
||||
color: var(--color-fg-muted);
|
||||
}
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--color-border-muted);
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: 24px 0;
|
||||
background-color: var(--color-border-default);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: ${(props) => props.theme.sidebar.bg};
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.markdown-body {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledMarkdownBodyWrapper;
|
||||
32
packages/bruno-app/src/components/MarkDown/index.js
Normal file
32
packages/bruno-app/src/components/MarkDown/index.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const md = new MarkdownIt();
|
||||
|
||||
const Markdown = ({ onDoubleClick, content }) => {
|
||||
const handleOnDoubleClick = (event) => {
|
||||
switch (event.detail) {
|
||||
case 2: {
|
||||
onDoubleClick();
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
const htmlFromMarkdown = md.render(content || '');
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div
|
||||
className="markdown-body"
|
||||
dangerouslySetInnerHTML={{ __html: htmlFromMarkdown }}
|
||||
onClick={handleOnDoubleClick}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Markdown;
|
||||
@@ -1,6 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
color: ${(props) => props.theme.text};
|
||||
|
||||
&.modal--animate-out {
|
||||
animation: fade-out 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);
|
||||
|
||||
@@ -20,6 +22,7 @@ const Wrapper = styled.div`
|
||||
justify-content: center;
|
||||
overflow-y: auto;
|
||||
z-index: 10;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.bruno-modal-card {
|
||||
|
||||
@@ -61,19 +61,20 @@ const Modal = ({
|
||||
children,
|
||||
confirmDisabled,
|
||||
hideCancel,
|
||||
hideFooter
|
||||
hideFooter,
|
||||
closeModalFadeTimeout = 500
|
||||
}) => {
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
const escFunction = (event) => {
|
||||
const escKeyCode = 27;
|
||||
if (event.keyCode === escKeyCode) {
|
||||
closeModal();
|
||||
closeModal({ type: 'esc' });
|
||||
}
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
const closeModal = (args) => {
|
||||
setIsClosing(true);
|
||||
setTimeout(() => handleCancel(), 500);
|
||||
setTimeout(() => handleCancel(args), closeModalFadeTimeout);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -94,12 +95,12 @@ const Modal = ({
|
||||
return (
|
||||
<StyledWrapper className={classes}>
|
||||
<div className={`bruno-modal-card modal-${size}`}>
|
||||
<ModalHeader title={title} handleCancel={() => closeModal()} />
|
||||
<ModalHeader title={title} handleCancel={() => closeModal({ type: 'icon' })} />
|
||||
<ModalContent>{children}</ModalContent>
|
||||
<ModalFooter
|
||||
confirmText={confirmText}
|
||||
cancelText={cancelText}
|
||||
handleCancel={() => closeModal()}
|
||||
handleCancel={() => closeModal({ type: 'button' })}
|
||||
handleSubmit={handleConfirm}
|
||||
confirmDisabled={confirmDisabled}
|
||||
hideCancel={hideCancel}
|
||||
@@ -108,7 +109,12 @@ const Modal = ({
|
||||
</div>
|
||||
|
||||
{/* Clicking on backdrop closes the modal */}
|
||||
<div className="bruno-modal-backdrop" onClick={() => closeModal()} />
|
||||
<div
|
||||
className="bruno-modal-backdrop"
|
||||
onClick={() => {
|
||||
closeModal({ type: 'backdrop' });
|
||||
}}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
color: ${(props) => props.theme.text};
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
53
packages/bruno-app/src/components/Preferences/Font/index.js
Normal file
53
packages/bruno-app/src/components/Preferences/Font/index.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Font = ({ close }) => {
|
||||
const dispatch = useDispatch();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const [codeFont, setCodeFont] = useState(get(preferences, 'font.codeFont', 'default'));
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
setCodeFont(event.target.value);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(
|
||||
savePreferences({
|
||||
...preferences,
|
||||
font: {
|
||||
codeFont
|
||||
}
|
||||
})
|
||||
).then(() => {
|
||||
close();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<label className="block font-medium">Code Editor Font</label>
|
||||
<input
|
||||
type="text"
|
||||
className="block textbox mt-2 w-full"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={handleInputChange}
|
||||
defaultValue={codeFont}
|
||||
/>
|
||||
|
||||
<div className="mt-10">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Font;
|
||||
@@ -1,36 +1,103 @@
|
||||
import React, { useState } from 'react';
|
||||
import { usePreferences } from 'providers/Preferences';
|
||||
import React from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const General = () => {
|
||||
const { preferences, setPreferences } = usePreferences();
|
||||
const General = ({ close }) => {
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [sslVerification, setSslVerification] = useState(preferences.request.sslVerification);
|
||||
|
||||
const handleCheckboxChange = () => {
|
||||
const updatedPreferences = {
|
||||
...preferences,
|
||||
request: {
|
||||
...preferences.request,
|
||||
sslVerification: !sslVerification
|
||||
}
|
||||
};
|
||||
|
||||
setPreferences(updatedPreferences)
|
||||
.then(() => {
|
||||
setSslVerification(!sslVerification);
|
||||
const preferencesSchema = Yup.object().shape({
|
||||
sslVerification: Yup.boolean(),
|
||||
timeout: Yup.mixed()
|
||||
.transform((value, originalValue) => {
|
||||
return originalValue === '' ? undefined : value;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
.nullable()
|
||||
.test('isNumber', 'Request Timeout must be a number', (value) => {
|
||||
return value === undefined || !isNaN(value);
|
||||
})
|
||||
.test('isValidTimeout', 'Request Timeout must be equal or greater than 0', (value) => {
|
||||
return value === undefined || Number(value) >= 0;
|
||||
})
|
||||
});
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
sslVerification: preferences.request.sslVerification,
|
||||
timeout: preferences.request.timeout
|
||||
},
|
||||
validationSchema: preferencesSchema,
|
||||
onSubmit: async (values) => {
|
||||
try {
|
||||
const newPreferences = await preferencesSchema.validate(values, { abortEarly: true });
|
||||
handleSave(newPreferences);
|
||||
} catch (error) {
|
||||
console.error('Preferences validation error:', error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const handleSave = (newPreferences) => {
|
||||
dispatch(
|
||||
savePreferences({
|
||||
...preferences,
|
||||
request: {
|
||||
sslVerification: newPreferences.sslVerification,
|
||||
timeout: newPreferences.timeout
|
||||
}
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
close();
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to update preferences'));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="flex items-center mt-2">
|
||||
<input type="checkbox" checked={sslVerification} onChange={handleCheckboxChange} className="mr-3 mousetrap" />
|
||||
SSL Certificate Verification
|
||||
</div>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="flex items-center mt-2">
|
||||
<label className="block font-medium mr-2 select-none" style={{ minWidth: 200 }} htmlFor="sslVerification">
|
||||
SSL/TLS Certificate Verification
|
||||
</label>
|
||||
<input
|
||||
id="ssl-cert-verification"
|
||||
type="checkbox"
|
||||
name="sslVerification"
|
||||
checked={formik.values.sslVerification}
|
||||
onChange={formik.handleChange}
|
||||
className="mousetrap mr-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col mt-6">
|
||||
<label className="block font-medium select-none" htmlFor="timeout">
|
||||
Request Timeout (in ms)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="timeout"
|
||||
className="block textbox mt-2 w-16"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.timeout}
|
||||
/>
|
||||
</div>
|
||||
{formik.touched.timeout && formik.errors.timeout ? (
|
||||
<div className="text-red-500">{formik.errors.timeout}</div>
|
||||
) : null}
|
||||
<div className="mt-10">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.settings-label {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.textbox {
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.15rem 0.45rem;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,292 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import toast from 'react-hot-toast';
|
||||
import { savePreferences } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
const ProxySettings = ({ close }) => {
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const proxySchema = Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
protocol: Yup.string().required().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()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (port) => port.required('Specify port between 1 and 65535').typeError('Specify port between 1 and 65535'),
|
||||
otherwise: (port) => port.nullable().transform((_, val) => (val ? Number(val) : null))
|
||||
})
|
||||
.min(1)
|
||||
.max(65535),
|
||||
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: preferences.proxy.enabled || false,
|
||||
protocol: preferences.proxy.protocol || 'http',
|
||||
hostname: preferences.proxy.hostname || '',
|
||||
port: preferences.proxy.port || 0,
|
||||
auth: {
|
||||
enabled: preferences.proxy.auth ? preferences.proxy.auth.enabled || false : false,
|
||||
username: preferences.proxy.auth ? preferences.proxy.auth.username || '' : '',
|
||||
password: preferences.proxy.auth ? preferences.proxy.auth.password || '' : ''
|
||||
},
|
||||
bypassProxy: preferences.proxy.bypassProxy || ''
|
||||
},
|
||||
validationSchema: proxySchema,
|
||||
onSubmit: (values) => {
|
||||
onUpdate(values);
|
||||
}
|
||||
});
|
||||
|
||||
const onUpdate = (values) => {
|
||||
proxySchema
|
||||
.validate(values, { abortEarly: true })
|
||||
.then((validatedProxy) => {
|
||||
dispatch(
|
||||
savePreferences({
|
||||
...preferences,
|
||||
proxy: validatedProxy
|
||||
})
|
||||
).then(() => {
|
||||
close();
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
let errMsg = error.message || 'Preferences validation error';
|
||||
toast.error(errMsg);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
formik.setValues({
|
||||
enabled: preferences.proxy.enabled || false,
|
||||
protocol: preferences.proxy.protocol || 'http',
|
||||
hostname: preferences.proxy.hostname || '',
|
||||
port: preferences.proxy.port || '',
|
||||
auth: {
|
||||
enabled: preferences.proxy.auth ? preferences.proxy.auth.enabled || false : false,
|
||||
username: preferences.proxy.auth ? preferences.proxy.auth.username || '' : '',
|
||||
password: preferences.proxy.auth ? preferences.proxy.auth.password || '' : ''
|
||||
},
|
||||
bypassProxy: preferences.proxy.bypassProxy || ''
|
||||
});
|
||||
}, [preferences]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<h1 className="font-medium mb-3">Global Proxy Settings</h1>
|
||||
<form className="bruno-form" onSubmit={formik.handleSubmit}>
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label" htmlFor="enabled">
|
||||
Enabled
|
||||
</label>
|
||||
<input type="checkbox" name="enabled" checked={formik.values.enabled} onChange={formik.handleChange} />
|
||||
</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">
|
||||
<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="socks5"
|
||||
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>
|
||||
<input
|
||||
id="auth.password"
|
||||
type="text"
|
||||
name="auth.password"
|
||||
className="block textbox"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={formik.values.auth.password}
|
||||
onChange={formik.handleChange}
|
||||
/>
|
||||
{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="mt-6">
|
||||
<button type="submit" className="submit btn btn-md btn-secondary">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProxySettings;
|
||||
@@ -3,7 +3,9 @@ import classnames from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
import Support from './Support';
|
||||
import General from './General';
|
||||
import Font from './Font';
|
||||
import Theme from './Theme';
|
||||
import Proxy from './ProxySettings';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Preferences = ({ onClose }) => {
|
||||
@@ -18,16 +20,24 @@ const Preferences = ({ onClose }) => {
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'general': {
|
||||
return <General />;
|
||||
return <General close={onClose} />;
|
||||
}
|
||||
|
||||
case 'proxy': {
|
||||
return <Proxy close={onClose} />;
|
||||
}
|
||||
|
||||
case 'theme': {
|
||||
return <Theme />;
|
||||
return <Theme close={onClose} />;
|
||||
}
|
||||
|
||||
case 'support': {
|
||||
return <Support />;
|
||||
}
|
||||
|
||||
case 'font': {
|
||||
return <Font close={onClose} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -41,6 +51,12 @@ const Preferences = ({ onClose }) => {
|
||||
<div className={getTabClassname('theme')} role="tab" onClick={() => setTab('theme')}>
|
||||
Theme
|
||||
</div>
|
||||
<div className={getTabClassname('font')} role="tab" onClick={() => setTab('font')}>
|
||||
Font
|
||||
</div>
|
||||
<div className={getTabClassname('proxy')} role="tab" onClick={() => setTab('proxy')}>
|
||||
Proxy
|
||||
</div>
|
||||
<div className={getTabClassname('support')} role="tab" onClick={() => setTab('support')}>
|
||||
Support
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import darkTheme from 'themes/dark';
|
||||
import lightTheme from 'themes/light';
|
||||
|
||||
/**
|
||||
* Assertion operators
|
||||
@@ -76,10 +79,16 @@ const AssertionOperator = ({ operator, onChange }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<select value={operator} onChange={handleChange} className="mousetrap">
|
||||
{operators.map((operator) => (
|
||||
<option key={operator} value={operator}>
|
||||
<option
|
||||
style={{ backgroundColor: storedTheme === 'dark' ? darkTheme.bg : lightTheme.bg }}
|
||||
key={operator}
|
||||
value={operator}
|
||||
>
|
||||
{getLabel(operator)}
|
||||
</option>
|
||||
))}
|
||||
|
||||
@@ -178,7 +178,7 @@ const AssertionRow = ({
|
||||
handleAssertionChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
value: `${operator} ${newValue}`
|
||||
}
|
||||
},
|
||||
assertion,
|
||||
@@ -197,10 +197,11 @@ const AssertionRow = ({
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={assertion.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleAssertionChange(e, assertion, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveAssertion(assertion)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveAssertion(assertion)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.auth-mode-selector {
|
||||
background: transparent;
|
||||
|
||||
.auth-mode-label {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateRequestAuthMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestAuthMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const AuthMode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center auth-mode-label select-none">
|
||||
{humanizeRequestAuthMode(authMode)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onModeChange = (value) => {
|
||||
dispatch(
|
||||
updateRequestAuthMode({
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
mode: value
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
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
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('basic');
|
||||
}}
|
||||
>
|
||||
Basic Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('bearer');
|
||||
}}
|
||||
>
|
||||
Bearer Token
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('none');
|
||||
}}
|
||||
>
|
||||
No Auth
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default AuthMode;
|
||||
@@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,205 @@
|
||||
import React, { useState } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { update } from 'lodash';
|
||||
|
||||
const AwsV4Auth = ({ onTokenChange, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const awsv4Auth = item.draft ? get(item, 'draft.request.auth.awsv4', {}) : get(item, 'request.auth.awsv4', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleAccessKeyIdChange = (accessKeyId) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSecretAccessKeyChange = (secretAccessKey) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleSessionTokenChange = (sessionToken) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleServiceChange = (service) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRegionChange = (region) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: region,
|
||||
profileName: awsv4Auth.profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleProfileNameChange = (profileName) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'awsv4',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
accessKeyId: awsv4Auth.accessKeyId,
|
||||
secretAccessKey: awsv4Auth.secretAccessKey,
|
||||
sessionToken: awsv4Auth.sessionToken,
|
||||
service: awsv4Auth.service,
|
||||
region: awsv4Auth.region,
|
||||
profileName: profileName
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
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">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.accessKeyId || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleAccessKeyIdChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Secret Access Key</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.secretAccessKey || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleSecretAccessKeyChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Session Token</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.sessionToken || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleSessionTokenChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Service</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.service || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleServiceChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Region</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.region || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleRegionChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Profile Name</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={awsv4Auth.profileName || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleProfileNameChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AwsV4Auth;
|
||||
@@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BasicAuth = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const basicAuth = item.draft ? get(item, 'draft.request.auth.basic', {}) : get(item, 'request.auth.basic', {});
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'basic',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: basicAuth.password
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (password) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'basic',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
username: basicAuth.username,
|
||||
password: password
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Username</label>
|
||||
<div className="single-line-editor-wrapper mb-2">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.username || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleUsernameChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Password</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={basicAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicAuth;
|
||||
@@ -0,0 +1,16 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const BearerAuth = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const bearerToken = item.draft
|
||||
? get(item, 'draft.request.auth.bearer.token')
|
||||
: get(item, 'request.auth.bearer.token');
|
||||
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
const handleTokenChange = (token) => {
|
||||
dispatch(
|
||||
updateAuth({
|
||||
mode: 'bearer',
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
content: {
|
||||
token: token
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<label className="block font-medium mb-2">Token</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={bearerToken}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleTokenChange(val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default BearerAuth;
|
||||
@@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div``;
|
||||
|
||||
export default Wrapper;
|
||||
35
packages/bruno-app/src/components/RequestPane/Auth/index.js
Normal file
35
packages/bruno-app/src/components/RequestPane/Auth/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import AuthMode from './AuthMode';
|
||||
import AwsV4Auth from './AwsV4Auth';
|
||||
import BearerAuth from './BearerAuth';
|
||||
import BasicAuth from './BasicAuth';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Auth = ({ item, collection }) => {
|
||||
const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'awsv4': {
|
||||
return <AwsV4Auth collection={collection} item={item} />;
|
||||
}
|
||||
case 'basic': {
|
||||
return <BasicAuth collection={collection} item={item} />;
|
||||
}
|
||||
case 'bearer': {
|
||||
return <BearerAuth collection={collection} item={item} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full mt-1">
|
||||
<div className="flex flex-grow justify-start items-center">
|
||||
<AuthMode item={item} collection={collection} />
|
||||
</div>
|
||||
{getAuthView()}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default Auth;
|
||||
@@ -107,6 +107,7 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
'value'
|
||||
)
|
||||
}
|
||||
allowNewlines={true}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
@@ -116,10 +117,11 @@ const FormUrlEncodedParams = ({ item, collection }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveParams(param)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import get from 'lodash/get';
|
||||
import classnames from 'classnames';
|
||||
import { IconRefresh, IconLoader2, IconBook, IconDownload } from '@tabler/icons';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import QueryEditor from 'components/RequestPane/QueryEditor';
|
||||
import Auth from 'components/RequestPane/Auth';
|
||||
import GraphQLVariables from 'components/RequestPane/GraphQLVariables';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import Vars from 'components/RequestPane/Vars';
|
||||
@@ -15,9 +15,9 @@ import Tests from 'components/RequestPane/Tests';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import useGraphqlSchema from './useGraphqlSchema';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
import GraphQLSchemaActions from '../GraphQLSchemaActions/index';
|
||||
|
||||
const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -27,23 +27,11 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
const variables = item.draft
|
||||
? get(item, 'draft.request.body.graphql.variables')
|
||||
: get(item, 'request.body.graphql.variables');
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
|
||||
let { schema, loadSchema, isLoading: isSchemaLoading, error: schemaError } = useGraphqlSchema(url, environment);
|
||||
|
||||
const loadGqlSchema = () => {
|
||||
if (!isSchemaLoading) {
|
||||
loadSchema();
|
||||
}
|
||||
};
|
||||
const [schema, setSchema] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (onSchemaLoad) {
|
||||
onSchemaLoad(schema);
|
||||
}
|
||||
onSchemaLoad(schema);
|
||||
}, [schema]);
|
||||
|
||||
const onQueryChange = (value) => {
|
||||
@@ -90,6 +78,9 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
@@ -102,6 +93,9 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
case 'tests': {
|
||||
return <Tests item={item} collection={collection} />;
|
||||
}
|
||||
case 'docs': {
|
||||
return <Documentation item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
@@ -135,6 +129,9 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
@@ -147,18 +144,10 @@ const GraphQLRequestPane = ({ item, collection, leftPaneWidth, onSchemaLoad, tog
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
<div className="flex flex-grow justify-end items-center" style={{ fontSize: 13 }}>
|
||||
<div className="flex items-center cursor-pointer hover:underline" onClick={loadGqlSchema}>
|
||||
{isSchemaLoading ? <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} /> : null}
|
||||
{!isSchemaLoading && !schema ? <IconDownload size={18} strokeWidth={1.5} /> : null}
|
||||
{!isSchemaLoading && schema ? <IconRefresh size={18} strokeWidth={1.5} /> : null}
|
||||
<span className="ml-1">Schema</span>
|
||||
</div>
|
||||
<div className="flex items-center cursor-pointer hover:underline ml-2" onClick={toggleDocs}>
|
||||
<IconBook size={18} strokeWidth={1.5} />
|
||||
<span className="ml-1">Docs</span>
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
<GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />
|
||||
</div>
|
||||
<section className="flex w-full mt-5">{getTabPanel(focusedTab.requestPaneTab)}</section>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { buildClientSchema } from 'graphql';
|
||||
import { fetchGqlSchema } from 'utils/network';
|
||||
import { simpleHash } from 'utils/common';
|
||||
|
||||
const schemaHashPrefix = 'bruno.graphqlSchema';
|
||||
|
||||
const useGraphqlSchema = (endpoint, environment) => {
|
||||
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [schema, setSchema] = useState(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(localStorageKey);
|
||||
if (!saved) {
|
||||
return null;
|
||||
}
|
||||
return buildClientSchema(JSON.parse(saved));
|
||||
} catch {
|
||||
localStorage.setItem(localStorageKey, null);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const loadSchema = () => {
|
||||
setIsLoading(true);
|
||||
fetchGqlSchema(endpoint, environment)
|
||||
.then((res) => res.data)
|
||||
.then((s) => {
|
||||
if (s && s.data) {
|
||||
setSchema(buildClientSchema(s.data));
|
||||
setIsLoading(false);
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(s.data));
|
||||
toast.success('GraphQL Schema loaded successfully');
|
||||
} else {
|
||||
return Promise.reject(new Error('An error occurred while introspecting schema'));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setIsLoading(false);
|
||||
setError(err);
|
||||
toast.error('Error occurred while loading GraphQL Schema');
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
schema,
|
||||
loadSchema,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
export default useGraphqlSchema;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React, { useEffect, useRef, forwardRef } from 'react';
|
||||
import useGraphqlSchema from './useGraphqlSchema';
|
||||
import { IconBook, IconDownload, IconLoader2, IconRefresh } from '@tabler/icons';
|
||||
import get from 'lodash/get';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
import Dropdown from '../../Dropdown';
|
||||
|
||||
const GraphQLSchemaActions = ({ item, collection, onSchemaLoad, toggleDocs }) => {
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
const request = item.draft ? item.draft.request : item.request;
|
||||
|
||||
let {
|
||||
schema,
|
||||
schemaSource,
|
||||
loadSchema,
|
||||
isLoading: isSchemaLoading
|
||||
} = useGraphqlSchema(url, environment, request, collection);
|
||||
|
||||
useEffect(() => {
|
||||
if (onSchemaLoad) {
|
||||
onSchemaLoad(schema);
|
||||
}
|
||||
}, [schema]);
|
||||
|
||||
const schemaDropdownTippyRef = useRef();
|
||||
const onSchemaDropdownCreate = (ref) => (schemaDropdownTippyRef.current = ref);
|
||||
|
||||
const MenuIcon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="dropdown-icon cursor-pointer flex hover:underline ml-2">
|
||||
{isSchemaLoading && <IconLoader2 className="animate-spin" size={18} strokeWidth={1.5} />}
|
||||
{!isSchemaLoading && schema && <IconRefresh size={18} strokeWidth={1.5} />}
|
||||
{!isSchemaLoading && !schema && <IconDownload size={18} strokeWidth={1.5} />}
|
||||
<span className="ml-1">Schema</span>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-grow justify-end items-center" style={{ fontSize: 13 }}>
|
||||
<div className="flex items-center cursor-pointer hover:underline" onClick={toggleDocs}>
|
||||
<IconBook size={18} strokeWidth={1.5} />
|
||||
<span className="ml-1">Docs</span>
|
||||
</div>
|
||||
<Dropdown onCreate={onSchemaDropdownCreate} icon={<MenuIcon />} placement="bottom-start">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
schemaDropdownTippyRef.current.hide();
|
||||
loadSchema('introspection');
|
||||
}}
|
||||
>
|
||||
{schema && schemaSource === 'introspection' ? 'Refresh from Introspection' : 'Load from Introspection'}
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
schemaDropdownTippyRef.current.hide();
|
||||
loadSchema('file');
|
||||
}}
|
||||
>
|
||||
Load from File
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GraphQLSchemaActions;
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { buildClientSchema } from 'graphql';
|
||||
import { fetchGqlSchema } from 'utils/network';
|
||||
import { simpleHash } from 'utils/common';
|
||||
|
||||
const schemaHashPrefix = 'bruno.graphqlSchema';
|
||||
|
||||
const useGraphqlSchema = (endpoint, environment, request, collection) => {
|
||||
const { ipcRenderer } = window;
|
||||
const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;
|
||||
const [error, setError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [schemaSource, setSchemaSource] = useState('');
|
||||
const [schema, setSchema] = useState(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(localStorageKey);
|
||||
if (!saved) {
|
||||
return null;
|
||||
}
|
||||
return buildClientSchema(JSON.parse(saved));
|
||||
} catch {
|
||||
localStorage.setItem(localStorageKey, null);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const loadSchemaFromIntrospection = async () => {
|
||||
const response = await fetchGqlSchema(endpoint, environment, request, collection);
|
||||
if (!response) {
|
||||
throw new Error('Introspection query failed');
|
||||
}
|
||||
if (response.status !== 200) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
const data = response.data?.data;
|
||||
if (!data) {
|
||||
throw new Error('No data returned from introspection query');
|
||||
}
|
||||
setSchemaSource('introspection');
|
||||
return data;
|
||||
};
|
||||
|
||||
const loadSchemaFromFile = async () => {
|
||||
const schemaContent = await ipcRenderer.invoke('renderer:load-gql-schema-file');
|
||||
if (!schemaContent) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
setSchemaSource('file');
|
||||
return schemaContent.data;
|
||||
};
|
||||
|
||||
const loadSchema = async (schemaSource) => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
let data;
|
||||
if (schemaSource === 'file') {
|
||||
data = await loadSchemaFromFile();
|
||||
} else {
|
||||
// fallback to introspection if source is unknown
|
||||
data = await loadSchemaFromIntrospection();
|
||||
}
|
||||
setSchema(buildClientSchema(data));
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(data));
|
||||
toast.success('GraphQL Schema loaded successfully');
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
toast.error(`Error occurred while loading GraphQL Schema: ${err.message}`);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
schema,
|
||||
schemaSource,
|
||||
loadSchema,
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
export default useGraphqlSchema;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -10,6 +11,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
@@ -30,6 +32,7 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
collection={collection}
|
||||
value={variables || ''}
|
||||
theme={storedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
|
||||
@@ -7,11 +7,15 @@ import QueryParams from 'components/RequestPane/QueryParams';
|
||||
import RequestHeaders from 'components/RequestPane/RequestHeaders';
|
||||
import RequestBody from 'components/RequestPane/RequestBody';
|
||||
import RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';
|
||||
import Auth from 'components/RequestPane/Auth';
|
||||
import AuthMode from 'components/RequestPane/Auth/AuthMode';
|
||||
import Vars from 'components/RequestPane/Vars';
|
||||
import Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
import Tests from 'components/RequestPane/Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { get } from 'lodash';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
|
||||
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -38,6 +42,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
case 'headers': {
|
||||
return <RequestHeaders item={item} collection={collection} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth item={item} collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars item={item} collection={collection} />;
|
||||
}
|
||||
@@ -50,6 +57,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
case 'tests': {
|
||||
return <Tests item={item} collection={collection} />;
|
||||
}
|
||||
case 'docs': {
|
||||
return <Documentation item={item} collection={collection} />;
|
||||
}
|
||||
default: {
|
||||
return <div className="mt-4">404 | Not found</div>;
|
||||
}
|
||||
@@ -71,39 +81,65 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
});
|
||||
};
|
||||
|
||||
// get the length of active params, headers, asserts and vars
|
||||
const params = item.draft ? get(item, 'draft.request.params', []) : get(item, 'request.params', []);
|
||||
const headers = item.draft ? get(item, 'draft.request.headers', []) : get(item, 'request.headers', []);
|
||||
const assertions = item.draft ? get(item, 'draft.request.assertions', []) : get(item, 'request.assertions', []);
|
||||
const requestVars = item.draft ? get(item, 'draft.request.vars.req', []) : get(item, 'request.vars.req', []);
|
||||
const responseVars = item.draft ? get(item, 'draft.request.vars.res', []) : get(item, 'request.vars.res', []);
|
||||
|
||||
const activeParamsLength = params.filter((param) => param.enabled).length;
|
||||
const activeHeadersLength = headers.filter((header) => header.enabled).length;
|
||||
const activeAssertionsLength = assertions.filter((assertion) => assertion.enabled).length;
|
||||
const activeVarsLength =
|
||||
requestVars.filter((request) => request.enabled).length +
|
||||
responseVars.filter((response) => response.enabled).length;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('params')} role="tab" onClick={() => selectTab('params')}>
|
||||
Query
|
||||
{activeParamsLength > 0 && <sup className="ml-1 font-medium">{activeParamsLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
|
||||
Body
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => selectTab('headers')}>
|
||||
Headers
|
||||
{activeHeadersLength > 0 && <sup className="ml-1 font-medium">{activeHeadersLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>
|
||||
Auth
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => selectTab('vars')}>
|
||||
Vars
|
||||
{activeVarsLength > 0 && <sup className="ml-1 font-medium">{activeVarsLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
</div>
|
||||
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
|
||||
Assert
|
||||
{activeAssertionsLength > 0 && <sup className="ml-1 font-medium">{activeAssertionsLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
</div>
|
||||
{/* Moved to post mvp */}
|
||||
{/* <div className={getTabClassname('auth')} role="tab" onClick={() => selectTab('auth')}>Auth</div> */}
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
</div>
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
<RequestBodyMode item={item} collection={collection} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<section className={`flex w-full ${['script', 'vars'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'}`}>
|
||||
<section
|
||||
className={`flex w-full ${
|
||||
['script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'
|
||||
}`}
|
||||
>
|
||||
{getTabPanel(focusedTab.requestPaneTab)}
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -116,10 +116,11 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveParams(param)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveParams(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -115,10 +115,11 @@ const QueryParams = ({ item, collection }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveParam(param)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveParam(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,50 @@ const Wrapper = styled.div`
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tooltip:hover .tooltiptext {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tooltiptext {
|
||||
visibility: hidden;
|
||||
width: auto;
|
||||
background-color: ${(props) => props.theme.requestTabs.active.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 34px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tooltiptext::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
border-width: 4px;
|
||||
border-style: solid;
|
||||
border-color: ${(props) => props.theme.requestTabs.active.bg} transparent transparent transparent;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -5,8 +5,9 @@ import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/sli
|
||||
import { saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import HttpMethodSelector from './HttpMethodSelector';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import SendIcon from 'components/Icons/Send';
|
||||
import { IconDeviceFloppy, IconArrowRight } from '@tabler/icons';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { isMacOS } from 'utils/common/platform';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
@@ -14,6 +15,8 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
const dispatch = useDispatch();
|
||||
const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');
|
||||
const url = item.draft ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
const isMac = isMacOS();
|
||||
const saveShortcut = isMac ? 'Cmd + S' : 'Ctrl + S';
|
||||
|
||||
const [methodSelectorWidth, setMethodSelectorWidth] = useState(90);
|
||||
|
||||
@@ -22,7 +25,10 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
setMethodSelectorWidth(el.offsetWidth);
|
||||
}, [method]);
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const onSave = () => {
|
||||
dispatch(saveRequest(item.uid, collection.uid));
|
||||
};
|
||||
|
||||
const onUrlChange = (value) => {
|
||||
dispatch(
|
||||
requestUrlChanged({
|
||||
@@ -65,7 +71,25 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
collection={collection}
|
||||
/>
|
||||
<div className="flex items-center h-full mr-2 cursor-pointer" id="send-request" onClick={handleRun}>
|
||||
<SendIcon color={theme.requestTabPanel.url.icon} width={22} />
|
||||
<div
|
||||
className="tooltip mr-3"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!item.draft) return;
|
||||
onSave();
|
||||
}}
|
||||
>
|
||||
<IconDeviceFloppy
|
||||
color={item.draft ? theme.colors.text.yellow : theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
className={`${item.draft ? 'cursor-pointer' : 'cursor-default'}`}
|
||||
/>
|
||||
<span className="tooltiptext text-xs">
|
||||
Save <span className="shortcut">({saveShortcut})</span>
|
||||
</span>
|
||||
</div>
|
||||
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} />
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -4,7 +4,7 @@ const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.body-mode-selector {
|
||||
background: ${(props) => props.theme.requestTabPanel.bodyModeSelect.color};
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
|
||||
.dropdown-item {
|
||||
@@ -15,6 +15,10 @@ const Wrapper = styled.div`
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.selected-body-mode {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
|
||||
@@ -6,16 +6,19 @@ import { useDispatch } from 'react-redux';
|
||||
import { updateRequestBodyMode } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeRequestBodyMode } from 'utils/collections';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { toastError } from 'utils/common/error';
|
||||
|
||||
const RequestBodyMode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
const bodyMode = item.draft ? get(item, 'draft.request.body.mode') : get(item, 'request.body.mode');
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
const bodyMode = body?.mode;
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none">
|
||||
<div ref={ref} className="flex items-center justify-center pl-3 py-1 select-none selected-body-mode">
|
||||
{humanizeRequestBodyMode(bodyMode)} <IconCaretDown className="caret ml-2 mr-2" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
@@ -31,6 +34,24 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onPrettify = () => {
|
||||
if (body?.json && bodyMode === 'json') {
|
||||
try {
|
||||
const bodyJson = JSON.parse(body.json);
|
||||
const prettyBodyJson = JSON.stringify(bodyJson, null, 2);
|
||||
dispatch(
|
||||
updateRequestBody({
|
||||
content: prettyBodyJson,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
toastError(new Error('Unable to prettify. Invalid JSON format.'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="inline-flex items-center cursor-pointer body-mode-selector">
|
||||
@@ -82,6 +103,15 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
>
|
||||
TEXT
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('sparql');
|
||||
}}
|
||||
>
|
||||
SPARQL
|
||||
</div>
|
||||
<div className="label-item font-medium">Other</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
@@ -94,6 +124,11 @@ const RequestBodyMode = ({ item, collection }) => {
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
{bodyMode === 'json' && (
|
||||
<button className="ml-1" onClick={onPrettify}>
|
||||
Prettify
|
||||
</button>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import get from 'lodash/get';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import FormUrlEncodedParams from 'components/RequestPane/FormUrlEncodedParams';
|
||||
import MultipartFormParams from 'components/RequestPane/MultipartFormParams';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { updateRequestBody } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -14,6 +14,7 @@ const RequestBody = ({ item, collection }) => {
|
||||
const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');
|
||||
const bodyMode = item.draft ? get(item, 'draft.request.body.mode') : get(item, 'request.body.mode');
|
||||
const { storedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
@@ -28,17 +29,19 @@ const RequestBody = ({ item, collection }) => {
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
if (['json', 'xml', 'text'].includes(bodyMode)) {
|
||||
if (['json', 'xml', 'text', 'sparql'].includes(bodyMode)) {
|
||||
let codeMirrorMode = {
|
||||
json: 'application/ld+json',
|
||||
text: 'application/text',
|
||||
xml: 'application/xml'
|
||||
xml: 'application/xml',
|
||||
sparql: 'application/sparql-query'
|
||||
};
|
||||
|
||||
let bodyContent = {
|
||||
json: body.json,
|
||||
text: body.text,
|
||||
xml: body.xml
|
||||
xml: body.xml,
|
||||
sparql: body.sparql
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -46,6 +49,7 @@ const RequestBody = ({ item, collection }) => {
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={storedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
value={bodyContent[bodyMode] || ''}
|
||||
onEdit={onEdit}
|
||||
onRun={onRun}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { addRequestHeader, updateRequestHeader, deleteRequestHeader } from 'prov
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { headers as StandardHTTPHeaders } from 'know-your-http-well';
|
||||
const headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);
|
||||
|
||||
const RequestHeaders = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -91,6 +93,7 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
'name'
|
||||
)
|
||||
}
|
||||
autocomplete={headerAutoCompleteList}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
@@ -120,10 +123,11 @@ const RequestHeaders = ({ item, collection }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={header.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleHeaderValueChange(e, header, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveHeader(header)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveHeader(header)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -13,6 +13,7 @@ const Script = ({ item, collection }) => {
|
||||
const responseScript = item.draft ? get(item, 'draft.request.script.res') : get(item, 'request.script.res');
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
@@ -45,6 +46,7 @@ const Script = ({ item, collection }) => {
|
||||
collection={collection}
|
||||
value={requestScript || ''}
|
||||
theme={storedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
onEdit={onRequestScriptEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
@@ -57,6 +59,7 @@ const Script = ({ item, collection }) => {
|
||||
collection={collection}
|
||||
value={responseScript || ''}
|
||||
theme={storedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
onEdit={onResponseScriptEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import { updateRequestTests } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
@@ -12,6 +12,7 @@ const Tests = ({ item, collection }) => {
|
||||
const tests = item.draft ? get(item, 'draft.request.tests') : get(item, 'request.tests');
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
@@ -32,6 +33,7 @@ const Tests = ({ item, collection }) => {
|
||||
collection={collection}
|
||||
value={tests || ''}
|
||||
theme={storedTheme}
|
||||
font={get(preferences, 'font.codeFont', 'default')}
|
||||
onEdit={onEdit}
|
||||
mode="javascript"
|
||||
onRun={onRun}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collection
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import Tooltip from 'components/Tooltip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -29,7 +30,19 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
const _var = cloneDeep(v);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
_var.name = e.target.value;
|
||||
const value = e.target.value;
|
||||
|
||||
if (/^(?!\d).*$/.test(value) === false) {
|
||||
toast.error('Variable names must not start with a number!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^\w*$/.test(value) === false) {
|
||||
toast.error('Variable contains invalid character! Variables must only contain alpha-numeric characters.');
|
||||
return;
|
||||
}
|
||||
|
||||
_var.name = value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
@@ -88,7 +101,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
</thead>
|
||||
<tbody>
|
||||
{vars && vars.length
|
||||
? vars.map((_var, index) => {
|
||||
? vars.map((_var) => {
|
||||
return (
|
||||
<tr key={_var.uid}>
|
||||
<td>
|
||||
@@ -128,10 +141,11 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={_var.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'enabled')}
|
||||
/>
|
||||
<button onClick={() => handleRemoveVar(_var)}>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveVar(_var)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,42 +1,12 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Vars = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = item.draft ? get(item, 'draft.request.vars.req') : get(item, 'request.vars.req');
|
||||
const responseVars = item.draft ? get(item, 'draft.request.vars.res') : get(item, 'request.vars.res');
|
||||
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const onRequestScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onResponseScriptEdit = (value) => {
|
||||
dispatch(
|
||||
updateResponseScript({
|
||||
script: value,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user