mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-03 01:18:32 +00:00
Compare commits
663 Commits
v1.34.1
...
feature/pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a006fe8230 | ||
|
|
577d54b432 | ||
|
|
afaebf6b3d | ||
|
|
6e89001825 | ||
|
|
e7dd78ea53 | ||
|
|
9ad0f2d169 | ||
|
|
bf19645282 | ||
|
|
bb01199877 | ||
|
|
5627c5624f | ||
|
|
8e23a7054f | ||
|
|
d820069371 | ||
|
|
2de9b87c6f | ||
|
|
178773d63a | ||
|
|
7994946c85 | ||
|
|
b020255269 | ||
|
|
73b0f0919d | ||
|
|
8975b9eef6 | ||
|
|
865e813b42 | ||
|
|
51f36b1903 | ||
|
|
6b122d7262 | ||
|
|
a8e5ce9c13 | ||
|
|
8ac916b0ff | ||
|
|
8d860a051c | ||
|
|
4ac2c4ac34 | ||
|
|
7c27193983 | ||
|
|
2c3d2ff6a7 | ||
|
|
a4fff01647 | ||
|
|
2cd985faf7 | ||
|
|
9a35302d4b | ||
|
|
553f7675f2 | ||
|
|
b299879b82 | ||
|
|
3696562414 | ||
|
|
e02c6c274b | ||
|
|
ab0a4b8140 | ||
|
|
1b268ae9db | ||
|
|
8debb9fd11 | ||
|
|
7c07488e16 | ||
|
|
6073a9e2c3 | ||
|
|
9c652f86c9 | ||
|
|
3c0090d86f | ||
|
|
9132755d49 | ||
|
|
2a44691cb3 | ||
|
|
0d8a696498 | ||
|
|
bfa2706598 | ||
|
|
5fdb52388a | ||
|
|
799dc9a1ca | ||
|
|
2bb56e8a4b | ||
|
|
084d2bf692 | ||
|
|
10640c7561 | ||
|
|
9f044c48fe | ||
|
|
79f4e69a05 | ||
|
|
f13148af3d | ||
|
|
d2eb2d2941 | ||
|
|
942c0ee113 | ||
|
|
fbd3a38587 | ||
|
|
45b660985e | ||
|
|
0888125899 | ||
|
|
c85d9bcd84 | ||
|
|
dbf8af1146 | ||
|
|
d7ccf1454e | ||
|
|
652d447f8b | ||
|
|
2f58379feb | ||
|
|
c14d3f4274 | ||
|
|
d4673a2f07 | ||
|
|
3a0c94577f | ||
|
|
5a4e33e503 | ||
|
|
5649799167 | ||
|
|
c407b73c22 | ||
|
|
361add3385 | ||
|
|
9d6ab69d37 | ||
|
|
0f6da35c0b | ||
|
|
b699088dd6 | ||
|
|
458c070004 | ||
|
|
babac6df3c | ||
|
|
f58477931f | ||
|
|
2171d491a6 | ||
|
|
aa911f88f2 | ||
|
|
bdbcaeff67 | ||
|
|
b2756b3c63 | ||
|
|
27f11ab583 | ||
|
|
2776970317 | ||
|
|
9d28bf7e82 | ||
|
|
6455b00742 | ||
|
|
16179a3b50 | ||
|
|
6a37c9d076 | ||
|
|
1915b1c00a | ||
|
|
a9982d6e28 | ||
|
|
1daeb8fe93 | ||
|
|
3dfb158382 | ||
|
|
fb7d247fa7 | ||
|
|
6bf2312a94 | ||
|
|
0cdcb83a7a | ||
|
|
e4f48e81fc | ||
|
|
1d32a95a09 | ||
|
|
4c934a78a6 | ||
|
|
c47bc86d37 | ||
|
|
a125781312 | ||
|
|
dfa951e574 | ||
|
|
76779e6f95 | ||
|
|
e9a79a32da | ||
|
|
967170a7b2 | ||
|
|
3326784315 | ||
|
|
fc553e1009 | ||
|
|
da172ff9b5 | ||
|
|
fc422853ef | ||
|
|
2852c07ec7 | ||
|
|
ead1c9ecab | ||
|
|
5b5066577f | ||
|
|
4af0bb3943 | ||
|
|
f2eaa79318 | ||
|
|
2ee7ce5829 | ||
|
|
0d7c94e7e9 | ||
|
|
9e29821012 | ||
|
|
38c307d6f1 | ||
|
|
520567793a | ||
|
|
e0fb379511 | ||
|
|
ba9362ccb2 | ||
|
|
261a36c435 | ||
|
|
cb92e46f8d | ||
|
|
126186041e | ||
|
|
6379e1703c | ||
|
|
2b246e431b | ||
|
|
526fcabffe | ||
|
|
75ff31f0cf | ||
|
|
46dab6e474 | ||
|
|
c7e8c07d40 | ||
|
|
932d2b77dc | ||
|
|
049de84cbb | ||
|
|
3bd44ea7ef | ||
|
|
317ccccfc6 | ||
|
|
220da6b58e | ||
|
|
6a7750d354 | ||
|
|
529803f791 | ||
|
|
4c23ab5664 | ||
|
|
e3c28fd0ec | ||
|
|
56ab61c29c | ||
|
|
d3056ba843 | ||
|
|
e34e2ec1f1 | ||
|
|
524bb5e4b7 | ||
|
|
3f8ea7764e | ||
|
|
f0d1e6936e | ||
|
|
9a21eec1b9 | ||
|
|
1703346bb6 | ||
|
|
b93d8e73a2 | ||
|
|
17c9813c98 | ||
|
|
e5ebe20a20 | ||
|
|
54a03fd0d3 | ||
|
|
e8affcfde9 | ||
|
|
d376947a91 | ||
|
|
59e38fbdb0 | ||
|
|
492449b7c5 | ||
|
|
7cd21636d6 | ||
|
|
6ff49589be | ||
|
|
c950806541 | ||
|
|
3dcc12042f | ||
|
|
92925648e6 | ||
|
|
811c492bce | ||
|
|
73fa2e19e4 | ||
|
|
921e1af72b | ||
|
|
cc905da630 | ||
|
|
74bbfce8a0 | ||
|
|
8b67a0423d | ||
|
|
f1d527aa9c | ||
|
|
9e45d4d227 | ||
|
|
2dd0424d8f | ||
|
|
838f25b9db | ||
|
|
8809469f8e | ||
|
|
289f138c2a | ||
|
|
3d0dd60f56 | ||
|
|
9bb9a914ac | ||
|
|
44cef9999c | ||
|
|
3a792a021c | ||
|
|
2e5c63cfb9 | ||
|
|
9845363349 | ||
|
|
1a6fa7a799 | ||
|
|
6cd44662a8 | ||
|
|
9daf418886 | ||
|
|
37ee13353d | ||
|
|
8439e8871f | ||
|
|
4c1d3b4f3a | ||
|
|
cd3c66cb14 | ||
|
|
265b0114e4 | ||
|
|
17a63d599d | ||
|
|
d9e87fcd82 | ||
|
|
78c4cb11eb | ||
|
|
6feea75e45 | ||
|
|
2d1f7d0f33 | ||
|
|
841facc853 | ||
|
|
0e60bd3da7 | ||
|
|
5dc7f1ae2f | ||
|
|
6862cb4e58 | ||
|
|
0591530d44 | ||
|
|
592679538b | ||
|
|
9ef2699372 | ||
|
|
e4c37b916a | ||
|
|
7a8a0ae37e | ||
|
|
bd25097e44 | ||
|
|
3f140e818f | ||
|
|
dbba22131c | ||
|
|
8908828af0 | ||
|
|
20c9e1d406 | ||
|
|
741576526d | ||
|
|
b928ec112e | ||
|
|
559026b65c | ||
|
|
eb6fef63b3 | ||
|
|
1b767f8d26 | ||
|
|
3b061cda89 | ||
|
|
e0d7bb50f3 | ||
|
|
20e8e9167f | ||
|
|
268ede869d | ||
|
|
8b8ddaf31b | ||
|
|
f16fbeade7 | ||
|
|
d27677030d | ||
|
|
578e912faf | ||
|
|
bd8c6caa39 | ||
|
|
7635230c88 | ||
|
|
eb0d746082 | ||
|
|
61b3853390 | ||
|
|
76485cdd56 | ||
|
|
3d5401a8db | ||
|
|
6faecc2874 | ||
|
|
4650ca40c1 | ||
|
|
14ee26557a | ||
|
|
8e873013a9 | ||
|
|
b0caf46406 | ||
|
|
a0926c4064 | ||
|
|
b795b1c5ce | ||
|
|
61ba5f5c39 | ||
|
|
f177287fb6 | ||
|
|
ef929e78f3 | ||
|
|
274a55e257 | ||
|
|
9e24223085 | ||
|
|
3cd3d8bdcc | ||
|
|
eed3f2ff4c | ||
|
|
b09b4b1d17 | ||
|
|
cd9c667e8a | ||
|
|
2675e79dbd | ||
|
|
ef94f55c82 | ||
|
|
926919524b | ||
|
|
087f691544 | ||
|
|
3a81ebf0e2 | ||
|
|
3ffaaab8f3 | ||
|
|
5a98da2031 | ||
|
|
2e4014863f | ||
|
|
ccd4a14da6 | ||
|
|
98bd997665 | ||
|
|
a7cf24278e | ||
|
|
039c157f33 | ||
|
|
1009d42f92 | ||
|
|
1be0e8d31c | ||
|
|
ab9befd773 | ||
|
|
7506f83800 | ||
|
|
74d9b0aafe | ||
|
|
d3fcb42a8f | ||
|
|
3808089e60 | ||
|
|
cd2f5d5233 | ||
|
|
51be153527 | ||
|
|
5728b7c8a8 | ||
|
|
71b6907c31 | ||
|
|
eead96ca26 | ||
|
|
f99e8770f0 | ||
|
|
7ae33d05c9 | ||
|
|
df196f5d25 | ||
|
|
1f8a10d1df | ||
|
|
ccb951dadd | ||
|
|
9bde3c44f7 | ||
|
|
5ac52a531f | ||
|
|
51e60d5083 | ||
|
|
01b982a0e7 | ||
|
|
d0b16841c9 | ||
|
|
59d7141f70 | ||
|
|
11c14530eb | ||
|
|
0fb926648b | ||
|
|
d37cf28e10 | ||
|
|
f8a14e35fa | ||
|
|
eefb0f836b | ||
|
|
989e553648 | ||
|
|
cd1d4f09d2 | ||
|
|
122e0a1d02 | ||
|
|
7e16304426 | ||
|
|
0888219e95 | ||
|
|
d89fd455ff | ||
|
|
df4a682f97 | ||
|
|
2385c4d5c1 | ||
|
|
243398bcd0 | ||
|
|
2a2f2dfa15 | ||
|
|
d57634e6ea | ||
|
|
1be0b97895 | ||
|
|
6a85635c49 | ||
|
|
0fbbe8a996 | ||
|
|
4ff4e3b732 | ||
|
|
7c65317b07 | ||
|
|
b57c996564 | ||
|
|
5de75892a2 | ||
|
|
d5e828aef2 | ||
|
|
51c86bc0e9 | ||
|
|
253cb8b315 | ||
|
|
0876ad0dab | ||
|
|
a1c133b303 | ||
|
|
38cf206075 | ||
|
|
9d598db55e | ||
|
|
f6ab59ceda | ||
|
|
8cda05c431 | ||
|
|
7af7ff92bf | ||
|
|
3169e6cdf4 | ||
|
|
f1004e2e36 | ||
|
|
233c57e625 | ||
|
|
b399576dab | ||
|
|
655eec09c1 | ||
|
|
51eda3f08c | ||
|
|
a438c06b97 | ||
|
|
4977dbeb11 | ||
|
|
7cacc255b4 | ||
|
|
b28b60d4a7 | ||
|
|
2fc45de430 | ||
|
|
c58604716e | ||
|
|
31409c6206 | ||
|
|
4e88cbf318 | ||
|
|
dfb0b1b966 | ||
|
|
f8b4a0b85b | ||
|
|
200732bac5 | ||
|
|
c997924c42 | ||
|
|
711eb24c01 | ||
|
|
3b8a613914 | ||
|
|
81930f6fe6 | ||
|
|
e0750148e6 | ||
|
|
528f822294 | ||
|
|
31c11830a6 | ||
|
|
eff3b29bfb | ||
|
|
4656958f2f | ||
|
|
aa4575b0ea | ||
|
|
14d798eea7 | ||
|
|
8af285d41e | ||
|
|
6a0eb1ccde | ||
|
|
4a613ed1b7 | ||
|
|
7566d658d4 | ||
|
|
413b121ce1 | ||
|
|
90dff3d1e1 | ||
|
|
3fc0b0a668 | ||
|
|
70ee819bae | ||
|
|
8810b9e291 | ||
|
|
89a4cd62bc | ||
|
|
b5e53ec25c | ||
|
|
4c1765e9f9 | ||
|
|
8351589994 | ||
|
|
4d063b2d0b | ||
|
|
288628003f | ||
|
|
ced6ddfab5 | ||
|
|
5291bbaef7 | ||
|
|
26eaec4c72 | ||
|
|
667b15386c | ||
|
|
086e943c04 | ||
|
|
901c4e1ca2 | ||
|
|
14e9227e07 | ||
|
|
d8a2e6f405 | ||
|
|
5c096cff7b | ||
|
|
0913e668e0 | ||
|
|
f0c3125227 | ||
|
|
a5d23599a1 | ||
|
|
28ca0494e0 | ||
|
|
722d9788ca | ||
|
|
01a62d66cc | ||
|
|
f668e93f52 | ||
|
|
038f2d1f0b | ||
|
|
8f604efc7e | ||
|
|
af182a9c00 | ||
|
|
dd23962958 | ||
|
|
324b7cec51 | ||
|
|
8abf8ff9c8 | ||
|
|
ec5a5c9b56 | ||
|
|
d0419edb92 | ||
|
|
4598acd068 | ||
|
|
062ab00a66 | ||
|
|
650cb47a8b | ||
|
|
08139a8f3e | ||
|
|
51e087efba | ||
|
|
ff5683f19f | ||
|
|
d13e4b3b54 | ||
|
|
2d08567d8d | ||
|
|
b51f8109a2 | ||
|
|
8f241a32ae | ||
|
|
b7fda331dc | ||
|
|
10e0fde2a8 | ||
|
|
9f5f975f70 | ||
|
|
b5bd259a1b | ||
|
|
0633d45a10 | ||
|
|
16e27d2ca4 | ||
|
|
956d5a38e9 | ||
|
|
bfc1101371 | ||
|
|
05be59f00c | ||
|
|
c5eeb190d3 | ||
|
|
1d1e701ccb | ||
|
|
f38c7ae03a | ||
|
|
8f754142c7 | ||
|
|
fee631d496 | ||
|
|
d03de2b622 | ||
|
|
31b2818821 | ||
|
|
8a71dfc022 | ||
|
|
3e6204e49b | ||
|
|
dab4bb6a1c | ||
|
|
3c8cb702f5 | ||
|
|
2df7fd6588 | ||
|
|
e5d7cd1be9 | ||
|
|
2bce9b3716 | ||
|
|
5bfcc9b6e7 | ||
|
|
472b5452f7 | ||
|
|
5b04e0c189 | ||
|
|
3da12a05db | ||
|
|
a73d2a02cf | ||
|
|
63d3cb380d | ||
|
|
10a5935a12 | ||
|
|
cf2cb0736e | ||
|
|
6abd063749 | ||
|
|
dbf1cad124 | ||
|
|
00c5298b7d | ||
|
|
27ef28ae9b | ||
|
|
abb0a7b0db | ||
|
|
d2d7638a54 | ||
|
|
5500070b49 | ||
|
|
72b8c547b2 | ||
|
|
15b870996d | ||
|
|
3cb15fc001 | ||
|
|
96d6bf1664 | ||
|
|
f5ff40abfa | ||
|
|
c5de2343e9 | ||
|
|
39e8b66135 | ||
|
|
9f9294d161 | ||
|
|
5f63cc4ab4 | ||
|
|
539d22125c | ||
|
|
36343b30b3 | ||
|
|
b5ae2b2b45 | ||
|
|
704977f20f | ||
|
|
c852257bda | ||
|
|
55eac64ca5 | ||
|
|
52672e67a2 | ||
|
|
d32f987bc6 | ||
|
|
5fe9208089 | ||
|
|
14ecc09cde | ||
|
|
949bf539b8 | ||
|
|
c4be6a88e4 | ||
|
|
99302e3a1d | ||
|
|
1d3cbd2335 | ||
|
|
218d6527df | ||
|
|
42ada4a364 | ||
|
|
548f958a0f | ||
|
|
005eb273bf | ||
|
|
343e6dae47 | ||
|
|
7b86febc87 | ||
|
|
ca5fbea7b6 | ||
|
|
f34711c6e0 | ||
|
|
cd722a2bd9 | ||
|
|
75a9959d47 | ||
|
|
754a15dd58 | ||
|
|
2a6f6704c3 | ||
|
|
7d67239b11 | ||
|
|
e0ab274452 | ||
|
|
776afbd28a | ||
|
|
ca8f96fba0 | ||
|
|
5c1ab647fc | ||
|
|
83e63e749e | ||
|
|
9d94ad9b73 | ||
|
|
395fb188fe | ||
|
|
f09fd19ca0 | ||
|
|
f578c188fb | ||
|
|
0d2b449b27 | ||
|
|
e897dc1eb0 | ||
|
|
85b6cae03d | ||
|
|
d215cf740b | ||
|
|
4b277aa874 | ||
|
|
e9378d7895 | ||
|
|
78aa0d07ae | ||
|
|
e4574e3a56 | ||
|
|
795cd196f2 | ||
|
|
7b935bd206 | ||
|
|
43e892f9b0 | ||
|
|
1f2bee1f90 | ||
|
|
767db75730 | ||
|
|
b6b4b7362f | ||
|
|
54d8fbc478 | ||
|
|
9a2d8bfff3 | ||
|
|
907f6a19ad | ||
|
|
b612da4f3c | ||
|
|
625140d1f4 | ||
|
|
284519cd43 | ||
|
|
26daee5d98 | ||
|
|
87988b6879 | ||
|
|
f8711a91d9 | ||
|
|
582e8e5eac | ||
|
|
236bc48d98 | ||
|
|
a63afd6c0b | ||
|
|
fec99f0780 | ||
|
|
bf142af6d9 | ||
|
|
57a85e535c | ||
|
|
f72d643e02 | ||
|
|
aea25842ce | ||
|
|
0831b610cf | ||
|
|
f871bc0fa2 | ||
|
|
db90d31b3f | ||
|
|
e44dcad01a | ||
|
|
7a8d1624d1 | ||
|
|
0ad0af041b | ||
|
|
ad59e3f8d1 | ||
|
|
b9ec0acab4 | ||
|
|
0d126abfbd | ||
|
|
a5096ce413 | ||
|
|
3bf98aab3b | ||
|
|
9b83cd7b84 | ||
|
|
21f9e80706 | ||
|
|
e8bc32b39b | ||
|
|
380047e025 | ||
|
|
434ae6c70f | ||
|
|
22612a7dbe | ||
|
|
06c0b7c78a | ||
|
|
c154dec2b5 | ||
|
|
3abe611752 | ||
|
|
f3cfacdd43 | ||
|
|
af4b2105be | ||
|
|
7ae64605c2 | ||
|
|
83bbbe3fb3 | ||
|
|
73ea5f155d | ||
|
|
366bd99e92 | ||
|
|
6b6fc9a3dc | ||
|
|
6323b54c38 | ||
|
|
57e6af703c | ||
|
|
09120a96e8 | ||
|
|
f1e6d5eefe | ||
|
|
ba41f17439 | ||
|
|
d37e9aaafa | ||
|
|
5a9bda2a0f | ||
|
|
eaa4f4e57b | ||
|
|
8992a457a8 | ||
|
|
b181aba646 | ||
|
|
47179535d5 | ||
|
|
316b632338 | ||
|
|
dc469afeea | ||
|
|
ee715a6dc6 | ||
|
|
19ad0ecef7 | ||
|
|
993424a2b8 | ||
|
|
086c4c063e | ||
|
|
a6ac98b709 | ||
|
|
22ecd0284f | ||
|
|
33e86a9097 | ||
|
|
3efcdf254e | ||
|
|
ea1f385d1c | ||
|
|
4dcaaab52c | ||
|
|
6326dc3d9c | ||
|
|
a55ed9bd50 | ||
|
|
57d86eb118 | ||
|
|
85c6b2d97f | ||
|
|
0c574aeb1e | ||
|
|
3fe0d43bdc | ||
|
|
67ead9739e | ||
|
|
36021b5b38 | ||
|
|
1e45725ba1 | ||
|
|
d4616c78c8 | ||
|
|
5e5656d268 | ||
|
|
52e01935f5 | ||
|
|
99f912312d | ||
|
|
915ebf3387 | ||
|
|
bb18c532da | ||
|
|
4b4bd3bc95 | ||
|
|
482cb05d63 | ||
|
|
32153c4dbf | ||
|
|
7535b3d4ba | ||
|
|
4dd4800ee9 | ||
|
|
e1ebaabcc7 | ||
|
|
f110d898f5 | ||
|
|
5b6172e5ac | ||
|
|
fd22ff8962 | ||
|
|
5f5cc5eb22 | ||
|
|
eb6944a1c9 | ||
|
|
b4ea101350 | ||
|
|
0a8217e4ab | ||
|
|
514da55923 | ||
|
|
bd2cf554b6 | ||
|
|
2f752085f3 | ||
|
|
f2cfcab091 | ||
|
|
4283bb4bb0 | ||
|
|
707cddea90 | ||
|
|
39a44e9b4f | ||
|
|
fb8c54dd7a | ||
|
|
7400d890e4 | ||
|
|
3a29e2e333 | ||
|
|
1fb4298681 | ||
|
|
6d8cc38946 | ||
|
|
9d4246d74b | ||
|
|
1238bf7270 | ||
|
|
e9d459fa5e | ||
|
|
1c4acf7301 | ||
|
|
6385d00807 | ||
|
|
157389424d | ||
|
|
1b30229903 | ||
|
|
72bd1b4cbf | ||
|
|
4a4481a26f | ||
|
|
0bec17facd | ||
|
|
1c86b5f340 | ||
|
|
917205299a | ||
|
|
916f28633e | ||
|
|
6c2451b6f2 | ||
|
|
24563bdaaf | ||
|
|
28d30b1ef7 | ||
|
|
6442e3ceca | ||
|
|
56c3bf0899 | ||
|
|
11a3ea9fbb | ||
|
|
84095a4183 | ||
|
|
d92dd46d4e | ||
|
|
a752921413 | ||
|
|
44debfd9b9 | ||
|
|
278ca8bf29 | ||
|
|
23c22a96bc | ||
|
|
77d3fa7e1e | ||
|
|
412a0ed078 | ||
|
|
1cb0d4e191 | ||
|
|
aff7c405cd | ||
|
|
59108472a2 | ||
|
|
c4492b5d94 | ||
|
|
7fd7eafdcb | ||
|
|
41040bc296 | ||
|
|
ad5b625655 | ||
|
|
cd629451e4 | ||
|
|
dc77ee7c04 | ||
|
|
c322baa9c8 | ||
|
|
b206b70d2e | ||
|
|
9a325caeee | ||
|
|
d0ef70473d | ||
|
|
4894ac2754 | ||
|
|
df206dc4d9 | ||
|
|
45cc97ee20 | ||
|
|
642413e35c | ||
|
|
abb6490232 | ||
|
|
40001949b8 | ||
|
|
bdfe9c16f1 | ||
|
|
d007feb3d1 | ||
|
|
f827b85f47 | ||
|
|
e025ed8436 | ||
|
|
b82a2c3312 | ||
|
|
ca6c2ebb03 | ||
|
|
b900d3070d | ||
|
|
a0fcb6c91f | ||
|
|
1fe7af4fad | ||
|
|
de2053f988 | ||
|
|
1c110f0cb0 | ||
|
|
5fd6773f43 | ||
|
|
9c2c86baf6 | ||
|
|
3bd8f09c88 | ||
|
|
dd9cb21f8c | ||
|
|
2064cc88ab | ||
|
|
d982e35a17 | ||
|
|
4afcd44216 | ||
|
|
63252d3ee2 | ||
|
|
22a9502976 | ||
|
|
62babef678 | ||
|
|
f972733426 | ||
|
|
a703b84681 | ||
|
|
9ba03a5f02 | ||
|
|
cdf56fcec1 | ||
|
|
f27e79cb01 | ||
|
|
40872f6e9e | ||
|
|
b7f4edac24 | ||
|
|
8e99ed3258 | ||
|
|
c3c91d61c8 | ||
|
|
39f60daca7 | ||
|
|
dce1481185 |
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @helloanoop @maintainer-bruno @lohit-bruno @naman-bruno
|
||||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
||||
github: helloanoop
|
||||
44
.github/ISSUE_TEMPLATE/BugReport.yaml
vendored
44
.github/ISSUE_TEMPLATE/BugReport.yaml
vendored
@@ -6,26 +6,60 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
|
||||
Before submitting, please make sure you've searched existing issues:
|
||||
👉 [Search existing issues](https://github.com/usebruno/bruno/issues?q=is%3Aissue)
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'I have checked the following:'
|
||||
options:
|
||||
- label: I use the newest version of bruno.
|
||||
required: true
|
||||
- label: I've searched existing issues and found nothing related to my issue.
|
||||
- label: "I have searched existing issues and found nothing related to my issue."
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'This bug is:'
|
||||
options:
|
||||
- label: making Bruno unusable for me
|
||||
required: false
|
||||
- label: slowing me down but I'm able to continue working
|
||||
required: false
|
||||
- label: annoying
|
||||
required: false
|
||||
- label: this feature was working in a previous version but is broken in the current release.
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Bruno version
|
||||
description: Please specify the version of Bruno you are using in which the issue occurs.
|
||||
placeholder: 1.38.1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: Information about the operating system the issue occurs on.
|
||||
placeholder: Windows 11 26100.3037 / macOS 15.1 (24B83) / Linux 6.13.1
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of the bug.
|
||||
description: A clear and concise description of the bug and how it's effecting your work along with steps to reproduce.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: .bru file to reproduce the bug
|
||||
description: Attach your .bru file here that can reqroduce the problem.
|
||||
description: Attach your .bru file here that can reproduce the problem.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots/Live demo link
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/FeatureRequest.yaml
vendored
14
.github/ISSUE_TEMPLATE/FeatureRequest.yaml
vendored
@@ -8,13 +8,23 @@ body:
|
||||
options:
|
||||
- label: I've searched existing issues and found nothing related to my issue.
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'This feature'
|
||||
options:
|
||||
- label: blocks me from using Bruno
|
||||
required: false
|
||||
- label: would improve my quality of life in Bruno
|
||||
required: false
|
||||
- label: is something I've never seen an API client do before
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Suggest an idea for this project.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the feature you want to add
|
||||
label: Describe the feature you want to add, and how it would change your usage of Bruno
|
||||
description: A clear and concise description of the feature you want to be added.
|
||||
validations:
|
||||
required: true
|
||||
@@ -23,4 +33,4 @@ body:
|
||||
label: Mockups or Images of the feature
|
||||
description: Add some images to support your feature.
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
|
||||
31
.github/dependabot.yml
vendored
Normal file
31
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
groups:
|
||||
bruno-dependencies:
|
||||
patterns:
|
||||
- "*usebruno*"
|
||||
babel-dependencies:
|
||||
patterns:
|
||||
- "*babel*"
|
||||
fortawesome-dependencies:
|
||||
patterns:
|
||||
- "*fortawesome*"
|
||||
electron-dependencies:
|
||||
patterns:
|
||||
- "*electron*"
|
||||
rollup-dependencies:
|
||||
patterns:
|
||||
- "*rollup*"
|
||||
jest-dependencies:
|
||||
patterns:
|
||||
- "*jest*"
|
||||
9
.github/workflows/npm-bru-cli.yml
vendored
9
.github/workflows/npm-bru-cli.yml
vendored
@@ -20,10 +20,13 @@ permissions:
|
||||
jobs:
|
||||
test:
|
||||
name: CLI Tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
@@ -40,7 +43,7 @@ jobs:
|
||||
bru run --env Prod --output junit.xml --format junit
|
||||
|
||||
- name: Publish Test Report
|
||||
uses: dorny/test-reporter@v1
|
||||
uses: dorny/test-reporter@v2
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: Test Report
|
||||
|
||||
53
.github/workflows/tests.yml
vendored
53
.github/workflows/tests.yml
vendored
@@ -10,6 +10,8 @@ jobs:
|
||||
name: Unit Tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -26,6 +28,11 @@ jobs:
|
||||
npm run build --workspace=packages/bruno-common
|
||||
npm run build --workspace=packages/bruno-query
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
|
||||
- name: Lint Check
|
||||
run: npm run lint
|
||||
|
||||
# tests
|
||||
- name: Test Package bruno-js
|
||||
@@ -43,12 +50,18 @@ jobs:
|
||||
run: npm run test --workspace=packages/bruno-app
|
||||
- name: Test Package bruno-common
|
||||
run: npm run test --workspace=packages/bruno-common
|
||||
- name: Test Package bruno-converters
|
||||
run: npm run test --workspace=packages/bruno-converters
|
||||
- name: Test Package bruno-electron
|
||||
run: npm run test --workspace=packages/bruno-electron
|
||||
|
||||
cli-test:
|
||||
name: CLI Tests
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
checks: write
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -65,6 +78,8 @@ jobs:
|
||||
npm run build --workspace=packages/bruno-query
|
||||
npm run build --workspace=packages/bruno-common
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build --workspace=packages/bruno-converters
|
||||
npm run build --workspace=packages/bruno-requests
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
@@ -76,5 +91,43 @@ jobs:
|
||||
uses: EnricoMi/publish-unit-test-result-action@v2
|
||||
if: always()
|
||||
with:
|
||||
check_name: CLI Test Results
|
||||
files: packages/bruno-tests/collection/junit.xml
|
||||
comment_mode: always
|
||||
e2e-test:
|
||||
name: Playwright E2E Tests
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: v22.11.x
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get --no-install-recommends install -y \
|
||||
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
|
||||
xvfb
|
||||
npm ci --legacy-peer-deps
|
||||
sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox
|
||||
|
||||
- name: Build libraries
|
||||
run: |
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: |
|
||||
xvfb-run npm run test:e2e
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -46,4 +46,8 @@ yarn-error.log*
|
||||
|
||||
#dev editor
|
||||
bruno.iml
|
||||
.idea
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# Playwright
|
||||
/blob-report/
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 86 KiB |
@@ -15,15 +15,15 @@
|
||||
| [正體中文](docs/contributing/contributing_zhtw.md)
|
||||
| [日本語](docs/contributing/contributing_ja.md)
|
||||
| [हिंदी](docs/contributing/contributing_hi.md)
|
||||
| [Nederlands](docs/contributing/contributing_nl.md)
|
||||
| [Dutch](docs/contributing/contributing_nl.md)
|
||||
|
||||
## Let's make Bruno better, together!!
|
||||
|
||||
We are happy that you are looking to improve Bruno. Below are the guidelines to get started bringing up Bruno on your computer.
|
||||
We are happy that you are looking to improve Bruno. Below are the guidelines to run Bruno on your computer.
|
||||
|
||||
### Technology Stack
|
||||
|
||||
Bruno is built using Next.js and React. We also use electron to ship a desktop version (that supports local collections)
|
||||
Bruno is built using React and Electron.
|
||||
|
||||
Libraries we use
|
||||
|
||||
@@ -37,38 +37,76 @@ Libraries we use
|
||||
- Filesystem Watcher - chokidar
|
||||
- i18n - i18next
|
||||
|
||||
### Dependencies
|
||||
|
||||
You would need [Node v20.x or the latest LTS version](https://nodejs.org/en/) and npm 8.x. We use npm workspaces in the project
|
||||
> [!IMPORTANT]
|
||||
> You would need [Node v22.x or the latest LTS version](https://nodejs.org/en/). We use npm workspaces in the project
|
||||
|
||||
## Development
|
||||
|
||||
Bruno is being developed as a desktop app. You need to load the app by running the Next.js app in one terminal and then run the electron app in another terminal.
|
||||
Bruno is a desktop app. Below are the instructions to run Bruno.
|
||||
|
||||
### Local Development
|
||||
> Note: We use React for the frontend and rsbuild for build and dev server.
|
||||
|
||||
## Install Dependencies
|
||||
|
||||
```bash
|
||||
# use nodejs 20 version
|
||||
# use nodejs 22 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
npm i --legacy-peer-deps
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
#### Build packages
|
||||
|
||||
##### Option 1
|
||||
|
||||
```bash
|
||||
# build packages
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# bundle js sandbox libraries
|
||||
npm run sandbox:bundle-libraries --workspace=packages/bruno-js
|
||||
```
|
||||
##### Option 2
|
||||
|
||||
# run next app (terminal 1)
|
||||
```bash
|
||||
# install dependencies and setup
|
||||
npm run setup
|
||||
```
|
||||
|
||||
#### Run the app
|
||||
|
||||
##### Option 1
|
||||
|
||||
```bash
|
||||
# run react app (terminal 1)
|
||||
npm run dev:web
|
||||
|
||||
# run electron app (terminal 2)
|
||||
npm run dev:electron
|
||||
```
|
||||
|
||||
##### Option 2
|
||||
```bash
|
||||
# run electron and react app concurrently
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### Customize Electron `userData` path
|
||||
If `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.
|
||||
|
||||
e.g.
|
||||
```sh
|
||||
ELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron
|
||||
```
|
||||
This will create a `bruno-test` folder on your Desktop and use it as the `userData` path.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
You might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.
|
||||
@@ -86,11 +124,32 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
# run bruno-schema tests
|
||||
npm run test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# run bruno-query tests
|
||||
npm run test --workspace=packages/bruno-query
|
||||
|
||||
# run bruno-common tests
|
||||
npm run test --workspace=packages/bruno-common
|
||||
|
||||
# run bruno-converters tests
|
||||
npm run test --workspace=packages/bruno-converters
|
||||
|
||||
# run bruno-app tests
|
||||
npm run test --workspace=packages/bruno-app
|
||||
|
||||
# run bruno-electron tests
|
||||
npm run test --workspace=packages/bruno-electron
|
||||
|
||||
# run bruno-lang tests
|
||||
npm run test --workspace=packages/bruno-lang
|
||||
|
||||
# run bruno-toml tests
|
||||
npm run test --workspace=packages/bruno-toml
|
||||
|
||||
# run tests over all workspaces
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Raising Pull Requests
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testing (পরীক্ষা)
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# ব্রুনো-স্কিমা পরীক্ষা চালান
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# সমস্ত কর্মক্ষেত্রে পরীক্ষা চালান
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Raising Pull Request (পুল অনুরোধ উত্থাপন)
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### 测试
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# 运行 bruno-schema 测试
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# 在所有工作区上运行测试
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### 提交 Pull Request
|
||||
|
||||
@@ -21,7 +21,7 @@ Bibliotheken die wir benutzen
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
Du benötigst [Node v20.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
|
||||
Du benötigst [Node v22.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.
|
||||
|
||||
### Lass uns coden
|
||||
|
||||
@@ -42,12 +42,12 @@ Bruno wird als Desktop-Anwendung entwickelt. Um die App zu starten, musst Du zue
|
||||
|
||||
### Abhängigkeiten
|
||||
|
||||
- NodeJS v18
|
||||
- NodeJS v22
|
||||
|
||||
### Lokales Entwickeln
|
||||
|
||||
```bash
|
||||
# use nodejs 18 version
|
||||
# use nodejs 22 version
|
||||
nvm use
|
||||
|
||||
# install deps
|
||||
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testen
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# Führen Sie Bruno-Schema-Tests aus
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# Führen Sie Tests für alle Arbeitsbereiche durch
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Pruebas
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# ejecutar pruebas de esquema bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# ejecutar pruebas en todos los espacios de trabajo
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Crea un Pull Request
|
||||
|
||||
@@ -73,11 +73,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# exécuter des tests de schéma bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# exécuter des tests sur tous les espaces de travail
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Ouvrir une Pull Request
|
||||
|
||||
@@ -40,6 +40,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# Next.js ऐप चलाएँ (टर्मिनल 1 पर)
|
||||
npm run dev:web
|
||||
@@ -65,11 +67,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### परिक्षण
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# ब्रूनो-स्कीमा परीक्षण चलाएँ
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# सभी कार्यस्थानों पर परीक्षण चलाएँ
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### पुल अनुरोध प्रक्रिया
|
||||
|
||||
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# esegui i test dello schema bruno
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# esegui test su tutti gli spazi di lavoro
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
@@ -40,6 +40,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# run next app (terminal 1)
|
||||
npm run dev:web
|
||||
@@ -65,11 +67,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### テストを動かすには
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# ブルーノスキーマのテストを実行します
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# すべてのワークスペースでテストを実行します
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### プルリクエストの手順
|
||||
|
||||
@@ -40,6 +40,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# next 앱 실행 (1번 터미널)
|
||||
npm run dev:web
|
||||
@@ -66,11 +68,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### 테스팅
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# bruno-schema 테스트 실행
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# 모든 작업 공간에서 테스트 실행
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Pull Requests 요청
|
||||
|
||||
@@ -40,6 +40,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# draai next app (terminal 1)
|
||||
npm run dev:web
|
||||
@@ -65,11 +67,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testen
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# voer bruno-schema tests uit
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# voer tests uit over alle werkruimten
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Pull Requests indienen
|
||||
|
||||
@@ -71,11 +71,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testowanie
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# uruchom testy bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# uruchom testy we wszystkich przestrzeniach roboczych
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Tworzenie Pull Request
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testando
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# executar testes do bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# executar testes em todos os ambientes de trabalho
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Envio de Pull Request
|
||||
|
||||
@@ -64,11 +64,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testarea
|
||||
|
||||
```shell
|
||||
# bruno-schema
|
||||
# executați teste bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# executați teste peste toate spațiile de lucru
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Crearea unui Pull Request
|
||||
|
||||
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Тестирование
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# запустите тесты bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# запустите тесты во всех рабочих пространствах
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
@@ -42,6 +42,8 @@ npm i --legacy-peer-deps
|
||||
npm run build:graphql-docs
|
||||
npm run build:bruno-query
|
||||
npm run build:bruno-common
|
||||
npm run build:bruno-converters
|
||||
npm run build:bruno-requests
|
||||
|
||||
# spustite ďalšiu aplikáciu (terminál 1)
|
||||
npm run dev:web
|
||||
@@ -67,11 +69,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Testovanie
|
||||
|
||||
````bash
|
||||
# bruno-schema
|
||||
# spustiť bruno-schema testy
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# spustiť testy vo všetkých pracovných priestoroch
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Vyrobenie Pull Request
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Test
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# bruno-schema testlerini çalıştır
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# tüm çalışma alanlarında testleri çalıştır
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### Pull Request Oluşturma
|
||||
|
||||
@@ -83,9 +83,9 @@ find . -type f -name "package-lock.json" -delete
|
||||
### Тестування
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# запустити тести bruno-schema
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# запустити тести у всіх робочих просторах
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
@@ -70,11 +70,11 @@ find . -type f -name "package-lock.json" -delete
|
||||
### 測試
|
||||
|
||||
```bash
|
||||
# bruno-schema
|
||||
# 執行布魯諾架構測試
|
||||
npm test --workspace=packages/bruno-schema
|
||||
|
||||
# bruno-lang
|
||||
npm test --workspace=packages/bruno-lang
|
||||
# 對所有工作區執行測試
|
||||
npm test --workspaces --if-present
|
||||
```
|
||||
|
||||
### 發送 Pull Request
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
| [日本語](./readme_ja.md)
|
||||
| [ქართული](./readme_ka.md)
|
||||
|
||||
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статус кво, запровадженого інструментами на кшталт Postman.
|
||||
Bruno це новий та іноваційний API клієнт, націлений на революційну зміну статусy кво, запровадженого інструментами на кшталт Postman.
|
||||
|
||||
Bruno зберігає ваші колекції напряму у теці на вашому диску. Він використовує текстову мову розмітки Bru для збереження інформації про ваші API запити.
|
||||
|
||||
Ви можете використовувати git або будь-яку іншу систему контролю версій щоб спільно працювати над вашими колекціями API запитів.
|
||||
|
||||
Bruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Взнати більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269)
|
||||
Bruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Дізнатись більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269)
|
||||
|
||||
 <br /><br />
|
||||
|
||||
@@ -69,13 +69,13 @@ Bruno є повністю автономним. Немає жодних план
|
||||
|
||||
### Поділитись відгуками 📣
|
||||
|
||||
Якщо Bruno допоміг вам у вашій роботі і вашим командам, будь ласка не забудьте поділитись вашими [відгуками у github дискусії](https://github.com/usebruno/bruno/discussions/343)
|
||||
Якщо Bruno допоміг у роботі вам або вашій команді, будь ласка не забудьте поділитись вашими [відгуками у github дискусії](https://github.com/usebruno/bruno/discussions/343)
|
||||
|
||||
### Зробити свій внесок 👩💻🧑💻
|
||||
|
||||
Я радий що ви бажаєте покращити Bruno. Будь ласка переглянте [інструкцію по контрибуції](../contributing/contributing_ua.md)
|
||||
|
||||
Навіть якщо ви не можете зробити свій внесок пишучи програмний код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі.
|
||||
Навіть якщо ви не можете зробити свій внесок пишучи код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі.
|
||||
|
||||
### Автори
|
||||
|
||||
|
||||
5
e2e-tests/001-sanity-tests/001-home-screen.spec.ts
Normal file
5
e2e-tests/001-sanity-tests/001-home-screen.spec.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Check if the logo on top left is visible', async ({ page }) => {
|
||||
await expect(page.getByRole('button', { name: 'bruno' })).toBeVisible();
|
||||
});
|
||||
31
e2e-tests/001-sanity-tests/002-create-new-collection.spec.ts
Normal file
31
e2e-tests/001-sanity-tests/002-create-new-collection.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Create new collection and add a simple HTTP request', async ({ page, createTmpDir }) => {
|
||||
await page.getByLabel('Create Collection').click();
|
||||
await page.getByLabel('Name').click();
|
||||
await page.getByLabel('Name').fill('test-collection');
|
||||
await page.getByLabel('Name').press('Tab');
|
||||
await page.getByLabel('Location').fill(await createTmpDir('test-collection'));
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
await page.getByText('test-collection').click();
|
||||
await page.getByLabel('Safe ModeBETA').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('#create-new-tab').getByRole('img').click();
|
||||
await page.getByPlaceholder('Request Name').fill('r1');
|
||||
await page.getByPlaceholder('Request URL').click();
|
||||
await page.getByPlaceholder('Request URL').fill('http://localhost:8081');
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
await page.locator('pre').filter({ hasText: 'http://localhost:' }).click();
|
||||
await page.locator('textarea').fill('/ping');
|
||||
await page.locator('#send-request').getByRole('img').nth(2).click();
|
||||
|
||||
await expect(page.getByRole('main')).toContainText('200 OK');
|
||||
|
||||
await page.getByRole('tab', { name: 'GET r1' }).locator('circle').click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await page.getByText('GETr1').click();
|
||||
await page.getByRole('button', { name: 'Clear response' }).click();
|
||||
await page.locator('body').press('ControlOrMeta+Enter');
|
||||
|
||||
await expect(page.getByRole('main')).toContainText('200 OK');
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"maximized": true,
|
||||
"lastOpenedCollections": ["{{projectRoot}}/packages/bruno-tests/collection"]
|
||||
}
|
||||
49
e2e-tests/bruno-testbench/run-testbench-requests.spec.ts
Normal file
49
e2e-tests/bruno-testbench/run-testbench-requests.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test.describe.parallel('Run Testbench Requests', () => {
|
||||
test('Run bruno-testbench in Developer Mode', async ({ pageWithUserData: page }) => {
|
||||
test.setTimeout(2 * 60 * 1000);
|
||||
|
||||
await page.getByText('bruno-testbench').click();
|
||||
await page.getByLabel('Developer Mode(use only if').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('.environment-selector').nth(1).click();
|
||||
await page.locator('.dropdown-item').getByText('Prod').click();
|
||||
await page.locator('.collection-actions').hover();
|
||||
await page.locator('.collection-actions .icon').click();
|
||||
await page.getByText('Run', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Run Collection' }).click();
|
||||
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
|
||||
|
||||
const result = await page.getByText('Total Requests: ').innerText();
|
||||
const [totalRequests, passed, failed, skipped] = result
|
||||
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
|
||||
.slice(1);
|
||||
|
||||
await expect(parseInt(failed)).toBe(0);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1);
|
||||
});
|
||||
|
||||
test.fixme('Run bruno-testbench in Safe Mode', async ({ pageWithUserData: page }) => {
|
||||
test.setTimeout(2 * 60 * 1000);
|
||||
|
||||
await page.getByText('bruno-testbench').click();
|
||||
await page.getByLabel('Safe ModeBETA').check();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.locator('.environment-selector').nth(1).click();
|
||||
await page.locator('.dropdown-item').getByText('Prod').click();
|
||||
await page.locator('.collection-actions').hover();
|
||||
await page.locator('.collection-actions .icon').click();
|
||||
await page.getByText('Run', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Run Collection' }).click();
|
||||
await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });
|
||||
|
||||
const result = await page.getByText('Total Requests: ').innerText();
|
||||
const [totalRequests, passed, failed, skipped] = result
|
||||
.match(/Total Requests: (\d+), Passed: (\d+), Failed: (\d+), Skipped: (\d+)/)
|
||||
.slice(1);
|
||||
|
||||
await expect(parseInt(failed)).toBe(0);
|
||||
await expect(parseInt(passed)).toBe(parseInt(totalRequests) - parseInt(skipped) - 1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { test, expect } from '../../playwright';
|
||||
|
||||
test('Should verify all support links with correct URL in preference > Support tab', async ({ page }) => {
|
||||
|
||||
// Open Preferences
|
||||
await page.getByLabel('Open Preferences').click();
|
||||
|
||||
// Verify Support tab
|
||||
await page.getByRole('tab', { name: 'Support' }).click();
|
||||
|
||||
const locator_twitter = page.getByRole('link', { name: 'Twitter' });
|
||||
expect(await locator_twitter.getAttribute('href')).toEqual('https://twitter.com/use_bruno');
|
||||
|
||||
const locator_github = page.getByRole('link', { name: 'GitHub', exact: true });
|
||||
expect(await locator_github.getAttribute('href')).toEqual('https://github.com/usebruno/bruno');
|
||||
|
||||
const locator_discord = page.getByRole('link', { name: 'Discord', exact: true });
|
||||
expect(await locator_discord.getAttribute('href')).toEqual('https://discord.com/invite/KgcZUncpjq');
|
||||
|
||||
const locator_reportissues = page.getByRole('link', { name: 'Report Issues', exact: true });
|
||||
expect(await locator_reportissues.getAttribute('href')).toEqual('https://github.com/usebruno/bruno/issues');
|
||||
|
||||
const locator_documentation = page.getByRole('link', { name: 'Documentation', exact: true });
|
||||
expect(await locator_documentation.getAttribute('href')).toEqual('https://docs.usebruno.com');
|
||||
|
||||
|
||||
});
|
||||
41
eslint.config.js
Normal file
41
eslint.config.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// eslint.config.js
|
||||
const { defineConfig } = require("eslint/config");
|
||||
const globals = require("globals");
|
||||
|
||||
module.exports = defineConfig([
|
||||
{
|
||||
files: ["packages/bruno-app/**/*.{js,jsx,ts}"],
|
||||
ignores: ["**/*.config.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.jest,
|
||||
global: false,
|
||||
require: false,
|
||||
Buffer: false,
|
||||
process: false
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["packages/bruno-electron/**/*.{js}"],
|
||||
ignores: ["**/*.config.js"],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-undef": "error",
|
||||
},
|
||||
}
|
||||
]);
|
||||
13908
package-lock.json
generated
13908
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -6,36 +6,48 @@
|
||||
"packages/bruno-electron",
|
||||
"packages/bruno-cli",
|
||||
"packages/bruno-common",
|
||||
"packages/bruno-converters",
|
||||
"packages/bruno-schema",
|
||||
"packages/bruno-query",
|
||||
"packages/bruno-js",
|
||||
"packages/bruno-lang",
|
||||
"packages/bruno-tests",
|
||||
"packages/bruno-toml",
|
||||
"packages/bruno-graphql-docs"
|
||||
"packages/bruno-graphql-docs",
|
||||
"packages/bruno-requests"
|
||||
],
|
||||
"homepage": "https://usebruno.com",
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@jest/globals": "^29.2.0",
|
||||
"@playwright/test": "^1.27.1",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.14.1",
|
||||
"concurrently": "^8.2.2",
|
||||
"eslint": "^9.26.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"husky": "^8.0.3",
|
||||
"globals": "^16.1.0",
|
||||
"jest": "^29.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"playwright": "^1.51.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"randomstring": "^1.2.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"ts-jest": "^29.0.5"
|
||||
"ts-jest": "^29.2.6"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "node ./scripts/setup.js",
|
||||
"watch:converters": "npm run watch --workspace=packages/bruno-converters",
|
||||
"dev": "concurrently --kill-others \"npm run dev:web\" \"npm run dev:electron\"",
|
||||
"dev:web": "npm run dev --workspace=packages/bruno-app",
|
||||
"build:web": "npm run build --workspace=packages/bruno-app",
|
||||
"prettier:web": "npm run prettier --workspace=packages/bruno-app",
|
||||
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
|
||||
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
|
||||
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
|
||||
"build:bruno-requests": "npm run build --workspace=packages/bruno-requests",
|
||||
"build:bruno-converters": "npm run build --workspace=packages/bruno-converters",
|
||||
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
|
||||
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
|
||||
"build:electron": "node ./scripts/build-electron.js",
|
||||
@@ -45,12 +57,18 @@
|
||||
"build:electron:deb": "./scripts/build-electron.sh deb",
|
||||
"build:electron:rpm": "./scripts/build-electron.sh rpm",
|
||||
"build:electron:snap": "./scripts/build-electron.sh snap",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:report": "npx playwright show-report",
|
||||
"watch:common": "npm run watch --workspace=packages/bruno-common",
|
||||
"test:codegen": "node playwright/codegen.ts",
|
||||
"test:e2e": "playwright test",
|
||||
"test:prettier:web": "npm run test:prettier --workspace=packages/bruno-app",
|
||||
"prepare": "husky install"
|
||||
"lint": "node --max_old_space_size=4096 $(npx which eslint)"
|
||||
},
|
||||
"overrides": {
|
||||
"rollup":"3.29.5"
|
||||
"rollup": "3.29.5",
|
||||
"electron-store": {
|
||||
"conf": {
|
||||
"json-schema-typed": "8.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"presets": ["next/babel"],
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": [["styled-components", { "ssr": true }]]
|
||||
}
|
||||
4
packages/bruno-app/.gitignore
vendored
4
packages/bruno-app/.gitignore
vendored
@@ -31,4 +31,6 @@ yarn-error.log*
|
||||
|
||||
# next.js
|
||||
.next/
|
||||
out/
|
||||
dist/
|
||||
|
||||
.env
|
||||
16
packages/bruno-app/jest.config.js
Normal file
16
packages/bruno-app/jest.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
rootDir: '.',
|
||||
moduleNameMapper: {
|
||||
'^assets/(.*)$': '<rootDir>/src/assets/$1',
|
||||
'^components/(.*)$': '<rootDir>/src/components/$1',
|
||||
'^hooks/(.*)$': '<rootDir>/src/hooks/$1',
|
||||
'^themes/(.*)$': '<rootDir>/src/themes/$1',
|
||||
'^api/(.*)$': '<rootDir>/src/api/$1',
|
||||
'^pageComponents/(.*)$': '<rootDir>/src/pageComponents/$1',
|
||||
'^providers/(.*)$': '<rootDir>/src/providers/$1',
|
||||
'^utils/(.*)$': '<rootDir>/src/utils/$1'
|
||||
},
|
||||
clearMocks: true,
|
||||
moduleDirectories: ['node_modules', 'src'],
|
||||
testEnvironment: 'node'
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
module.exports = {
|
||||
output: 'export',
|
||||
reactStrictMode: false,
|
||||
publicRuntimeConfig: {
|
||||
CI: process.env.CI,
|
||||
PLAYWRIGHT: process.env.PLAYWRIGHT,
|
||||
ENV: process.env.ENV
|
||||
},
|
||||
webpack: (config, { isServer }) => {
|
||||
// Fixes npm packages that depend on `fs` module
|
||||
if (!isServer) {
|
||||
config.resolve.fallback.fs = false;
|
||||
}
|
||||
Object.defineProperty(config, 'devtool', {
|
||||
get() {
|
||||
return 'source-map';
|
||||
},
|
||||
set() {},
|
||||
});
|
||||
return config;
|
||||
},
|
||||
};
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "@usebruno/app",
|
||||
"version": "0.3.0",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "cross-env ENV=dev next dev -p 3000",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"dev": "rsbuild dev",
|
||||
"build": "rsbuild build -m production",
|
||||
"preview": "rsbuild preview",
|
||||
"test": "jest",
|
||||
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
|
||||
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@fontsource/inter": "^5.0.15",
|
||||
"@prantlf/jsonlint": "^16.0.0",
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
@@ -20,11 +20,11 @@
|
||||
"@usebruno/common": "0.1.0",
|
||||
"@usebruno/graphql-docs": "0.1.0",
|
||||
"@usebruno/schema": "0.7.0",
|
||||
"axios": "1.7.5",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "5.65.2",
|
||||
"codemirror-graphql": "2.1.1",
|
||||
"cookie": "0.7.1",
|
||||
"dompurify": "^3.2.4",
|
||||
"escape-html": "^1.0.3",
|
||||
"file": "^0.2.2",
|
||||
"file-dialog": "^0.0.8",
|
||||
@@ -34,22 +34,24 @@
|
||||
"graphiql": "3.7.1",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"httpsnippet": "^3.0.6",
|
||||
"i18next": "^23.14.0",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"i18next": "24.1.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"jsesc": "^3.0.2",
|
||||
"jshint": "^2.13.6",
|
||||
"json5": "^2.2.3",
|
||||
"jsonc-parser": "^3.2.1",
|
||||
"jsonpath-plus": "10.1.0",
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"know-your-http-well": "^0.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.2",
|
||||
"markdown-it-replace-link": "^1.2.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.47",
|
||||
"mousetrap": "^1.6.5",
|
||||
"nanoid": "3.3.4",
|
||||
"next": "14.2.16",
|
||||
"nanoid": "3.3.8",
|
||||
"path": "^0.12.7",
|
||||
"pdfjs-dist": "4.4.168",
|
||||
"platform": "^1.3.6",
|
||||
@@ -57,18 +59,20 @@
|
||||
"prettier": "^2.7.1",
|
||||
"qs": "^6.11.0",
|
||||
"query-string": "^7.0.1",
|
||||
"react": "18.2.0",
|
||||
"react": "19.0.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-inspector": "^6.0.2",
|
||||
"react-pdf": "9.1.1",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-player": "^2.16.0",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-tooltip": "^5.5.2",
|
||||
"sass": "^1.46.0",
|
||||
"semver": "^7.7.1",
|
||||
"strip-json-comments": "^5.0.1",
|
||||
"styled-components": "^5.3.3",
|
||||
"system": "^2.0.1",
|
||||
@@ -78,13 +82,14 @@
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/plugin-transform-spread": "^7.16.7",
|
||||
"@babel/preset-env": "^7.16.4",
|
||||
"@babel/preset-react": "^7.16.0",
|
||||
"@babel/runtime": "^7.16.3",
|
||||
"@rsbuild/core": "^1.1.2",
|
||||
"@rsbuild/plugin-babel": "^1.0.3",
|
||||
"@rsbuild/plugin-node-polyfill": "^1.2.0",
|
||||
"@rsbuild/plugin-react": "^1.0.7",
|
||||
"@rsbuild/plugin-sass": "^1.1.0",
|
||||
"@rsbuild/plugin-styled-components": "1.1.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "7.1.2",
|
||||
"file-loader": "^6.2.0",
|
||||
|
||||
49
packages/bruno-app/rsbuild.config.mjs
Normal file
49
packages/bruno-app/rsbuild.config.mjs
Normal file
@@ -0,0 +1,49 @@
|
||||
import { defineConfig } from '@rsbuild/core';
|
||||
import { pluginReact } from '@rsbuild/plugin-react';
|
||||
import { pluginBabel } from '@rsbuild/plugin-babel';
|
||||
import { pluginStyledComponents } from '@rsbuild/plugin-styled-components';
|
||||
import { pluginSass } from '@rsbuild/plugin-sass';
|
||||
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
pluginNodePolyfill(),
|
||||
pluginReact(),
|
||||
pluginStyledComponents(),
|
||||
pluginSass(),
|
||||
pluginBabel({
|
||||
include: /\.(?:js|jsx|tsx)$/,
|
||||
babelLoaderOptions(opts) {
|
||||
opts.plugins?.unshift('babel-plugin-react-compiler');
|
||||
}
|
||||
})
|
||||
],
|
||||
source: {
|
||||
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file,
|
||||
},
|
||||
html: {
|
||||
title: 'Bruno'
|
||||
},
|
||||
tools: {
|
||||
rspack: {
|
||||
module: {
|
||||
parser: {
|
||||
javascript: {
|
||||
// This loads the JavaScript contents from a library along with the main JavaScript bundle.
|
||||
dynamicImportMode: "eager",
|
||||
},
|
||||
},
|
||||
},
|
||||
ignoreWarnings: [
|
||||
(warning) => warning.message.includes('Critical dependency: the request of a dependency is an expression') && warning?.moduleDescriptor?.name?.includes('flow-parser')
|
||||
],
|
||||
// Add externals configuration to exclude Node.js libraries
|
||||
externals: {
|
||||
// List specific Node.js modules you want to exclude
|
||||
// Format: 'module-name': 'commonjs module-name'
|
||||
'worker_threads': 'commonjs worker_threads',
|
||||
// 'path': 'commonjs path'
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
62
packages/bruno-app/src/components/Accordion/index.js
Normal file
62
packages/bruno-app/src/components/Accordion/index.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import { IconChevronDown } from '@tabler/icons';
|
||||
import { AccordionItem, AccordionHeader, AccordionContent } from './styledWrapper';
|
||||
|
||||
const AccordionContext = createContext();
|
||||
|
||||
const Accordion = ({ children, defaultIndex }) => {
|
||||
const [openIndex, setOpenIndex] = useState(defaultIndex);
|
||||
|
||||
const toggleItem = (index) => {
|
||||
setOpenIndex(openIndex === index ? null : index);
|
||||
};
|
||||
|
||||
return (
|
||||
<AccordionContext.Provider value={{ openIndex, toggleItem }}>
|
||||
<div>{children}</div>
|
||||
</AccordionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = ({ index, children, ...props }) => {
|
||||
return (
|
||||
<AccordionItem {...props}>
|
||||
{React.Children.map(children, (child) => React.cloneElement(child, { index }))}
|
||||
</AccordionItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const Header = ({ index, children, ...props }) => {
|
||||
const { openIndex, toggleItem } = useContext(AccordionContext);
|
||||
const isOpen = openIndex === index;
|
||||
|
||||
return (
|
||||
<AccordionHeader onClick={() => toggleItem(index)} {...props} className={isOpen ? 'open' : ''}>
|
||||
<div className="w-full">{children}</div>
|
||||
|
||||
<IconChevronDown
|
||||
className="w-5 h-5 ml-auto"
|
||||
style={{
|
||||
transform: `rotate(${isOpen ? '180deg' : '0deg'})`,
|
||||
transition: 'transform 0.3s ease-in-out'
|
||||
}}
|
||||
/>
|
||||
</AccordionHeader>
|
||||
);
|
||||
};
|
||||
|
||||
const Content = ({ index, children, ...props }) => {
|
||||
const { openIndex } = useContext(AccordionContext);
|
||||
const isOpen = openIndex === index;
|
||||
|
||||
return (
|
||||
<AccordionContent isOpen={isOpen} {...props}>
|
||||
{children}
|
||||
</AccordionContent>
|
||||
);
|
||||
};
|
||||
|
||||
Accordion.Item = Item;
|
||||
Accordion.Header = Header;
|
||||
Accordion.Content = Content;
|
||||
export default Accordion;
|
||||
28
packages/bruno-app/src/components/Accordion/styledWrapper.js
Normal file
28
packages/bruno-app/src/components/Accordion/styledWrapper.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const AccordionItem = styled.div`
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
|
||||
const AccordionHeader = styled.button`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 0.75rem 1rem;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
|
||||
&.open, &:hover {
|
||||
background-color: ${(props) => props.theme.plainGrid.hoverBg};
|
||||
}
|
||||
`;
|
||||
|
||||
const AccordionContent = styled.div`
|
||||
padding: ${(props) => (props.isOpen ? '1rem' : '0')};
|
||||
max-height: ${(props) => (props.isOpen ? 'auto' : '0')};
|
||||
`;
|
||||
|
||||
export { AccordionItem, AccordionHeader, AccordionContent };
|
||||
@@ -8,6 +8,8 @@ const StyledWrapper = styled.div`
|
||||
font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};
|
||||
line-break: anywhere;
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
/* Removes the glow outline around the folded json */
|
||||
@@ -26,6 +28,16 @@ const StyledWrapper = styled.div`
|
||||
|
||||
.CodeMirror-dialog {
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
top: unset;
|
||||
left: unset;
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: 1px solid #d3d6db;
|
||||
outline: none;
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
#search-results-count {
|
||||
@@ -82,6 +94,21 @@ const StyledWrapper = styled.div`
|
||||
.CodeMirror-search-hint {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.cm-s-default span.cm-property {
|
||||
color: #1f61a0 !important;
|
||||
}
|
||||
|
||||
.cm-s-default span.cm-variable {
|
||||
color: #397d13 !important;
|
||||
}
|
||||
|
||||
//matching bracket fix
|
||||
.CodeMirror-matchingbracket {
|
||||
background: #5cc0b48c !important;
|
||||
text-decoration:unset;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -7,15 +7,15 @@
|
||||
|
||||
import React from 'react';
|
||||
import { isEqual, escapeRegExp } from 'lodash';
|
||||
import { getEnvironmentVariables } from 'utils/collections';
|
||||
import { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import * as jsonlint from '@prantlf/jsonlint';
|
||||
import { JSHINT } from 'jshint';
|
||||
import stripJsonComments from 'strip-json-comments';
|
||||
import { getAllVariables } from 'utils/collections';
|
||||
|
||||
let CodeMirror;
|
||||
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
|
||||
const TAB_SIZE = 2;
|
||||
|
||||
if (!SERVER_RENDERED) {
|
||||
@@ -31,9 +31,11 @@ if (!SERVER_RENDERED) {
|
||||
'res.body',
|
||||
'res.responseTime',
|
||||
'res.getStatus()',
|
||||
'res.getStatusText()',
|
||||
'res.getHeader(name)',
|
||||
'res.getHeaders()',
|
||||
'res.getBody()',
|
||||
'res.setBody(data)',
|
||||
'res.getResponseTime()',
|
||||
'req',
|
||||
'req.url',
|
||||
@@ -56,15 +58,17 @@ if (!SERVER_RENDERED) {
|
||||
'req.getTimeout()',
|
||||
'req.setTimeout(timeout)',
|
||||
'req.getExecutionMode()',
|
||||
'req.getName()',
|
||||
'bru',
|
||||
'bru.cwd()',
|
||||
'bru.getEnvName(key)',
|
||||
'bru.getEnvName()',
|
||||
'bru.getProcessEnv(key)',
|
||||
'bru.hasEnvVar(key)',
|
||||
'bru.getEnvVar(key)',
|
||||
'bru.getFolderVar(key)',
|
||||
'bru.getCollectionVar(key)',
|
||||
'bru.setEnvVar(key,value)',
|
||||
'bru.deleteEnvVar(key)',
|
||||
'bru.hasVar(key)',
|
||||
'bru.getVar(key)',
|
||||
'bru.setVar(key,value)',
|
||||
@@ -73,9 +77,18 @@ if (!SERVER_RENDERED) {
|
||||
'bru.setNextRequest(requestName)',
|
||||
'req.disableParsingResponseJson()',
|
||||
'bru.getRequestVar(key)',
|
||||
'bru.runRequest(requestPathName)',
|
||||
'bru.getAssertionResults()',
|
||||
'bru.getTestResults()',
|
||||
'bru.sleep(ms)',
|
||||
'bru.getCollectionName()',
|
||||
'bru.getGlobalEnvVar(key)',
|
||||
'bru.setGlobalEnvVar(key, value)'
|
||||
'bru.setGlobalEnvVar(key, value)',
|
||||
'bru.runner',
|
||||
'bru.runner.setNextRequest(requestName)',
|
||||
'bru.runner.skipRequest()',
|
||||
'bru.runner.stopExecution()',
|
||||
'bru.interpolate(str)'
|
||||
];
|
||||
CodeMirror.registerHelper('hint', 'brunoJS', (editor, options) => {
|
||||
const cursor = editor.getCursor();
|
||||
@@ -97,7 +110,7 @@ if (!SERVER_RENDERED) {
|
||||
if (curWordBru) {
|
||||
hintWords.forEach((h) => {
|
||||
if (h.includes('.') == curWordBru.includes('.') && h.startsWith(curWordBru)) {
|
||||
result.list.push(curWordBru.includes('.') ? h.split('.')[1] : h);
|
||||
result.list.push(curWordBru.includes('.') ? h.split('.')?.at(-1) : h);
|
||||
}
|
||||
});
|
||||
result.list?.sort();
|
||||
@@ -166,11 +179,21 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
},
|
||||
'Cmd-F': (cm) => {
|
||||
if (this._isSearchOpen()) {
|
||||
// replace the older search component with the new one
|
||||
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
search && search.remove();
|
||||
}
|
||||
cm.execCommand('findPersistent');
|
||||
this._bindSearchHandler();
|
||||
this._appendSearchResultsCount();
|
||||
},
|
||||
'Ctrl-F': (cm) => {
|
||||
if (this._isSearchOpen()) {
|
||||
// replace the older search component with the new one
|
||||
const search = document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
search && search.remove();
|
||||
}
|
||||
cm.execCommand('findPersistent');
|
||||
this._bindSearchHandler();
|
||||
this._appendSearchResultsCount();
|
||||
@@ -189,31 +212,19 @@ export default class CodeEditor extends React.Component {
|
||||
'Cmd-Y': 'foldAll',
|
||||
'Ctrl-I': 'unfoldAll',
|
||||
'Cmd-I': 'unfoldAll',
|
||||
'Cmd-/': (cm) => {
|
||||
// comment/uncomment every selected line(s)
|
||||
const selections = cm.listSelections();
|
||||
selections.forEach((range) => {
|
||||
for (let i = range.from().line; i <= range.to().line; i++) {
|
||||
const selectedLine = cm.getLine(i);
|
||||
// if commented line, remove comment
|
||||
if (selectedLine.trim().startsWith('//')) {
|
||||
cm.replaceRange(
|
||||
selectedLine.replace(/^(\s*)\/\/\s?/, '$1'),
|
||||
{ line: i, ch: 0 },
|
||||
{ line: i, ch: selectedLine.length }
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// otherwise add comment
|
||||
cm.replaceRange(
|
||||
selectedLine.search(/\S|$/) >= TAB_SIZE
|
||||
? ' '.repeat(TAB_SIZE) + '// ' + selectedLine.trim()
|
||||
: '// ' + selectedLine,
|
||||
{ line: i, ch: 0 },
|
||||
{ line: i, ch: selectedLine.length }
|
||||
);
|
||||
}
|
||||
});
|
||||
'Ctrl-/': () => {
|
||||
if (['application/ld+json', 'application/json'].includes(this.props.mode)) {
|
||||
this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });
|
||||
} else {
|
||||
this.editor.toggleComment();
|
||||
}
|
||||
},
|
||||
'Cmd-/': () => {
|
||||
if (['application/ld+json', 'application/json'].includes(this.props.mode)) {
|
||||
this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });
|
||||
} else {
|
||||
this.editor.toggleComment();
|
||||
}
|
||||
}
|
||||
},
|
||||
foldOptions: {
|
||||
@@ -281,9 +292,9 @@ export default class CodeEditor extends React.Component {
|
||||
while (end < currentLine.length && /[^{}();\s\[\]\,]/.test(currentLine.charAt(end))) ++end;
|
||||
while (start && /[^{}();\s\[\]\,]/.test(currentLine.charAt(start - 1))) --start;
|
||||
let curWord = start != end && currentLine.slice(start, end);
|
||||
//Qualify if autocomplete will be shown
|
||||
// Qualify if autocomplete will be shown
|
||||
if (
|
||||
/^(?!Shift|Tab|Enter|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|\s)\w*/.test(event.key) &&
|
||||
/^(?!Shift|Tab|Enter|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Meta|Alt|Home|End\s)\w*/.test(event.key) &&
|
||||
curWord.length > 0 &&
|
||||
!/\/\/|\/\*|.*{{|`[^$]*{|`[^{]*$/.test(currentLine.slice(0, end)) &&
|
||||
/(?<!\d)[a-zA-Z\._]$/.test(curWord)
|
||||
@@ -312,7 +323,7 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
if (this.editor) {
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
let variables = getAllVariables(this.props.collection, this.props.item);
|
||||
if (!isEqual(variables, this.variables)) {
|
||||
this.addOverlay();
|
||||
}
|
||||
@@ -352,10 +363,10 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
addOverlay = () => {
|
||||
const mode = this.props.mode || 'application/ld+json';
|
||||
let variables = getEnvironmentVariables(this.props.collection);
|
||||
let variables = getAllVariables(this.props.collection, this.props.item);
|
||||
this.variables = variables;
|
||||
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode);
|
||||
defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting);
|
||||
this.editor.setOption('mode', 'brunovariables');
|
||||
};
|
||||
|
||||
@@ -369,6 +380,10 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
_isSearchOpen = () => {
|
||||
return document.querySelector('.CodeMirror-dialog.CodeMirror-dialog-top');
|
||||
};
|
||||
|
||||
/**
|
||||
* Bind handler to search input to count number of search results
|
||||
*/
|
||||
|
||||
@@ -79,6 +79,15 @@ const AuthMode = ({ collection }) => {
|
||||
>
|
||||
Digest Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onModeChange('ntlm');
|
||||
}}
|
||||
>
|
||||
NTLM Auth
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
@@ -86,7 +95,7 @@ const AuthMode = ({ collection }) => {
|
||||
onModeChange('oauth2');
|
||||
}}
|
||||
>
|
||||
Oauth2
|
||||
OAuth 2.0
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
|
||||
@@ -4,6 +4,7 @@ const Wrapper = styled.div`
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.single-line-editor-wrapper {
|
||||
max-width: 400px;
|
||||
padding: 0.15rem 0.4rem;
|
||||
@@ -0,0 +1,110 @@
|
||||
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 NTLMAuth = ({ collection }) => {
|
||||
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const ntlmAuth = get(collection, 'root.request.auth.ntlm', {});
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
|
||||
const handleUsernameChange = (username) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'ntlm',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: username,
|
||||
password: ntlmAuth.password,
|
||||
domain: ntlmAuth.domain
|
||||
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (password) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'ntlm',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: ntlmAuth.username,
|
||||
password: password,
|
||||
domain: ntlmAuth.domain
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleDomainChange = (domain) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'ntlm',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
username: ntlmAuth.username,
|
||||
password: ntlmAuth.password,
|
||||
domain: domain
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
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={ntlmAuth.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={ntlmAuth.password || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handlePasswordChange(val)}
|
||||
collection={collection}
|
||||
isSecret={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="block font-medium mb-2">Domain</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={ntlmAuth.domain || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleDomainChange(val)}
|
||||
collection={collection}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default NTLMAuth;
|
||||
@@ -1,120 +0,0 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { clearOauth2Cache } from 'utils/network/index';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { callbackUrl, authorizationUrl, accessTokenUrl, clientId, clientSecret, scope, state, pkce } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
state,
|
||||
pkce,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handlePKCEToggle = (e) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code',
|
||||
callbackUrl,
|
||||
authorizationUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
state,
|
||||
pkce: !Boolean(oAuth?.['pkce'])
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleClearCache = (e) => {
|
||||
clearOauth2Cache(collection?.uid)
|
||||
.then(() => {
|
||||
toast.success('cleared cache successfully');
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-row w-full gap-4" key="pkce">
|
||||
<label className="block font-medium">Use PKCE</label>
|
||||
<input
|
||||
className="cursor-pointer"
|
||||
type="checkbox"
|
||||
checked={Boolean(oAuth?.['pkce'])}
|
||||
onChange={handlePKCEToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4">
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
<button onClick={handleClearCache} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Clear Cache
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
||||
@@ -1,33 +0,0 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'callbackUrl',
|
||||
label: 'Callback URL'
|
||||
},
|
||||
{
|
||||
key: 'authorizationUrl',
|
||||
label: 'Authorization URL'
|
||||
},
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
},
|
||||
{
|
||||
key: 'state',
|
||||
label: 'State'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
||||
@@ -1,70 +0,0 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const OAuth2ClientCredentials = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { accessTokenUrl, clientId, clientSecret, scope } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'client_credentials',
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2ClientCredentials;
|
||||
@@ -1,21 +0,0 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
||||
@@ -1,54 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
font-size: 0.8125rem;
|
||||
|
||||
.grant-type-mode-selector {
|
||||
padding: 0.5rem 0px;
|
||||
border-radius: 3px;
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
|
||||
.dropdown {
|
||||
width: fit-content;
|
||||
|
||||
div[data-tippy-root] {
|
||||
width: fit-content;
|
||||
}
|
||||
.tippy-box {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
|
||||
.tippy-content: {
|
||||
width: fit-content;
|
||||
max-width: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grant-type-label {
|
||||
width: fit-content;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
justify-content: space-between;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
padding: 0.2rem 0.6rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140 140 140);
|
||||
}
|
||||
label {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -1,98 +0,0 @@
|
||||
import React, { useRef, forwardRef } from 'react';
|
||||
import get from 'lodash/get';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconCaretDown } from '@tabler/icons';
|
||||
import { updateAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { humanizeGrantType } from 'utils/collections';
|
||||
import { useEffect } from 'react';
|
||||
import { updateCollectionAuth, updateCollectionAuthMode } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const GrantTypeSelector = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const dropdownTippyRef = useRef();
|
||||
const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="flex items-center justify-end grant-type-label select-none">
|
||||
{humanizeGrantType(oAuth?.grantType)} <IconCaretDown className="caret ml-1 mr-1" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const onGrantTypeChange = (grantType) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// initialize redux state with a default oauth2 grant type
|
||||
// authorization_code - default option
|
||||
!oAuth?.grantType &&
|
||||
dispatch(
|
||||
updateCollectionAuthMode({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
!oAuth?.grantType &&
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'authorization_code'
|
||||
}
|
||||
})
|
||||
);
|
||||
}, [oAuth]);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<label className="block font-medium mb-2">Grant Type</label>
|
||||
<div className="inline-flex items-center cursor-pointer grant-type-mode-selector w-fit">
|
||||
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement="bottom-end">
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('password');
|
||||
}}
|
||||
>
|
||||
Password Credentials
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('authorization_code');
|
||||
}}
|
||||
>
|
||||
Authorization Code
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item"
|
||||
onClick={() => {
|
||||
dropdownTippyRef.current.hide();
|
||||
onGrantTypeChange('client_credentials');
|
||||
}}
|
||||
>
|
||||
Client Credentials
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default GrantTypeSelector;
|
||||
@@ -1,72 +0,0 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { saveCollectionRoot, sendCollectionOauth2Request } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { inputsConfig } from './inputsConfig';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const OAuth2AuthorizationCode = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
|
||||
const handleRun = async () => {
|
||||
dispatch(sendCollectionOauth2Request(collection.uid));
|
||||
};
|
||||
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
|
||||
const { accessTokenUrl, username, password, clientId, clientSecret, scope } = oAuth;
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
dispatch(
|
||||
updateCollectionAuth({
|
||||
mode: 'oauth2',
|
||||
collectionUid: collection.uid,
|
||||
content: {
|
||||
grantType: 'password',
|
||||
accessTokenUrl,
|
||||
username,
|
||||
password,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scope,
|
||||
[key]: value
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 flex w-full gap-4 flex-col">
|
||||
{inputsConfig.map((input) => {
|
||||
const { key, label, isSecret } = input;
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-1" key={`input-${key}`}>
|
||||
<label className="block font-medium">{label}</label>
|
||||
<div className="single-line-editor-wrapper">
|
||||
<SingleLineEditor
|
||||
value={oAuth[key] || ''}
|
||||
theme={storedTheme}
|
||||
onSave={handleSave}
|
||||
onChange={(val) => handleChange(key, val)}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
isSecret={isSecret}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<button onClick={handleRun} className="submit btn btn-sm btn-secondary w-fit">
|
||||
Get Access Token
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuth2AuthorizationCode;
|
||||
@@ -1,29 +0,0 @@
|
||||
const inputsConfig = [
|
||||
{
|
||||
key: 'accessTokenUrl',
|
||||
label: 'Access Token URL'
|
||||
},
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Username'
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
label: 'Password'
|
||||
},
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'Client ID'
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'Client Secret',
|
||||
isSecret: true
|
||||
},
|
||||
{
|
||||
key: 'scope',
|
||||
label: 'Scope'
|
||||
}
|
||||
];
|
||||
|
||||
export { inputsConfig };
|
||||
@@ -1,21 +1,33 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import GrantTypeSelector from './GrantTypeSelector/index';
|
||||
import OAuth2PasswordCredentials from './PasswordCredentials/index';
|
||||
import OAuth2AuthorizationCode from './AuthorizationCode/index';
|
||||
import OAuth2ClientCredentials from './ClientCredentials/index';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
|
||||
import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
|
||||
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
|
||||
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
|
||||
|
||||
const GrantTypeComponentMap = ({collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const save = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
};
|
||||
|
||||
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', {});
|
||||
|
||||
const grantTypeComponentMap = (grantType, collection) => {
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
return <OAuth2PasswordCredentials collection={collection} />;
|
||||
return <OAuth2PasswordCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
|
||||
break;
|
||||
case 'authorization_code':
|
||||
return <OAuth2AuthorizationCode collection={collection} />;
|
||||
return <OAuth2AuthorizationCode save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
|
||||
break;
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials collection={collection} />;
|
||||
return <OAuth2ClientCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;
|
||||
break;
|
||||
default:
|
||||
return <div>TBD</div>;
|
||||
@@ -24,12 +36,12 @@ const grantTypeComponentMap = (grantType, collection) => {
|
||||
};
|
||||
|
||||
const OAuth2 = ({ collection }) => {
|
||||
const oAuth = get(collection, 'root.request.auth.oauth2', {});
|
||||
let request = collection.draft ? get(collection, 'draft.request', {}) : get(collection, 'root.request', {});
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-2 w-full">
|
||||
<GrantTypeSelector collection={collection} />
|
||||
{grantTypeComponentMap(oAuth?.grantType, collection)}
|
||||
<GrantTypeSelector request={request} updateAuth={updateCollectionAuth} collection={collection} />
|
||||
<GrantTypeComponentMap collection={collection} />
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div``;
|
||||
const Wrapper = styled.div`
|
||||
max-width: 800px;
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -11,6 +11,8 @@ import ApiKeyAuth from './ApiKeyAuth/';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import OAuth2 from './OAuth2';
|
||||
import NTLMAuth from './NTLMAuth';
|
||||
|
||||
|
||||
const Auth = ({ collection }) => {
|
||||
const authMode = get(collection, 'root.request.auth.mode');
|
||||
@@ -32,6 +34,9 @@ const Auth = ({ collection }) => {
|
||||
case 'digest': {
|
||||
return <DigestAuth collection={collection} />;
|
||||
}
|
||||
case 'ntlm': {
|
||||
return <NTLMAuth collection={collection} />;
|
||||
}
|
||||
case 'oauth2': {
|
||||
return <OAuth2 collection={collection} />;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@ import { useState } from 'react';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useRef } from 'react';
|
||||
import path from 'path';
|
||||
import slash from 'utils/common/slash';
|
||||
import { isWindowsOS } from 'utils/common/platform';
|
||||
import path from 'utils/common/path';
|
||||
|
||||
const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
const certFilePathInputRef = useRef();
|
||||
@@ -27,7 +25,10 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
passphrase: ''
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
domain: Yup.string().required(),
|
||||
domain: Yup.string()
|
||||
.required()
|
||||
.trim()
|
||||
.test('not-empty-after-trim', 'Domain is required', value => value && value.trim().length > 0),
|
||||
type: Yup.string().required().oneOf(['cert', 'pfx']),
|
||||
certFilePath: Yup.string().when('type', {
|
||||
is: (type) => type == 'cert',
|
||||
@@ -47,7 +48,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
let relevantValues = {};
|
||||
if (values.type === 'cert') {
|
||||
relevantValues = {
|
||||
domain: values.domain,
|
||||
domain: values.domain?.trim(),
|
||||
type: values.type,
|
||||
certFilePath: values.certFilePath,
|
||||
keyFilePath: values.keyFilePath,
|
||||
@@ -55,7 +56,7 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
};
|
||||
} else {
|
||||
relevantValues = {
|
||||
domain: values.domain,
|
||||
domain: values.domain?.trim(),
|
||||
type: values.type,
|
||||
pfxFilePath: values.pfxFilePath,
|
||||
passphrase: values.passphrase
|
||||
@@ -68,13 +69,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
});
|
||||
|
||||
const getFile = (e) => {
|
||||
if (e.files?.[0]?.path) {
|
||||
let relativePath;
|
||||
if (isWindowsOS()) {
|
||||
relativePath = slash(path.win32.relative(root, e.files[0].path));
|
||||
} else {
|
||||
relativePath = path.posix.relative(root, e.files[0].path);
|
||||
}
|
||||
const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]);
|
||||
if (filePath) {
|
||||
let relativePath = path.relative(root, filePath);
|
||||
formik.setFieldValue(e.name, relativePath);
|
||||
}
|
||||
};
|
||||
@@ -108,23 +105,23 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<ul className="mt-4">
|
||||
{!clientCertConfig.length
|
||||
? 'No client certificates added'
|
||||
: clientCertConfig.map((clientCert) => (
|
||||
<li key={uuid()} className="flex items-center available-certificates p-2 rounded-lg mb-2">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<div className="flex w-full items-center">
|
||||
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
|
||||
{clientCert.domain}
|
||||
</div>
|
||||
<div className="flex w-full items-center">
|
||||
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
|
||||
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
|
||||
</div>
|
||||
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
|
||||
<IconTrash size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
: clientCertConfig.map((clientCert, index) => (
|
||||
<li key={`client-cert-${index}`} className="flex items-center available-certificates p-2 rounded-lg mb-2">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<div className="flex w-full items-center">
|
||||
<IconWorld className="mr-2" size={18} strokeWidth={1.5} />
|
||||
{clientCert.domain}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
<div className="flex w-full items-center">
|
||||
<IconCertificate className="mr-2 flex-shrink-0" size={18} strokeWidth={1.5} />
|
||||
{clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}
|
||||
</div>
|
||||
<button onClick={() => onRemove(clientCert)} className="remove-certificate ml-2">
|
||||
<IconTrash size={18} strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<h1 className="font-semibold mt-8 mb-2">Add Client Certificate</h1>
|
||||
@@ -133,15 +130,20 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<label className="settings-label" htmlFor="domain">
|
||||
Domain
|
||||
</label>
|
||||
<input
|
||||
id="domain"
|
||||
type="text"
|
||||
name="domain"
|
||||
placeholder="*.example.org"
|
||||
className="block textbox non-passphrase-input"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.domain || ''}
|
||||
/>
|
||||
<div className="relative flex items-center">
|
||||
<div className="absolute left-0 pl-2 text-gray-400 pointer-events-none flex items-center h-full">
|
||||
https://
|
||||
</div>
|
||||
<input
|
||||
id="domain"
|
||||
type="text"
|
||||
name="domain"
|
||||
placeholder="example.org"
|
||||
className="block textbox non-passphrase-input !pl-[60px]"
|
||||
onChange={formik.handleChange}
|
||||
value={formik.values.domain || ''}
|
||||
/>
|
||||
</div>
|
||||
{formik.touched.domain && formik.errors.domain ? (
|
||||
<div className="ml-1 text-red-500">{formik.errors.domain}</div>
|
||||
) : null}
|
||||
@@ -197,9 +199,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<div
|
||||
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
|
||||
title={path.basename(slash(formik.values.certFilePath))}
|
||||
title={path.basename(formik.values.certFilePath)}
|
||||
>
|
||||
{path.basename(slash(formik.values.certFilePath))}
|
||||
{path.basename(formik.values.certFilePath)}
|
||||
</div>
|
||||
<IconTrash
|
||||
size={18}
|
||||
@@ -237,9 +239,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<div
|
||||
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
|
||||
title={path.basename(slash(formik.values.keyFilePath))}
|
||||
title={path.basename(formik.values.keyFilePath)}
|
||||
>
|
||||
{path.basename(slash(formik.values.keyFilePath))}
|
||||
{path.basename(formik.values.keyFilePath)}
|
||||
</div>
|
||||
<IconTrash
|
||||
size={18}
|
||||
@@ -280,9 +282,9 @@ const ClientCertSettings = ({ root, clientCertConfig, onUpdate, onRemove }) => {
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<div
|
||||
className="my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]"
|
||||
title={path.basename(slash(formik.values.pfxFilePath))}
|
||||
title={path.basename(formik.values.pfxFilePath)}
|
||||
>
|
||||
{path.basename(slash(formik.values.pfxFilePath))}
|
||||
{path.basename(formik.values.pfxFilePath)}
|
||||
</div>
|
||||
<IconTrash
|
||||
size={18}
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
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};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/acti
|
||||
import Markdown from 'components/MarkDown';
|
||||
import CodeEditor from 'components/CodeEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { IconEdit, IconX, IconFileText } from '@tabler/icons';
|
||||
|
||||
const Docs = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -29,19 +30,50 @@ const Docs = ({ collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleDiscardChanges = () => {
|
||||
dispatch(
|
||||
updateCollectionDocs({
|
||||
collectionUid: collection.uid,
|
||||
docs: docs
|
||||
})
|
||||
);
|
||||
toggleViewMode();
|
||||
}
|
||||
|
||||
const onSave = () => {
|
||||
dispatch(saveCollectionRoot(collection.uid));
|
||||
toggleViewMode();
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-1 h-full w-full relative">
|
||||
<div className="editing-mode mb-2" role="tab" onClick={toggleViewMode}>
|
||||
{isEditing ? 'Preview' : 'Edit'}
|
||||
<StyledWrapper className="mt-1 h-full w-full relative flex flex-col">
|
||||
<div className='flex flex-row w-full justify-between items-center mb-4'>
|
||||
<div className='text-lg font-medium flex items-center gap-2'>
|
||||
<IconFileText size={20} strokeWidth={1.5} />
|
||||
Documentation
|
||||
</div>
|
||||
<div className='flex flex-row gap-2 items-center justify-center'>
|
||||
{isEditing ? (
|
||||
<>
|
||||
<div className="editing-mode" role="tab" onClick={handleDiscardChanges}>
|
||||
<IconX className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={onSave}>
|
||||
Save
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
|
||||
<IconEdit className="cursor-pointer" size={20} strokeWidth={1.5} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
theme={displayedTheme}
|
||||
value={docs || ''}
|
||||
value={docs}
|
||||
onEdit={onEdit}
|
||||
onSave={onSave}
|
||||
mode="application/text"
|
||||
@@ -49,10 +81,44 @@ const Docs = ({ collection }) => {
|
||||
fontSize={get(preferences, 'font.codeFontSize')}
|
||||
/>
|
||||
) : (
|
||||
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
|
||||
<div className='h-full overflow-auto pl-1'>
|
||||
<div className='h-[1px] min-h-[500px]'>
|
||||
{
|
||||
docs?.length > 0 ?
|
||||
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
|
||||
:
|
||||
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={documentationPlaceholder} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Docs;
|
||||
|
||||
|
||||
const documentationPlaceholder = `
|
||||
Welcome to your collection documentation! This space is designed to help you document your API collection effectively.
|
||||
|
||||
## Overview
|
||||
Use this section to provide a high-level overview of your collection. You can describe:
|
||||
- The purpose of these API endpoints
|
||||
- Key features and functionalities
|
||||
- Target audience or users
|
||||
|
||||
## Best Practices
|
||||
- Keep documentation up to date
|
||||
- Include request/response examples
|
||||
- Document error scenarios
|
||||
- Add relevant links and references
|
||||
|
||||
## Markdown Support
|
||||
This documentation supports Markdown formatting! You can use:
|
||||
- **Bold** and *italic* text
|
||||
- \`code blocks\` and syntax highlighting
|
||||
- Tables and lists
|
||||
- [Links](https://usebruno.com)
|
||||
- And more!
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { getTotalRequestCountInCollection } from 'utils/collections/';
|
||||
|
||||
const Info = ({ collection }) => {
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col h-full">
|
||||
<div className="text-xs mb-4 text-muted">General information about the collection.</div>
|
||||
<table className="w-full border-collapse">
|
||||
<tbody>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Name :</td>
|
||||
<td className="py-2 px-2">{collection.name}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Location :</td>
|
||||
<td className="py-2 px-2 break-all">{collection.pathname}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Ignored files :</td>
|
||||
<td className="py-2 px-2 break-all">{collection.brunoConfig?.ignore?.map((x) => `'${x}'`).join(', ')}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Environments :</td>
|
||||
<td className="py-2 px-2">{collection.environments?.length || 0}</td>
|
||||
</tr>
|
||||
<tr className="">
|
||||
<td className="py-2 px-2 text-right">Requests :</td>
|
||||
<td className="py-2 px-2">{totalRequestsInCollection}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Info;
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from "react";
|
||||
import { getTotalRequestCountInCollection } from 'utils/collections/';
|
||||
import { IconFolder, IconWorld, IconApi, IconShare } from '@tabler/icons';
|
||||
import { areItemsLoading, getItemsLoadStats } from "utils/collections/index";
|
||||
import { useState } from "react";
|
||||
import ShareCollection from "components/ShareCollection/index";
|
||||
|
||||
const Info = ({ collection }) => {
|
||||
const totalRequestsInCollection = getTotalRequestCountInCollection(collection);
|
||||
|
||||
const isCollectionLoading = areItemsLoading(collection);
|
||||
const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);
|
||||
const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);
|
||||
|
||||
const handleToggleShowShareCollectionModal = (value) => (e) => {
|
||||
toggleShowShareCollectionModal(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col h-fit">
|
||||
<div className="rounded-lg py-6">
|
||||
<div className="grid gap-6">
|
||||
{/* Location Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<IconFolder className="w-5 h-5 text-blue-500" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-semibold text-sm">Location</div>
|
||||
<div className="mt-1 text-sm text-muted break-all">
|
||||
{collection.pathname}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Environments Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||
<IconWorld className="w-5 h-5 text-green-500" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-semibold text-sm">Environments</div>
|
||||
<div className="mt-1 text-sm text-muted">
|
||||
{collection.environments?.length || 0} environment{collection.environments?.length !== 1 ? 's' : ''} configured
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Requests Row */}
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
|
||||
<IconApi className="w-5 h-5 text-purple-500" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="font-semibold text-sm">Requests</div>
|
||||
<div className="mt-1 text-sm text-muted font-mono">
|
||||
{
|
||||
isCollectionLoading? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start group cursor-pointer" onClick={handleToggleShowShareCollectionModal(true)}>
|
||||
<div className="flex-shrink-0 p-3 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg">
|
||||
<IconShare className="w-5 h-5 text-indigo-500" stroke={1.5} />
|
||||
</div>
|
||||
<div className="ml-4 h-full flex flex-col justify-start">
|
||||
<div className="font-semibold text-sm h-fit my-auto">Share</div>
|
||||
<div className="mt-1 text-sm group-hover:underline text-link">
|
||||
Share Collection
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showShareCollectionModal && <ShareCollection collectionUid={collection.uid} onClose={handleToggleShowShareCollectionModal(false)} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Info;
|
||||
@@ -0,0 +1,25 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
&.card {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.card.bg};
|
||||
|
||||
.title {
|
||||
border-top: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-left: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
border-right: 1px solid ${(props) => props.theme.requestTabPanel.cardTable.border};
|
||||
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
|
||||
.table {
|
||||
thead {
|
||||
background-color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.bg};
|
||||
color: ${(props) => props.theme.requestTabPanel.cardTable.table.thead.color};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { flattenItems } from "utils/collections";
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { isItemARequest, itemIsOpenedInTabs } from 'utils/tabs/index';
|
||||
import { getDefaultRequestPaneTab } from 'utils/collections/index';
|
||||
import { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { hideHomePage } from 'providers/ReduxStore/slices/app';
|
||||
|
||||
const RequestsNotLoaded = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const flattenedItems = flattenItems(collection.items);
|
||||
const itemsFailedLoading = flattenedItems?.filter(item => item?.partial && !item?.loading);
|
||||
|
||||
if (!itemsFailedLoading?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleRequestClick = (item) => e => {
|
||||
e.preventDefault();
|
||||
if (isItemARequest(item)) {
|
||||
dispatch(hideHomePage());
|
||||
if (itemIsOpenedInTabs(item, tabs)) {
|
||||
dispatch(
|
||||
focusTab({
|
||||
uid: item.uid
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
dispatch(
|
||||
addTab({
|
||||
uid: item.uid,
|
||||
collectionUid: collection.uid,
|
||||
requestPaneTab: getDefaultRequestPaneTab(item)
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full card my-2">
|
||||
<div className="flex items-center gap-2 px-3 py-2 title bg-yellow-50 dark:bg-yellow-900/20">
|
||||
<IconAlertTriangle size={16} className="text-yellow-500" />
|
||||
<span className="font-medium">Following requests were not loaded</span>
|
||||
</div>
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-2 px-3 text-left font-medium">
|
||||
Pathname
|
||||
</th>
|
||||
<th className="py-2 px-3 text-left font-medium">
|
||||
Size
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{flattenedItems?.map((item, index) => (
|
||||
item?.partial && !item?.loading ? (
|
||||
<tr key={index} className='cursor-pointer' onClick={handleRequestClick(item)}>
|
||||
<td className="py-1.5 px-3">
|
||||
{item?.pathname?.split(`${collection?.pathname}/`)?.[1]}
|
||||
</td>
|
||||
<td className="py-1.5 px-3">
|
||||
{item?.size?.toFixed?.(2)} MB
|
||||
</td>
|
||||
</tr>
|
||||
) : null
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestsNotLoaded;
|
||||
@@ -0,0 +1,25 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.partial {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.completed {
|
||||
color: ${(props) => props.theme.colors.text.green};
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.failed {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
opacity: 0.8;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,27 @@
|
||||
import StyledWrapper from "./StyledWrapper";
|
||||
import Docs from "../Docs";
|
||||
import Info from "./Info";
|
||||
import { IconBox } from '@tabler/icons';
|
||||
import RequestsNotLoaded from "./RequestsNotLoaded";
|
||||
|
||||
const Overview = ({ collection }) => {
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div className="grid grid-cols-5 gap-4 h-full">
|
||||
<div className="col-span-2">
|
||||
<div className="text-xl font-semibold flex items-center gap-2">
|
||||
<IconBox size={24} stroke={1.5} />
|
||||
{collection?.name}
|
||||
</div>
|
||||
<Info collection={collection} />
|
||||
<RequestsNotLoaded collection={collection} />
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Docs collection={collection} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Overview;
|
||||
@@ -1,6 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
.settings-label {
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
@@ -104,18 +104,15 @@ const ProxySettings = ({ proxyConfig, onUpdate }) => {
|
||||
<div className="mb-3 flex items-center">
|
||||
<label className="settings-label flex items-center" htmlFor="enabled">
|
||||
Config
|
||||
<InfoTip
|
||||
text={`
|
||||
<InfoTip infotipId="request-var">
|
||||
<div>
|
||||
<ul>
|
||||
<li><span style="width: 50px;display:inline-block;">global</span> - use global proxy config</li>
|
||||
<li><span style="width: 50px;display:inline-block;">enabled</span> - use collection proxy config</li>
|
||||
<li><span style="width: 50px;display:inline-block;">disable</span> - disable proxy</li>
|
||||
<li><span style={{width: "50px", display: "inline-block"}}>global</span> - use global proxy config</li>
|
||||
<li><span style={{width: "50px", display: "inline-block"}}>enabled</span> - use collection proxy config</li>
|
||||
<li><span style={{width: "50px", display: "inline-block"}}>disable</span> - disable proxy</li>
|
||||
</ul>
|
||||
</div>
|
||||
`}
|
||||
infotipId="request-var"
|
||||
/>
|
||||
</InfoTip>
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<label className="flex items-center">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
div.CodeMirror {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
div.tabs {
|
||||
div.tab {
|
||||
padding: 6px 0px;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div``;
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
max-width: 800px;
|
||||
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ const VarsTable = ({ collection, vars, varType }) => {
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
<InfoTip content="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
|
||||
@@ -12,12 +12,11 @@ import Headers from './Headers';
|
||||
import Auth from './Auth';
|
||||
import Script from './Script';
|
||||
import Test from './Tests';
|
||||
import Docs from './Docs';
|
||||
import Presets from './Presets';
|
||||
import Info from './Info';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars/index';
|
||||
import DotIcon from 'components/Icons/Dot';
|
||||
import Overview from './Overview/index';
|
||||
|
||||
const ContentIndicator = () => {
|
||||
return (
|
||||
@@ -40,7 +39,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
};
|
||||
|
||||
const root = collection?.root;
|
||||
const hasScripts = root?.request?.script.res || root?.request?.script.req;
|
||||
const hasScripts = root?.request?.script?.res || root?.request?.script?.req;
|
||||
const hasTests = root?.request?.tests;
|
||||
const hasDocs = root?.docs;
|
||||
|
||||
@@ -50,7 +49,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
const requestVars = get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = get(collection, 'root.request.vars.res', []);
|
||||
const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;
|
||||
const auth = get(collection, 'root.request.auth', {}).mode;
|
||||
const authMode = get(collection, 'root.request.auth', {}).mode || 'none';
|
||||
|
||||
const proxyConfig = get(collection, 'brunoConfig.proxy', {});
|
||||
const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);
|
||||
@@ -97,6 +96,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'overview': {
|
||||
return <Overview collection={collection} />;
|
||||
}
|
||||
case 'headers': {
|
||||
return <Headers collection={collection} />;
|
||||
}
|
||||
@@ -128,12 +130,6 @@ const CollectionSettings = ({ collection }) => {
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'docs': {
|
||||
return <Docs collection={collection} />;
|
||||
}
|
||||
case 'info': {
|
||||
return <Info collection={collection} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -146,6 +142,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
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('overview')} role="tab" onClick={() => setTab('overview')}>
|
||||
Overview
|
||||
</div>
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
|
||||
Headers
|
||||
{activeHeadersCount > 0 && <sup className="ml-1 font-medium">{activeHeadersCount}</sup>}
|
||||
@@ -156,7 +155,7 @@ const CollectionSettings = ({ collection }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
{auth !== 'none' && <ContentIndicator />}
|
||||
{authMode !== 'none' && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => setTab('script')}>
|
||||
Script
|
||||
@@ -177,13 +176,6 @@ const CollectionSettings = ({ collection }) => {
|
||||
Client Certificates
|
||||
{clientCertConfig.length > 0 && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
|
||||
Docs
|
||||
{hasDocs && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('info')} role="tab" onClick={() => setTab('info')}>
|
||||
Info
|
||||
</div>
|
||||
</div>
|
||||
<section className="mt-4 h-full">{getTabPanel(tab)}</section>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div``;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,371 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import Modal from 'components/Modal/index';
|
||||
import { modifyCookie, addCookie, getParsedCookie, createCookieString } from 'providers/ReduxStore/slices/app';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import toast from 'react-hot-toast';
|
||||
import ToggleSwitch from 'components/ToggleSwitch/index';
|
||||
import { IconInfoCircle } from '@tabler/icons';
|
||||
import moment from 'moment';
|
||||
import 'moment-timezone';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
const removeEmptyValues = (obj) => {
|
||||
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== null && value !== undefined));
|
||||
};
|
||||
|
||||
const ModifyCookieModal = ({ onClose, domain, cookie }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [isRawMode, setIsRawMode] = useState(false);
|
||||
const [cookieString, setCookieString] = useState('');
|
||||
const initialParseRef = useRef(false);
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
...(cookie ? cookie : {}),
|
||||
key: cookie?.key || '',
|
||||
value: cookie?.value || '',
|
||||
path: cookie?.path || '/',
|
||||
domain: cookie?.domain || domain || '',
|
||||
expires: cookie?.expires ? moment(cookie.expires).format(moment.HTML5_FMT.DATETIME_LOCAL) : '',
|
||||
secure: cookie?.secure || false,
|
||||
httpOnly: cookie?.httpOnly || false
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
key: Yup.string().required('Key is required'),
|
||||
value: Yup.string().required('Value is required'),
|
||||
domain: Yup.string().required('Domain is required'),
|
||||
secure: Yup.boolean(),
|
||||
httpOnly: Yup.boolean(),
|
||||
expires: Yup.mixed()
|
||||
.nullable()
|
||||
.transform((value) => {
|
||||
if (!value || value === '') return null;
|
||||
return moment(value).isValid() ? moment(value).toDate() : null;
|
||||
})
|
||||
.test('future-date', 'Expiration date must be in the future', (value) => {
|
||||
if (!value) return true;
|
||||
return moment(value).isAfter(moment());
|
||||
})
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
const modValues = removeEmptyValues({
|
||||
...(cookie ? cookie : {}),
|
||||
...values,
|
||||
expires: values.expires
|
||||
? moment(values.expires).isValid()
|
||||
? moment(values.expires).toDate()
|
||||
: Infinity
|
||||
: Infinity
|
||||
});
|
||||
|
||||
handleCookieDispatch(cookie, domain, modValues, onClose);
|
||||
}
|
||||
});
|
||||
|
||||
const title = cookie ? 'Modify Cookie' : 'Add Cookie';
|
||||
|
||||
const handleCookieDispatch = (cookie, domain, modValues, onClose) => {
|
||||
if (cookie) {
|
||||
dispatch(modifyCookie(domain, cookie, modValues))
|
||||
.then(() => {
|
||||
toast.success('Cookie modified successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('An error occurred while modifying cookie');
|
||||
console.error(err);
|
||||
});
|
||||
} else {
|
||||
dispatch(addCookie(domain, modValues))
|
||||
.then(() => {
|
||||
toast.success('Cookie added successfully');
|
||||
onClose();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('An error occurred while adding cookie');
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
try {
|
||||
if (isRawMode) {
|
||||
const cookieObj = await dispatch(getParsedCookie(cookieString));
|
||||
|
||||
const modifiedCookie = removeEmptyValues({
|
||||
...formik.values,
|
||||
...cookieObj,
|
||||
expires: cookieObj?.expires
|
||||
? moment(cookieObj.expires).isValid()
|
||||
? moment(cookieObj.expires).toDate()
|
||||
: Infinity
|
||||
: Infinity
|
||||
});
|
||||
|
||||
if (!cookieObj) {
|
||||
toast.error('Please enter a valid cookie string');
|
||||
return;
|
||||
}
|
||||
|
||||
const validationErrors = await formik.setValues(
|
||||
(values) => ({
|
||||
...values,
|
||||
...modifiedCookie,
|
||||
expires:
|
||||
modifiedCookie?.expires && moment(modifiedCookie.expires).isValid()
|
||||
? moment(new Date(modifiedCookie.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL)
|
||||
: ''
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
if (!isEmpty(validationErrors)) {
|
||||
toast.error(Object.values(validationErrors).join("\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
handleCookieDispatch(cookie, domain, modifiedCookie, onClose);
|
||||
} else {
|
||||
formik.handleSubmit();
|
||||
}
|
||||
} catch (error) {
|
||||
const errMsg = error.message || 'An error occurred while parsing cookie string';
|
||||
toast.error(errMsg);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRawMode) return;
|
||||
const loadCookieString = async () => {
|
||||
if (cookie) {
|
||||
const str = await dispatch(createCookieString(cookie));
|
||||
setCookieString(str);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
loadCookieString();
|
||||
}, [cookie, isRawMode]);
|
||||
|
||||
// create the cookieString when raw mode is enabled
|
||||
useEffect(() => {
|
||||
if (isRawMode) {
|
||||
const createCookieStr = async () => {
|
||||
const str = await dispatch(createCookieString(formik.values));
|
||||
setCookieString(str);
|
||||
};
|
||||
|
||||
createCookieStr();
|
||||
}
|
||||
}, [isRawMode, formik.values]);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset the ref when raw mode changes
|
||||
if (isRawMode) {
|
||||
initialParseRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const setParsedCookie = async () => {
|
||||
if (!isRawMode && cookieString && !initialParseRef.current) {
|
||||
initialParseRef.current = true;
|
||||
|
||||
try {
|
||||
const cookieObj = await dispatch(getParsedCookie(cookieString));
|
||||
|
||||
if (!cookieObj) return;
|
||||
|
||||
formik.setValues(
|
||||
(values) => ({
|
||||
...values,
|
||||
...removeEmptyValues(cookieObj),
|
||||
expires:
|
||||
cookieObj?.expires && moment(cookieObj.expires).isValid()
|
||||
? moment(new Date(cookieObj.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL)
|
||||
: ''
|
||||
}),
|
||||
true
|
||||
);
|
||||
} catch (error) {
|
||||
const errMsg = error.message || 'An error occurred while parsing cookie string';
|
||||
toast.error(errMsg);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setParsedCookie();
|
||||
}, [isRawMode, cookieString, dispatch, formik]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="lg"
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
handleCancel={onClose}
|
||||
handleConfirm={onSubmit}
|
||||
customHeader={
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<h2 className="text-sm font-bold">{title}</h2>
|
||||
<div className="ml-auto flex items-center ">
|
||||
<ToggleSwitch
|
||||
className="mr-2"
|
||||
isOn={isRawMode}
|
||||
size="2xs"
|
||||
handleToggle={(e) => {
|
||||
setIsRawMode(e.target.checked);
|
||||
}}
|
||||
/>
|
||||
<label className="text-sm font-normal mr-4 normal-case">Edit Raw</label>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<form onSubmit={(e) => e.preventDefault()} className="px-2">
|
||||
{isRawMode ? (
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<label className="block text-sm">Set-Cookie String</label>
|
||||
<IconInfoCircle id="cookie-raw-info" size={16} strokeWidth={1.5} className="text-gray-400" />
|
||||
<Tooltip
|
||||
anchorId="cookie-raw-info"
|
||||
className="tooltip-mod"
|
||||
html="Key, Path, and Domain are immutable properties and cannot be modified for existing cookies"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
value={cookieString}
|
||||
onChange={(e) => setCookieString(e.target.value)}
|
||||
className="block textbox w-full h-24"
|
||||
placeholder="key=value; key2=value2"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Domain<span className="text-red-600">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="domain"
|
||||
// Auto-focus if its add-new i.e. when domain prop is empty
|
||||
autoFocus={!domain && !formik.values.domain}
|
||||
value={formik.values.domain}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.domain && formik.errors.domain && (
|
||||
<div className="text-red-500 text-sm mt-1">{formik.errors.domain}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Path</label>
|
||||
<input
|
||||
type="text"
|
||||
name="path"
|
||||
value={formik.values.path}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.path && formik.errors.path && (
|
||||
<div className="text-red-500 text-sm mt-1">{formik.errors.path}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Key<span className="text-red-600">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="key"
|
||||
// Auto focus when add-for-domain i.e. if domain is already prefilled
|
||||
autoFocus={!!domain && !formik.values.key}
|
||||
value={formik.values.key}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full disabled:opacity-50"
|
||||
disabled={!!cookie}
|
||||
/>
|
||||
{formik.touched.key && formik.errors.key && (
|
||||
<div className="text-red-500 text-sm mt-1">{formik.errors.key}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm mb-1">
|
||||
Value<span className="text-red-600">*</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="value"
|
||||
// Auto-focus when its in edit mode i.e. cookie prop is present
|
||||
autoFocus={!!cookie}
|
||||
value={formik.values.value}
|
||||
onChange={formik.handleChange}
|
||||
className="block textbox non-passphrase-input w-full"
|
||||
/>
|
||||
{formik.touched.value && formik.errors.value && (
|
||||
<div className="text-red-500 text-sm mt-1">{formik.errors.value}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date Picker */}
|
||||
<div className="w-full flex items-end">
|
||||
<div>
|
||||
<label className="block text-sm mb-1">Expiration ({moment.tz.guess()})</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
name="expires"
|
||||
value={formik.values.expires}
|
||||
onChange={(e) => {
|
||||
formik.handleChange(e);
|
||||
}}
|
||||
className="block textbox non-passphrase-input w-full"
|
||||
min={moment().format(moment.HTML5_FMT.DATETIME_LOCAL)}
|
||||
/>
|
||||
{formik.touched.expires && formik.errors.expires && (
|
||||
<div className="text-red-500 text-sm mt-1">{formik.errors.expires}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Checkboxes */}
|
||||
<div className="flex space-x-4 ml-auto">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="secure"
|
||||
checked={formik.values.secure}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm">Secure</span>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="httpOnly"
|
||||
checked={formik.values.httpOnly}
|
||||
onChange={formik.handleChange}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm">HTTP Only</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModifyCookieModal;
|
||||
@@ -11,6 +11,65 @@ const Wrapper = styled.div`
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.header {
|
||||
input {
|
||||
padding: 0.3rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.textbox {
|
||||
line-height: 1.42857143;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.45rem;
|
||||
box-shadow: none;
|
||||
border-radius: 0px;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
transition: border-color ease-in-out 0.1s;
|
||||
border-radius: 3px;
|
||||
background-color: ${(props) => props.theme.modal.input.bg};
|
||||
border: 1px solid ${(props) => props.theme.modal.input.border};
|
||||
|
||||
&:focus {
|
||||
border: solid 1px ${(props) => props.theme.modal.input.focusBorder} !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-box {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
|
||||
background:
|
||||
/* Shadow Cover TOP */
|
||||
linear-gradient(
|
||||
${(props) => props.theme.modal.body.bg} 20%,
|
||||
rgba(255, 255, 255, 0)
|
||||
) center top,
|
||||
|
||||
/* Shadow Cover BOTTOM */
|
||||
linear-gradient(
|
||||
rgba(255, 255, 255, 0),
|
||||
${(props) => props.theme.modal.body.bg} 80%
|
||||
) center bottom,
|
||||
|
||||
/* Shadow TOP */
|
||||
linear-gradient(
|
||||
rgba(0, 0, 0, 0.1) 0%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
) center top,
|
||||
|
||||
/* Shadow BOTTOM */
|
||||
linear-gradient(
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.1) 100%
|
||||
) center bottom;
|
||||
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 30px, 100% 30px, 100% 10px, 100% 10px;
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,53 +1,331 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import Modal from 'components/Modal';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { deleteCookiesForDomain } from 'providers/ReduxStore/slices/app';
|
||||
import Accordion from 'components/Accordion/index';
|
||||
import { IconTrash, IconEdit, IconCirclePlus, IconCookieOff, IconAlertTriangle, IconSearch } from '@tabler/icons';
|
||||
import { deleteCookiesForDomain, deleteCookie } from 'providers/ReduxStore/slices/app';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
import ModifyCookieModal from 'components/Cookies/ModifyCookieModal/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import moment from 'moment';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
|
||||
const ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (
|
||||
<Modal onClose={onClose} handleCancel={onClose} title="Clear Domain Cookies" hideFooter={true}>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
Are you sure you want to clear all cookies for the domain {domain}?
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-close" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onClear}>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const DeleteCookieModal = ({ onClose, cookieName, onDelete }) => (
|
||||
<Modal onClose={onClose} handleCancel={onClose} title="Delete Cookie" hideFooter={true}>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
Are you sure you want to delete the cookie {cookieName}?
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-close" onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={onDelete}>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
const CollectionProperties = ({ onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const cookies = useSelector((state) => state.app.cookies) || [];
|
||||
const [isModifyCookieModalOpen, setIsModifyCookieModalOpen] = useState(false);
|
||||
const [currentDomain, setCurrentDomain] = useState(null);
|
||||
const [cookieToEdit, setCookieToEdit] = useState(null);
|
||||
|
||||
const handleDeleteDomain = (domain) => {
|
||||
dispatch(deleteCookiesForDomain(domain))
|
||||
.then(() => {
|
||||
toast.success('Domain deleted successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to delete domain'));
|
||||
const [domainToClear, setDomainToClear] = useState(null);
|
||||
const [cookieToDelete, setCookieToDelete] = useState(null);
|
||||
const [searchText, setSearchText] = useState(null);
|
||||
|
||||
const handleAddCookie = (domain) => {
|
||||
if(domain) setCurrentDomain(domain);
|
||||
setIsModifyCookieModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEditCookie = (domain, cookie) => {
|
||||
setCurrentDomain(domain);
|
||||
setCookieToEdit(cookie);
|
||||
setIsModifyCookieModalOpen(true);
|
||||
};
|
||||
|
||||
const handleClearDomainCookies = (domain) => {
|
||||
setDomainToClear(domain);
|
||||
};
|
||||
|
||||
const clearDomainCookiesAction = () => {
|
||||
dispatch(deleteCookiesForDomain(domainToClear))
|
||||
.then(() => {
|
||||
toast.success('Domain cookies cleared successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to clear domain cookies'));
|
||||
setDomainToClear(null);
|
||||
};
|
||||
|
||||
const handleDeleteCookie = (domain, path, key) => {
|
||||
setCookieToDelete({ key, domain, path });
|
||||
};
|
||||
|
||||
const deleteCookieAction = () => {
|
||||
if (cookieToDelete) {
|
||||
const { domain, path, key } = cookieToDelete;
|
||||
dispatch(deleteCookie(domain, path, key))
|
||||
.then(() => {
|
||||
toast.success('Cookie deleted successfully');
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('Failed to delete cookie'));
|
||||
}
|
||||
setCookieToDelete(null);
|
||||
};
|
||||
|
||||
const filteredCookies = useMemo(() => {
|
||||
if (!searchText) return cookies;
|
||||
|
||||
return cookies.filter((cookie) =>
|
||||
cookie.domain.toLowerCase().includes(searchText.toLowerCase())
|
||||
);
|
||||
}, [cookies, searchText]);
|
||||
|
||||
const shouldShowHeader = cookies && cookies.length > 0;
|
||||
|
||||
return (
|
||||
<Modal size="md" title="Cookies" hideFooter={true} handleCancel={onClose}>
|
||||
<StyledWrapper>
|
||||
<table className="w-full border-collapse" style={{ marginTop: '-1rem' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-2 px-2 text-left">Domain</th>
|
||||
<th className="py-2 px-2 text-left">Cookie</th>
|
||||
<th className="py-2 px-2 text-center" style={{ width: 80 }}>
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cookies.map((cookie) => (
|
||||
<tr key={cookie.domain}>
|
||||
<td className="py-2 px-2">{cookie.domain}</td>
|
||||
<td className="py-2 px-2 break-all">{cookie.cookieString}</td>
|
||||
<td className="text-center">
|
||||
<button tabIndex="-1" onClick={() => handleDeleteDomain(cookie.domain)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
<>
|
||||
<Modal
|
||||
size="xl"
|
||||
title="Cookies"
|
||||
hideFooter={true}
|
||||
handleCancel={onClose}
|
||||
customHeader={shouldShowHeader ? (
|
||||
<StyledWrapper className="header flex items-center justify-between w-full">
|
||||
<h2 className="text-xs font-medium">Cookies</h2>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search by domain"
|
||||
value={searchText || ''}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
className="block textbox non-passphrase-input ml-auto font-normal"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-sm btn-secondary flex items-center gap-1 mx-4 font-medium"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddCookie();
|
||||
}}
|
||||
>
|
||||
<IconCirclePlus strokeWidth={1.5} size={16} />
|
||||
<span>Add Cookie</span>
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
) : null}
|
||||
>
|
||||
<StyledWrapper>
|
||||
{!cookies || !cookies.length ? (
|
||||
// No cookies found
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<IconCookieOff size={48} strokeWidth={1.5} className="text-gray-500" />
|
||||
<h2 className="text-lg font-semibold mt-4">No cookies found</h2>
|
||||
<p className="text-gray-500 mt-2">Add cookies to get started</p>
|
||||
<button
|
||||
type="submit"
|
||||
className="submit btn btn-sm btn-secondary flex items-center gap-1 mt-8"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddCookie();
|
||||
}}
|
||||
>
|
||||
<IconCirclePlus strokeWidth={1.5} size={16} />
|
||||
<span>Add Cookie</span>
|
||||
</button>
|
||||
</div>
|
||||
) : cookies.length && !filteredCookies.length ? (
|
||||
// No search results
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<IconSearch size={48} />
|
||||
<h2 className="text-lg font-semibold mt-4">No search results</h2>
|
||||
<p className="text-gray-500 mt-2">Try a different search term</p>
|
||||
</div>
|
||||
) : (
|
||||
// Show cookies list
|
||||
<div className="scroll-box">
|
||||
<Accordion defaultIndex={0}>
|
||||
{filteredCookies.map((domainWithCookies, i) => (
|
||||
<Accordion.Item key={i} index={i}>
|
||||
<Accordion.Header index={i} className="flex items-center">
|
||||
<div className="flex items-center">
|
||||
<span>{domainWithCookies.domain}</span>
|
||||
<span className="ml-2 text-xs dark:text-gray-300 text-gray-500">
|
||||
({domainWithCookies.cookies.length}{' '}
|
||||
{domainWithCookies.cookies.length === 1 ? 'cookie' : 'cookies'})
|
||||
</span>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center gap-1 text-gray-500 hover:text-gray-950 dark:text-white dark:hover:text-gray-300"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAddCookie(domainWithCookies.domain);
|
||||
}}
|
||||
>
|
||||
<IconCirclePlus strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClearDomainCookies(domainWithCookies.domain);
|
||||
}}
|
||||
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600 mr-2"
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content index={i}>
|
||||
<div className="flex items-center justify-between">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="text-left border-b border-gray-200 dark:border-neutral-600 text-gray-700 dark:text-gray-300">
|
||||
<th className="py-2 px-4 font-semibold w-32">Name</th>
|
||||
<th className="py-2 px-4 font-semibold w-52">Value</th>
|
||||
<th className="py-2 px-4 font-semibold">Path</th>
|
||||
<th className="py-2 px-4 font-semibold">Expires</th>
|
||||
<th className="py-2 px-4 font-semibold text-center">Secure</th>
|
||||
<th className="py-2 px-4 font-semibold text-center">HTTP Only</th>
|
||||
<th className="py-2 px-4 font-semibold text-right w-24">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{domainWithCookies.cookies.map((cookie) => (
|
||||
<tr key={cookie.key} className="border-b border-gray-200 dark:border-neutral-600 last:border-none">
|
||||
<td className="py-2 px-4 truncate">
|
||||
<span id={`cookie-key-${cookie.key}`}>{cookie.key}</span>
|
||||
<Tooltip
|
||||
anchorId={`cookie-key-${cookie.key}`}
|
||||
className="tooltip-mod"
|
||||
html={cookie.key}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-4 truncate">
|
||||
<span id={`cookie-value-${cookie.key}`}>{cookie.value}</span>
|
||||
<Tooltip
|
||||
anchorId={`cookie-value-${cookie.key}`}
|
||||
className="tooltip-mod"
|
||||
html={cookie.value}
|
||||
/>
|
||||
</td>
|
||||
<td className="py-2 px-4 truncate">{cookie.path || '/'}</td>
|
||||
<td className="py-2 px-4 truncate">
|
||||
<span id={`cookie-expires-${cookie.key}`}>
|
||||
{cookie.expires && moment(cookie.expires).isValid()
|
||||
? new Date(cookie.expires).toLocaleString()
|
||||
: 'Session'}
|
||||
</span>
|
||||
{cookie.expires && moment(cookie.expires).isValid() && (
|
||||
<Tooltip
|
||||
anchorId={`cookie-expires-${cookie.key}`}
|
||||
className="tooltip-mod"
|
||||
html={new Date(cookie.expires).toLocaleString()}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2 px-4 text-center">{cookie.secure ? '✓' : ''}</td>
|
||||
<td className="py-2 px-4 text-center">{cookie.httpOnly ? '✓' : ''}</td>
|
||||
<td className="py-2 px-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditCookie(domainWithCookies.domain, cookie);
|
||||
}}
|
||||
className="text-gray-700 hover:text-gray-950
|
||||
dark:text-white dark:hover:text-gray-300"
|
||||
>
|
||||
<IconEdit strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteCookie(domainWithCookies.domain, cookie.path, cookie.key);
|
||||
}}
|
||||
className="text-gray-950 dark:text-white dark:hover:hover:text-red-600 hover:text-red-600"
|
||||
>
|
||||
<IconTrash strokeWidth={1.5} size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
{isModifyCookieModalOpen && (
|
||||
<ModifyCookieModal
|
||||
onClose={() => {
|
||||
setCookieToEdit(null);
|
||||
setCurrentDomain(null);
|
||||
setIsModifyCookieModalOpen(false);
|
||||
}}
|
||||
domain={currentDomain}
|
||||
cookie={cookieToEdit}
|
||||
/>
|
||||
)}
|
||||
{domainToClear ? (
|
||||
<ClearDomainCookiesModal
|
||||
onClose={() => setDomainToClear(null)}
|
||||
domain={domainToClear}
|
||||
onClear={clearDomainCookiesAction}
|
||||
/>
|
||||
) : null}
|
||||
{cookieToDelete ? (
|
||||
<DeleteCookieModal
|
||||
onClose={() => setCookieToDelete(null)}
|
||||
cookieName={cookieToDelete.key}
|
||||
onDelete={deleteCookieAction}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import styled from 'styled-components';
|
||||
const StyledWrapper = styled.div`
|
||||
.editing-mode {
|
||||
cursor: pointer;
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
const Icon = forwardRef((props, ref) => {
|
||||
return (
|
||||
<div ref={ref} className="current-environment flex items-center justify-center pl-3 pr-2 py-1 select-none">
|
||||
{activeEnvironment ? activeEnvironment.name : 'No Environment'}
|
||||
<p className="text-nowrap truncate max-w-32">{activeEnvironment ? activeEnvironment.name : 'No Environment'}</p>
|
||||
<IconCaretDown className="caret" size={14} strokeWidth={2} />
|
||||
</div>
|
||||
);
|
||||
@@ -78,7 +78,10 @@ const EnvironmentSelector = ({ collection }) => {
|
||||
<IconDatabaseOff size={18} strokeWidth={1.5} />
|
||||
<span className="ml-2">No Environment</span>
|
||||
</div>
|
||||
<div className="dropdown-item border-top" onClick={handleSettingsIconClick}>
|
||||
<div className="dropdown-item border-top" onClick={() => {
|
||||
handleSettingsIconClick();
|
||||
dropdownTippyRef.current.hide();
|
||||
}}>
|
||||
<div className="pr-2 text-gray-600">
|
||||
<IconSettings size={18} strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
@@ -6,10 +6,16 @@ import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Portal from 'components/Portal';
|
||||
import Modal from 'components/Modal';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
|
||||
const CreateEnvironment = ({ collection, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const inputRef = useRef();
|
||||
|
||||
const validateEnvironmentName = (name) => {
|
||||
return !collection?.environments?.some((env) => env?.name?.toLowerCase().trim() === name?.toLowerCase().trim());
|
||||
};
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
initialValues: {
|
||||
@@ -17,9 +23,14 @@ const CreateEnvironment = ({ collection, onClose }) => {
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.required('name is required')
|
||||
.min(1, 'Must be at least 1 character')
|
||||
.max(255, 'Must be 255 characters or less')
|
||||
.test('is-valid-filename', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('Name is required')
|
||||
.test('duplicate-name', 'Environment already exists', validateEnvironmentName)
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
dispatch(addEnvironment(values.name, collection.uid))
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash, IconAlertCircle } from '@tabler/icons';
|
||||
import { IconTrash, IconAlertCircle, IconDeviceFloppy, IconRefresh, IconCircleCheck } from '@tabler/icons';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { uuid } from 'utils/common';
|
||||
@@ -12,11 +13,18 @@ import { variableNameRegex } from 'utils/common/regex';
|
||||
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { getGlobalEnvironmentVariables } from 'utils/collections';
|
||||
|
||||
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables }) => {
|
||||
const EnvironmentVariables = ({ environment, collection, setIsModified, originalEnvironmentVariables, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const addButtonRef = useRef(null);
|
||||
const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);
|
||||
|
||||
let _collection = cloneDeep(collection);
|
||||
|
||||
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
|
||||
_collection.globalEnvironmentVariables = globalEnvironmentVariables;
|
||||
|
||||
const formik = useFormik({
|
||||
enableReinitialize: true,
|
||||
@@ -84,6 +92,19 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
formik.setFieldValue(formik.values.length, newVariable, false);
|
||||
};
|
||||
|
||||
const onActivate = () => {
|
||||
dispatch(selectEnvironment(environment ? environment.uid : null, collection.uid))
|
||||
.then(() => {
|
||||
if (environment) {
|
||||
toast.success(`Environment changed to ${environment.name}`);
|
||||
onClose();
|
||||
} else {
|
||||
toast.success(`No Environments are active now`);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.log(err) && toast.error('An error occurred while selecting the environment'));
|
||||
};
|
||||
|
||||
const handleRemoveVar = (id) => {
|
||||
formik.setValues(formik.values.filter((variable) => variable.uid !== id));
|
||||
};
|
||||
@@ -146,7 +167,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
<div className="overflow-hidden grow w-full relative">
|
||||
<SingleLineEditor
|
||||
theme={storedTheme}
|
||||
collection={collection}
|
||||
collection={_collection}
|
||||
name={`${index}.value`}
|
||||
value={variable.value}
|
||||
isSecret={variable.secret}
|
||||
@@ -183,13 +204,19 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" className="submit btn btn-md btn-secondary mt-2" onClick={formik.handleSubmit}>
|
||||
<div className="flex items-center">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary mt-2 flex items-center" onClick={formik.handleSubmit}>
|
||||
<IconDeviceFloppy size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Save
|
||||
</button>
|
||||
<button type="submit" className="ml-2 px-1 submit btn btn-md btn-secondary mt-2" onClick={handleReset}>
|
||||
<button type="submit" className="ml-2 px-1 submit btn btn-sm btn-close mt-2 flex items-center" onClick={handleReset}>
|
||||
<IconRefresh size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Reset
|
||||
</button>
|
||||
<button type="submit" className="submit btn btn-sm btn-close mt-2 flex items-center" onClick={onActivate}>
|
||||
<IconCircleCheck size={16} strokeWidth={1.5} className="mr-1" />
|
||||
Activate
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import DeleteEnvironment from '../../DeleteEnvironment';
|
||||
import RenameEnvironment from '../../RenameEnvironment';
|
||||
import EnvironmentVariables from './EnvironmentVariables';
|
||||
|
||||
const EnvironmentDetails = ({ environment, collection, setIsModified }) => {
|
||||
const EnvironmentDetails = ({ environment, collection, setIsModified, onClose }) => {
|
||||
const [openEditModal, setOpenEditModal] = useState(false);
|
||||
const [openDeleteModal, setOpenDeleteModal] = useState(false);
|
||||
const [openCopyModal, setOpenCopyModal] = useState(false);
|
||||
@@ -38,7 +38,7 @@ const EnvironmentDetails = ({ environment, collection, setIsModified }) => {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<EnvironmentVariables environment={environment} collection={collection} setIsModified={setIsModified} />
|
||||
<EnvironmentVariables environment={environment} collection={collection} setIsModified={setIsModified} onClose={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,10 @@ const StyledWrapper = styled.div`
|
||||
padding: 8px 10px;
|
||||
border-left: solid 2px transparent;
|
||||
text-decoration: none;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
|
||||
@@ -8,8 +8,10 @@ import ImportEnvironment from '../ImportEnvironment';
|
||||
import ManageSecrets from '../ManageSecrets';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ConfirmSwitchEnv from './ConfirmSwitchEnv';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified }) => {
|
||||
const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collection, isModified, setIsModified, onClose }) => {
|
||||
const { environments } = collection;
|
||||
const [openCreateModal, setOpenCreateModal] = useState(false);
|
||||
const [openImportModal, setOpenImportModal] = useState(false);
|
||||
@@ -23,6 +25,11 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEnvironment) {
|
||||
const _selectedEnvironment = environments?.find(env => env?.uid === selectedEnvironment?.uid);
|
||||
const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment);
|
||||
if (hasSelectedEnvironmentChanged) {
|
||||
setSelectedEnvironment(_selectedEnvironment);
|
||||
}
|
||||
setOriginalEnvironmentVariables(selectedEnvironment.variables);
|
||||
return;
|
||||
}
|
||||
@@ -103,13 +110,15 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
{environments &&
|
||||
environments.length &&
|
||||
environments.map((env) => (
|
||||
<div
|
||||
key={env.uid}
|
||||
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
|
||||
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks
|
||||
>
|
||||
<span className="break-all">{env.name}</span>
|
||||
</div>
|
||||
<ToolHint key={env.uid} text={env.name} toolhintId={env.uid} place="right">
|
||||
<div
|
||||
id={env.uid}
|
||||
className={selectedEnvironment.uid === env.uid ? 'environment-item active' : 'environment-item'}
|
||||
onClick={() => handleEnvironmentClick(env)} // Use handleEnvironmentClick to handle clicks
|
||||
>
|
||||
<span className="break-all">{env.name}</span>
|
||||
</div>
|
||||
</ToolHint>
|
||||
))}
|
||||
<div className="btn-create-environment" onClick={() => handleCreateEnvClick()}>
|
||||
+ <span>Create</span>
|
||||
@@ -132,6 +141,7 @@ const EnvironmentList = ({ selectedEnvironment, setSelectedEnvironment, collecti
|
||||
collection={collection}
|
||||
setIsModified={setIsModified}
|
||||
originalEnvironmentVariables={originalEnvironmentVariables}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -28,7 +28,10 @@ const ImportEnvironment = ({ collection, onClose }) => {
|
||||
.then(() => {
|
||||
toast.success('Environment imported successfully');
|
||||
})
|
||||
.catch(() => toast.error('An error occurred while importing the environment'));
|
||||
.catch((error) => {
|
||||
toast.error('An error occurred while importing the environment');
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useFormik } from 'formik';
|
||||
import { renameEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import * as Yup from 'yup';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { validateName, validateNameError } from 'utils/common/regex';
|
||||
|
||||
const RenameEnvironment = ({ onClose, environment, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -18,7 +19,11 @@ const RenameEnvironment = ({ onClose, environment, collection }) => {
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string()
|
||||
.min(1, 'must be at least 1 character')
|
||||
.max(50, 'must be 50 characters or less')
|
||||
.max(255, 'Must be 255 characters or less')
|
||||
.test('is-valid-filename', function(value) {
|
||||
const isValid = validateName(value);
|
||||
return isValid ? true : this.createError({ message: validateNameError(value) });
|
||||
})
|
||||
.required('name is required')
|
||||
}),
|
||||
onSubmit: (values) => {
|
||||
|
||||
@@ -72,6 +72,7 @@ const EnvironmentSettings = ({ collection, onClose }) => {
|
||||
collection={collection}
|
||||
isModified={isModified}
|
||||
setIsModified={setIsModified}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import React from 'react';
|
||||
import path from 'path';
|
||||
import path from 'utils/common/path';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { IconX } from '@tabler/icons';
|
||||
import { isWindowsOS } from 'utils/common/platform';
|
||||
import slash from 'utils/common/slash';
|
||||
|
||||
const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
value = value || [];
|
||||
const FilePickerEditor = ({ value, onChange, collection, isSingleFilePicker = false }) => {
|
||||
const dispatch = useDispatch();
|
||||
const filenames = value
|
||||
const filenames = (isSingleFilePicker ? [value] : value || [])
|
||||
.filter((v) => v != null && v != '')
|
||||
.map((v) => {
|
||||
const separator = isWindowsOS() ? '\\' : '/';
|
||||
@@ -20,7 +18,7 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
const title = filenames.map((v) => `- ${v}`).join('\n');
|
||||
|
||||
const browse = () => {
|
||||
dispatch(browseFiles())
|
||||
dispatch(browseFiles([], [!isSingleFilePicker ? "multiSelections": ""]))
|
||||
.then((filePaths) => {
|
||||
// If file is in the collection's directory, then we use relative path
|
||||
// Otherwise, we use the absolute path
|
||||
@@ -28,13 +26,13 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
const collectionDir = collection.pathname;
|
||||
|
||||
if (filePath.startsWith(collectionDir)) {
|
||||
return path.relative(slash(collectionDir), slash(filePath));
|
||||
return path.relative(collectionDir, filePath);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
});
|
||||
|
||||
onChange(filePaths);
|
||||
onChange(isSingleFilePicker ? filePaths[0] : filePaths);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
@@ -42,14 +40,14 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
onChange([]);
|
||||
onChange(isSingleFilePicker ? '' : []);
|
||||
};
|
||||
|
||||
const renderButtonText = (filenames) => {
|
||||
if (filenames.length == 1) {
|
||||
return filenames[0];
|
||||
}
|
||||
return filenames.length + ' files selected';
|
||||
return filenames.length + ' file(s) selected';
|
||||
};
|
||||
|
||||
return filenames.length > 0 ? (
|
||||
@@ -66,9 +64,9 @@ const FilePickerEditor = ({ value, onChange, collection }) => {
|
||||
</div>
|
||||
) : (
|
||||
<button className="btn btn-secondary px-1" style={{ width: '100%' }} onClick={browse}>
|
||||
Select Files
|
||||
{isSingleFilePicker ? 'Select File' : 'Select Files'}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilePickerEditor;
|
||||
export default FilePickerEditor;
|
||||
@@ -11,6 +11,12 @@ const Wrapper = styled.div`
|
||||
border: solid 1px ${(props) => props.theme.input.border};
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
}
|
||||
.inherit-mode-text {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
.auth-mode-label {
|
||||
color: ${(props) => props.theme.colors.text.yellow};
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
export default Wrapper;
|
||||
226
packages/bruno-app/src/components/FolderSettings/Auth/index.js
Normal file
226
packages/bruno-app/src/components/FolderSettings/Auth/index.js
Normal file
@@ -0,0 +1,226 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';
|
||||
import { updateFolderAuth } from 'providers/ReduxStore/slices/collections';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';
|
||||
import OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';
|
||||
import GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';
|
||||
import AuthMode from '../AuthMode';
|
||||
import BasicAuth from 'components/RequestPane/Auth/BasicAuth';
|
||||
import BearerAuth from 'components/RequestPane/Auth/BearerAuth';
|
||||
import DigestAuth from 'components/RequestPane/Auth/DigestAuth';
|
||||
import NTLMAuth from 'components/RequestPane/Auth/NTLMAuth';
|
||||
import WsseAuth from 'components/RequestPane/Auth/WsseAuth';
|
||||
import ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';
|
||||
import AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';
|
||||
import { findItemInCollection, findParentItemInCollection, humanizeRequestAuthMode } from 'utils/collections/index';
|
||||
|
||||
const GrantTypeComponentMap = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const save = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
let request = get(folder, 'root.request', {});
|
||||
const grantType = get(request, 'auth.oauth2.grantType', 'authorization_code');
|
||||
|
||||
switch (grantType) {
|
||||
case 'password':
|
||||
return <OAuth2PasswordCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
case 'authorization_code':
|
||||
return <OAuth2AuthorizationCode save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
case 'client_credentials':
|
||||
return <OAuth2ClientCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;
|
||||
default:
|
||||
return <div>TBD</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const Auth = ({ collection, folder }) => {
|
||||
const dispatch = useDispatch();
|
||||
let request = get(folder, 'root.request', {});
|
||||
const authMode = get(folder, 'root.request.auth.mode');
|
||||
|
||||
const getTreePathFromCollectionToFolder = (collection, _folder) => {
|
||||
let path = [];
|
||||
let item = findItemInCollection(collection, _folder?.uid);
|
||||
while (item) {
|
||||
path.unshift(item);
|
||||
item = findParentItemInCollection(collection, item?.uid);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
const getEffectiveAuthSource = () => {
|
||||
if (authMode !== 'inherit') return null;
|
||||
|
||||
const collectionAuth = get(collection, 'root.request.auth');
|
||||
let effectiveSource = {
|
||||
type: 'collection',
|
||||
name: 'Collection',
|
||||
auth: collectionAuth
|
||||
};
|
||||
|
||||
// Get path from collection to current folder
|
||||
const folderTreePath = getTreePathFromCollectionToFolder(collection, folder);
|
||||
|
||||
// Check parent folders to find closest auth configuration
|
||||
// Skip the last item which is the current folder
|
||||
for (let i = 0; i < folderTreePath.length - 1; i++) {
|
||||
const parentFolder = folderTreePath[i];
|
||||
if (parentFolder.type === 'folder') {
|
||||
const folderAuth = get(parentFolder, 'root.request.auth');
|
||||
if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {
|
||||
effectiveSource = {
|
||||
type: 'folder',
|
||||
name: parentFolder.name,
|
||||
auth: folderAuth
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return effectiveSource;
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(saveFolderRoot(collection.uid, folder.uid));
|
||||
};
|
||||
|
||||
const getAuthView = () => {
|
||||
switch (authMode) {
|
||||
case 'basic': {
|
||||
return (
|
||||
<BasicAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'bearer': {
|
||||
return (
|
||||
<BearerAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'digest': {
|
||||
return (
|
||||
<DigestAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'ntlm': {
|
||||
return (
|
||||
<NTLMAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'wsse': {
|
||||
return (
|
||||
<WsseAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'apikey': {
|
||||
return (
|
||||
<ApiKeyAuth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'awsv4': {
|
||||
return (
|
||||
<AwsV4Auth
|
||||
collection={collection}
|
||||
item={folder}
|
||||
updateAuth={updateFolderAuth}
|
||||
request={request}
|
||||
save={() => handleSave()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'oauth2': {
|
||||
return (
|
||||
<>
|
||||
<GrantTypeSelector
|
||||
request={request}
|
||||
updateAuth={updateFolderAuth}
|
||||
collection={collection}
|
||||
item={folder}
|
||||
/>
|
||||
<GrantTypeComponentMap collection={collection} folder={folder} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'inherit': {
|
||||
const source = getEffectiveAuthSource();
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row w-full mt-2 gap-2">
|
||||
<div>Auth inherited from {source.name}: </div>
|
||||
<div className="inherit-mode-text">{humanizeRequestAuthMode(source.auth?.mode)}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'none': {
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<div className="text-xs mb-4 text-muted">
|
||||
Configures authentication for the entire folder. This applies to all requests using the{' '}
|
||||
<span className="font-medium">Inherit</span> option in the <span className="font-medium">Auth</span> tab.
|
||||
</div>
|
||||
<div className="flex flex-grow justify-start items-center mb-4">
|
||||
<AuthMode collection={collection} folder={folder} />
|
||||
</div>
|
||||
{getAuthView()}
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Auth;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user