mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-20 09:37:51 +01:00
Compare commits
1128 Commits
v2024.8.0-
...
v2024.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61c333fd04 | ||
|
|
94654cddef | ||
|
|
2951023ee8 | ||
|
|
974ecd511d | ||
|
|
606bfb6a83 | ||
|
|
72649b670e | ||
|
|
d70c8be85d | ||
|
|
a55e27ec20 | ||
|
|
873d02ea37 | ||
|
|
ebc45ca687 | ||
|
|
8913833157 | ||
|
|
75552cbb7d | ||
|
|
ea67304151 | ||
|
|
db416cea31 | ||
|
|
a479c5627c | ||
|
|
fad2702218 | ||
|
|
95e7dc3e6c | ||
|
|
6b76753f56 | ||
|
|
db344454be | ||
|
|
590ea559e3 | ||
|
|
f0f629f26f | ||
|
|
3d74d29775 | ||
|
|
aeb550034d | ||
|
|
b8fdf7ec94 | ||
|
|
9900b09793 | ||
|
|
31dc76727a | ||
|
|
aab0386565 | ||
|
|
77153525fb | ||
|
|
aeecac7ded | ||
|
|
b08750a72b | ||
|
|
a0c7090fda | ||
|
|
a805e39da9 | ||
|
|
65b7db873e | ||
|
|
ed48e1d52a | ||
|
|
b5fa3efb9e | ||
|
|
b616c5d78f | ||
|
|
f7bf245a4c | ||
|
|
e1fdf174c1 | ||
|
|
7907dcc220 | ||
|
|
36890b9a32 | ||
|
|
c0707bb246 | ||
|
|
3bf192953d | ||
|
|
0c8953c471 | ||
|
|
c02aa4f2d0 | ||
|
|
e4e888c47a | ||
|
|
d032495861 | ||
|
|
167a446ad8 | ||
|
|
74edadf7c4 | ||
|
|
021dceeac5 | ||
|
|
c663537ca9 | ||
|
|
48b288b1a6 | ||
|
|
9600d8ba1c | ||
|
|
7ef264223f | ||
|
|
d99fe98347 | ||
|
|
ff459d1570 | ||
|
|
7f952300b3 | ||
|
|
90e2eb67e5 | ||
|
|
942f959c36 | ||
|
|
0b9483954d | ||
|
|
3e8c556999 | ||
|
|
155d0ce3ba | ||
|
|
bdc0ecfcd8 | ||
|
|
71d9e7ddb5 | ||
|
|
737da7e0ae | ||
|
|
afa64acf83 | ||
|
|
b3865d383b | ||
|
|
fdc60445c8 | ||
|
|
690ef02a38 | ||
|
|
a92a85be0d | ||
|
|
5f1286ef6f | ||
|
|
ffd0010a59 | ||
|
|
9320162e22 | ||
|
|
959ace8720 | ||
|
|
55122b042b | ||
|
|
f404aa53c6 | ||
|
|
c80ebdb156 | ||
|
|
6f6bec5764 | ||
|
|
e89905fd04 | ||
|
|
ad81d35c71 | ||
|
|
e36f61b2c7 | ||
|
|
8d03ba5bdd | ||
|
|
11f811d900 | ||
|
|
9cdc13b632 | ||
|
|
e5ec86bfcf | ||
|
|
e35f34eaf5 | ||
|
|
503b7f1c87 | ||
|
|
4c6684623f | ||
|
|
dbfe2dc93d | ||
|
|
96125a0741 | ||
|
|
0f8aea3afd | ||
|
|
1fbcfeaa30 | ||
|
|
ec22191409 | ||
|
|
aa85ecb618 | ||
|
|
a7f0fadeae | ||
|
|
6bc697e4a7 | ||
|
|
795aaae2f5 | ||
|
|
522d293087 | ||
|
|
637e5196c3 | ||
|
|
c484dd4041 | ||
|
|
5eef910b8c | ||
|
|
ed214367d3 | ||
|
|
c69bee251d | ||
|
|
c9b4e6181c | ||
|
|
ecc7192bde | ||
|
|
08ea48b996 | ||
|
|
2f9532cf53 | ||
|
|
dab10d79fe | ||
|
|
d5b0b5481c | ||
|
|
d280df4a0b | ||
|
|
fbaf750c91 | ||
|
|
877f9ce15a | ||
|
|
00367c2b18 | ||
|
|
23f8f5ff7f | ||
|
|
b3bd070a8a | ||
|
|
1a9dfda90c | ||
|
|
411fd4f530 | ||
|
|
6232a46ca8 | ||
|
|
0c9d532c1f | ||
|
|
d907b0bdcd | ||
|
|
fb847ac1f0 | ||
|
|
5a5a443ff9 | ||
|
|
ffb7ab55be | ||
|
|
da29d80c82 | ||
|
|
209af3d149 | ||
|
|
3c4df087ea | ||
|
|
4bf6ddec9f | ||
|
|
b5eed9bf9d | ||
|
|
3153a38b7b | ||
|
|
063e6cf00c | ||
|
|
f967820f12 | ||
|
|
e5511922bf | ||
|
|
d6e5bc6df5 | ||
|
|
5639e358bc | ||
|
|
a3988188f3 | ||
|
|
082be6e1dd | ||
|
|
c2d5ad7c9f | ||
|
|
3dfb435386 | ||
|
|
86856e3506 | ||
|
|
a75b1a3472 | ||
|
|
ff3b32ba64 | ||
|
|
9d2de4a0b1 | ||
|
|
84e5618307 | ||
|
|
5efd0c9c10 | ||
|
|
4803539dd4 | ||
|
|
71e0d846b7 | ||
|
|
3001bafb7f | ||
|
|
430599d8b8 | ||
|
|
375287eeb3 | ||
|
|
f91dd24a9b | ||
|
|
3618fc198c | ||
|
|
410cb7969c | ||
|
|
c8a99a6603 | ||
|
|
def9a3cfd2 | ||
|
|
f925a0cc54 | ||
|
|
54090614ad | ||
|
|
307ca480f3 | ||
|
|
ff81ab4414 | ||
|
|
0ff3ec304c | ||
|
|
203cbc5788 | ||
|
|
97947e5680 | ||
|
|
4ed5489092 | ||
|
|
3825280380 | ||
|
|
bd46e5bdb4 | ||
|
|
2a35decf8c | ||
|
|
9f268a9316 | ||
|
|
f51575508e | ||
|
|
2170a04ccc | ||
|
|
2273bb2df5 | ||
|
|
f622f12bee | ||
|
|
e05d73965a | ||
|
|
bf1ad208c5 | ||
|
|
73e815d059 | ||
|
|
b019496bca | ||
|
|
25033dc831 | ||
|
|
fd2c6930f0 | ||
|
|
a4e223f261 | ||
|
|
1db44a1f16 | ||
|
|
38e0882dd1 | ||
|
|
ac8b1c018b | ||
|
|
b7148d510b | ||
|
|
ad6b8a126a | ||
|
|
2d422dab4b | ||
|
|
5aa3c06112 | ||
|
|
c57096640d | ||
|
|
01441b26db | ||
|
|
8c2da49412 | ||
|
|
680d599f04 | ||
|
|
9c08c5fea8 | ||
|
|
110ffc7529 | ||
|
|
5abf460fce | ||
|
|
f1433b59d4 | ||
|
|
63ba00d1a7 | ||
|
|
a684d71033 | ||
|
|
eb782353a0 | ||
|
|
72c58460e2 | ||
|
|
63a193bb3e | ||
|
|
54817fa6a4 | ||
|
|
42127874e0 | ||
|
|
1e106015f7 | ||
|
|
f1e1acdb22 | ||
|
|
1bf542d49a | ||
|
|
c3a4b3f68a | ||
|
|
a7a88ab490 | ||
|
|
52ee0b524f | ||
|
|
f0f12f7606 | ||
|
|
c480c8d6cf | ||
|
|
1d6ea42448 | ||
|
|
c48ffb2b94 | ||
|
|
fa611df585 | ||
|
|
0be8426af2 | ||
|
|
f5b8b92d95 | ||
|
|
c1fae5951a | ||
|
|
7c1ccbec6d | ||
|
|
12ab7ae045 | ||
|
|
9979dd3ca6 | ||
|
|
d8d338d5d4 | ||
|
|
bf265a2f22 | ||
|
|
0863c0f802 | ||
|
|
66c6ebaacf | ||
|
|
4e889b1688 | ||
|
|
6c6250a41b | ||
|
|
cf01ea7656 | ||
|
|
3e6df98e51 | ||
|
|
80e09c207c | ||
|
|
b829f370cd | ||
|
|
748d956eb0 | ||
|
|
544f6ff6b3 | ||
|
|
3f15ea85c2 | ||
|
|
7ea20a3fb8 | ||
|
|
6df1af4f94 | ||
|
|
1d6624602f | ||
|
|
ac42767aaf | ||
|
|
a722797b6a | ||
|
|
d630f4362c | ||
|
|
0ed5c61fac | ||
|
|
c3442f4326 | ||
|
|
9617ee95e0 | ||
|
|
102bd588c2 | ||
|
|
7e5408fc92 | ||
|
|
408f42b86b | ||
|
|
94abb6838a | ||
|
|
fdc96001db | ||
|
|
b6cd6e415a | ||
|
|
146fc133f0 | ||
|
|
3b784378bf | ||
|
|
bc35195ca8 | ||
|
|
0d106bdd90 | ||
|
|
ba9b914303 | ||
|
|
3c12b14572 | ||
|
|
907836a751 | ||
|
|
c993a5e658 | ||
|
|
4275169005 | ||
|
|
c511a053df | ||
|
|
1b036aabc1 | ||
|
|
d29e503309 | ||
|
|
f81ffe249e | ||
|
|
5cdcbc8dce | ||
|
|
0545c2d598 | ||
|
|
9520359e62 | ||
|
|
cbfd259436 | ||
|
|
f2213ff4e8 | ||
|
|
82abb4b004 | ||
|
|
73b2e44094 | ||
|
|
0317c46f8f | ||
|
|
ab1224c997 | ||
|
|
2589e3e0dd | ||
|
|
b49081cd06 | ||
|
|
6c331ed734 | ||
|
|
4469b84ad6 | ||
|
|
f9cd2fa7fa | ||
|
|
0674bae787 | ||
|
|
53833e1345 | ||
|
|
b9c6d9d877 | ||
|
|
a5041d4229 | ||
|
|
44ad8c7f30 | ||
|
|
a479df5254 | ||
|
|
ec0a84d588 | ||
|
|
43ca9a9390 | ||
|
|
06707ed54c | ||
|
|
6e4ee0045f | ||
|
|
55a426fc85 | ||
|
|
81ff405874 | ||
|
|
8c7f7a7a03 | ||
|
|
47fcb8bad4 | ||
|
|
f0c7a83134 | ||
|
|
3875f90fea | ||
|
|
016c3f7dac | ||
|
|
302a69ed98 | ||
|
|
526b64cc5a | ||
|
|
f1f1a02d79 | ||
|
|
d277c5677f | ||
|
|
d63dabe2fb | ||
|
|
a0bc0ff87c | ||
|
|
35072669ec | ||
|
|
9bfd0d1fbf | ||
|
|
c9a798c9cd | ||
|
|
4dad19db31 | ||
|
|
dd483dbdd8 | ||
|
|
7b2cb64b14 | ||
|
|
b6927435e5 | ||
|
|
a576ba7a23 | ||
|
|
714a96120a | ||
|
|
6585c049c3 | ||
|
|
c89359cf55 | ||
|
|
2c060e5769 | ||
|
|
595a92c324 | ||
|
|
53b922cc7c | ||
|
|
117b4bd285 | ||
|
|
a522236885 | ||
|
|
acb629919b | ||
|
|
99c8ffe121 | ||
|
|
c7cc086d1d | ||
|
|
0e3918d5a2 | ||
|
|
3a09752322 | ||
|
|
aa8c1649f9 | ||
|
|
b456e8ce94 | ||
|
|
f9412e6d8f | ||
|
|
4a88e80669 | ||
|
|
e9bf97bd3f | ||
|
|
3ed78d1088 | ||
|
|
1c1a794c2a | ||
|
|
0e47622e64 | ||
|
|
dbf5b5bdd7 | ||
|
|
acfc254a58 | ||
|
|
f43b38c893 | ||
|
|
372588f541 | ||
|
|
558b429807 | ||
|
|
cc1b3a9f25 | ||
|
|
366dbc9f2a | ||
|
|
30ee2bea34 | ||
|
|
6992436fc7 | ||
|
|
71afaa74d3 | ||
|
|
bcb991c83b | ||
|
|
83c6250a8c | ||
|
|
6d2e9b29d4 | ||
|
|
3135f9c187 | ||
|
|
ba166cc509 | ||
|
|
c53df99105 | ||
|
|
a0b08614f0 | ||
|
|
f4c91d131c | ||
|
|
28e396cb1b | ||
|
|
a669ed2c6d | ||
|
|
4a418be11f | ||
|
|
09953ff7d5 | ||
|
|
5127de831a | ||
|
|
0b6997f59c | ||
|
|
ddbd342033 | ||
|
|
d8d0622773 | ||
|
|
23dec8e96f | ||
|
|
cc9e8c4f1e | ||
|
|
ee6c7b6b1a | ||
|
|
8d605f3190 | ||
|
|
6ddaa99ef1 | ||
|
|
a1102d2ba6 | ||
|
|
0133432049 | ||
|
|
bb9f0b1607 | ||
|
|
ac1902c18b | ||
|
|
b13207072a | ||
|
|
3efb5bb4eb | ||
|
|
606977d795 | ||
|
|
9e8afc3cc9 | ||
|
|
a481cf403d | ||
|
|
0b4e367dfc | ||
|
|
e4022cf532 | ||
|
|
b942c22b20 | ||
|
|
fbc684140b | ||
|
|
7c71d8b751 | ||
|
|
d5c52e2ae5 | ||
|
|
4f736b4656 | ||
|
|
f3024a259e | ||
|
|
11814b56f1 | ||
|
|
a936038f23 | ||
|
|
9e6bce0e41 | ||
|
|
ab33630ef6 | ||
|
|
656c90b54f | ||
|
|
33768af571 | ||
|
|
56acec473b | ||
|
|
4c5087659b | ||
|
|
f1db72eb77 | ||
|
|
6465d8732e | ||
|
|
9780584f82 | ||
|
|
aad4110cc7 | ||
|
|
636a73d151 | ||
|
|
ed371c8cb7 | ||
|
|
92709774f1 | ||
|
|
8925fe9892 | ||
|
|
5dd9539ab3 | ||
|
|
2c70b8bb0f | ||
|
|
1f88b7a41a | ||
|
|
c9e69b4b35 | ||
|
|
4842832468 | ||
|
|
affa7fec29 | ||
|
|
77e7bfbbc1 | ||
|
|
cbbc01a7c6 | ||
|
|
0a98f08f2a | ||
|
|
9875d2353d | ||
|
|
4894677599 | ||
|
|
3f40f36217 | ||
|
|
9480117be5 | ||
|
|
50dc494b58 | ||
|
|
22aa14cdc2 | ||
|
|
b18c042483 | ||
|
|
80ea4c14a4 | ||
|
|
0e22228766 | ||
|
|
9ce1732d75 | ||
|
|
fd6ad952fe | ||
|
|
582da26574 | ||
|
|
b533a01677 | ||
|
|
acc07780a7 | ||
|
|
9f4c80ecf1 | ||
|
|
19d2574e33 | ||
|
|
2e3af37d16 | ||
|
|
9e14aae069 | ||
|
|
d67c0a614d | ||
|
|
1c9a3512a0 | ||
|
|
6b354413f3 | ||
|
|
819b54f376 | ||
|
|
53ce4bfc4f | ||
|
|
a14f41a77a | ||
|
|
0f0cbe7bcb | ||
|
|
657153beff | ||
|
|
bf4eee72df | ||
|
|
c64c794f86 | ||
|
|
e10ff3d136 | ||
|
|
b4d268b202 | ||
|
|
88ee60c97f | ||
|
|
82e2a6b73e | ||
|
|
896e3d5831 | ||
|
|
7f02060b9c | ||
|
|
91fdbe4d33 | ||
|
|
ba5bfd07f7 | ||
|
|
fd2132994b | ||
|
|
612a38d6e1 | ||
|
|
d2211c5e9e | ||
|
|
b8d5e5cecb | ||
|
|
6779251ff3 | ||
|
|
2904caea79 | ||
|
|
290636c098 | ||
|
|
0a44635bcf | ||
|
|
1683271198 | ||
|
|
cf46243e6d | ||
|
|
d2d5f2b957 | ||
|
|
e2220f771d | ||
|
|
6096199174 | ||
|
|
09a23ce357 | ||
|
|
a49ead969e | ||
|
|
8d24e8c282 | ||
|
|
25de5460ad | ||
|
|
776679e2c3 | ||
|
|
351cfae042 | ||
|
|
bb561d7b98 | ||
|
|
88ae30101a | ||
|
|
2d6caa1126 | ||
|
|
e2166d8a26 | ||
|
|
c49e81cde4 | ||
|
|
fa46611d76 | ||
|
|
93f9eee884 | ||
|
|
9d547dee3d | ||
|
|
f75446de87 | ||
|
|
e4bd257bae | ||
|
|
901cf53cd2 | ||
|
|
4b33a696ac | ||
|
|
3a9d4045d0 | ||
|
|
d514880cd8 | ||
|
|
49da0e5aa8 | ||
|
|
40f8a41a8d | ||
|
|
76d1478f19 | ||
|
|
8c09558f62 | ||
|
|
b177993f8a | ||
|
|
6e102175c0 | ||
|
|
dc1af2faec | ||
|
|
ef8528d2b4 | ||
|
|
d51c58aa3d | ||
|
|
11e7fb88cb | ||
|
|
efd7e7bf84 | ||
|
|
ce9ccd34e7 | ||
|
|
9dc2435e43 | ||
|
|
4f6a678ca6 | ||
|
|
5804344bbf | ||
|
|
18caa927b7 | ||
|
|
e5bafd088c | ||
|
|
33b15c52c2 | ||
|
|
796ceb56c4 | ||
|
|
597664d6f9 | ||
|
|
a40437a7fb | ||
|
|
902030bfff | ||
|
|
5e7b2db28d | ||
|
|
e12b85daae | ||
|
|
2705e90016 | ||
|
|
fd9056179d | ||
|
|
ac1f4395d2 | ||
|
|
d5afc37dd7 | ||
|
|
4876ff587a | ||
|
|
c2f3fac22b | ||
|
|
eae1177365 | ||
|
|
5a3596478a | ||
|
|
629e92a98e | ||
|
|
964e6ddb63 | ||
|
|
31b5032817 | ||
|
|
520ff0f68c | ||
|
|
3d61bac030 | ||
|
|
34a7c39637 | ||
|
|
4d481b82a5 | ||
|
|
79ee5a29b1 | ||
|
|
73f0e29c59 | ||
|
|
9a28a28ae6 | ||
|
|
06177c8077 | ||
|
|
ea61428ff5 | ||
|
|
2e980660c8 | ||
|
|
ad890e01b0 | ||
|
|
5bd2d0959a | ||
|
|
046a569ebe | ||
|
|
fd044005a6 | ||
|
|
0857ef9afd | ||
|
|
3dfb8b3fb2 | ||
|
|
4571d7dc93 | ||
|
|
321b013ce6 | ||
|
|
ca4d9b1ad6 | ||
|
|
bb628e699f | ||
|
|
a9d0bb3915 | ||
|
|
555389b667 | ||
|
|
ed55eb2238 | ||
|
|
e934ca9586 | ||
|
|
d953a75073 | ||
|
|
8d29fad261 | ||
|
|
ab8503d87c | ||
|
|
daf138c967 | ||
|
|
2205464e1e | ||
|
|
44b45fc900 | ||
|
|
323449b205 | ||
|
|
63f762a891 | ||
|
|
2679f5ebdf | ||
|
|
29571b4942 | ||
|
|
4d97dce6b1 | ||
|
|
c8fedd3d2c | ||
|
|
939117d5c8 | ||
|
|
a1b4d24907 | ||
|
|
c8feb6482b | ||
|
|
b7d9f0bf92 | ||
|
|
fe4696daf7 | ||
|
|
d63d9ca213 | ||
|
|
a57ad3bd7c | ||
|
|
b0a341b29f | ||
|
|
8f8bd00487 | ||
|
|
bd084cdf02 | ||
|
|
2cd184f0c7 | ||
|
|
27112a39f8 | ||
|
|
0a33a32475 | ||
|
|
eb08db3d4a | ||
|
|
630e573777 | ||
|
|
c71790195b | ||
|
|
90b0112f14 | ||
|
|
d274b85db1 | ||
|
|
a55cf9e216 | ||
|
|
7a960574a5 | ||
|
|
017de296a0 | ||
|
|
c7620e90a1 | ||
|
|
0d8203da47 | ||
|
|
671d0fe847 | ||
|
|
9f54eb77a0 | ||
|
|
bc4ef2d9f7 | ||
|
|
9c35f7c85c | ||
|
|
d668244f9b | ||
|
|
a2a36ceb54 | ||
|
|
14582e6bf4 | ||
|
|
2ea7e6ba27 | ||
|
|
395481b5cb | ||
|
|
0dc6b2d9e7 | ||
|
|
5ad13a61e6 | ||
|
|
3ed00c0955 | ||
|
|
e113f86c5d | ||
|
|
236087fdc8 | ||
|
|
7bb620e6d5 | ||
|
|
acb01cf086 | ||
|
|
07cad2e337 | ||
|
|
4ac0f20f2a | ||
|
|
be0cc4bfe4 | ||
|
|
6d6f865fb7 | ||
|
|
d2b44cb7d2 | ||
|
|
1c7bdb346a | ||
|
|
4360b3658f | ||
|
|
47a8f06c90 | ||
|
|
2a10113a57 | ||
|
|
fb817bc2d5 | ||
|
|
6c69fff27d | ||
|
|
d8948bb061 | ||
|
|
fdedb9bd28 | ||
|
|
4e781b752d | ||
|
|
b526ea506b | ||
|
|
e3a2b7146b | ||
|
|
1ae123bb51 | ||
|
|
c655557313 | ||
|
|
3ab1f5308c | ||
|
|
9a2688617d | ||
|
|
a3c979a987 | ||
|
|
1628d0c843 | ||
|
|
393df4e269 | ||
|
|
1e309e821e | ||
|
|
acdec8c96d | ||
|
|
c64f1108f0 | ||
|
|
c51d5c5377 | ||
|
|
373915671e | ||
|
|
650aa240ea | ||
|
|
2983f2544d | ||
|
|
2be41475e6 | ||
|
|
65c023c8b8 | ||
|
|
a54fff93a6 | ||
|
|
6104a8b3c2 | ||
|
|
6c44035d2b | ||
|
|
29e7bb1dcb | ||
|
|
ae9c23b740 | ||
|
|
1df65fbf87 | ||
|
|
13fdf9d9e4 | ||
|
|
7da01e21e2 | ||
|
|
05e8841e82 | ||
|
|
a837ffd7bb | ||
|
|
def44d3266 | ||
|
|
60acc86e52 | ||
|
|
874c73b50f | ||
|
|
99d69687b6 | ||
|
|
a49ae3d89d | ||
|
|
59f723827e | ||
|
|
b1e14a6dc4 | ||
|
|
e459674338 | ||
|
|
e196f1d98e | ||
|
|
dc552b8099 | ||
|
|
9658434503 | ||
|
|
ade6c1c4f7 | ||
|
|
2a759144d6 | ||
|
|
b0e1614aac | ||
|
|
13307a76af | ||
|
|
d23de93917 | ||
|
|
f55c8aba56 | ||
|
|
009036b004 | ||
|
|
8887d2a8e9 | ||
|
|
4df7356950 | ||
|
|
34a8fc4e22 | ||
|
|
b01747299f | ||
|
|
3e9a89a4c9 | ||
|
|
2ef6885949 | ||
|
|
218b593bfa | ||
|
|
b60bc091b8 | ||
|
|
55a2d92c8b | ||
|
|
b212b80927 | ||
|
|
9c4cd898a2 | ||
|
|
93d1ff778e | ||
|
|
a8630ede38 | ||
|
|
3600f3aa26 | ||
|
|
905bce0322 | ||
|
|
202e272e90 | ||
|
|
11161fda51 | ||
|
|
9f75497f15 | ||
|
|
8dcf9a8921 | ||
|
|
d414709d1a | ||
|
|
94361f7c27 | ||
|
|
6056859da6 | ||
|
|
b1692b41f0 | ||
|
|
312c01e405 | ||
|
|
6f15d1352b | ||
|
|
cb9960bbc8 | ||
|
|
9e31bbcfa3 | ||
|
|
43706009a2 | ||
|
|
c11a0ca823 | ||
|
|
77e3f4cc40 | ||
|
|
884909d449 | ||
|
|
e5e356a822 | ||
|
|
1d9aa6748d | ||
|
|
7742cde11a | ||
|
|
7381dcec05 | ||
|
|
2c7bf29ec6 | ||
|
|
dafaa5940a | ||
|
|
0281396d58 | ||
|
|
e98081b9f2 | ||
|
|
0f7839bfaf | ||
|
|
d2f0e690e0 | ||
|
|
c7581d283e | ||
|
|
a68024604b | ||
|
|
ceb55bc56b | ||
|
|
c9b2842e62 | ||
|
|
cb55bc1746 | ||
|
|
af59695a55 | ||
|
|
a7a2608c44 | ||
|
|
78b8b96b8f | ||
|
|
ed1427d421 | ||
|
|
5c3f1af87b | ||
|
|
8872b96efa | ||
|
|
44fa2c8dbb | ||
|
|
3ad06be9d6 | ||
|
|
21bbb29a4f | ||
|
|
48923634d9 | ||
|
|
f9703fbc1d | ||
|
|
8839a0b7af | ||
|
|
edb820c7d9 | ||
|
|
8584f0aa33 | ||
|
|
f60cb35b5e | ||
|
|
a4f5e4a6b8 | ||
|
|
ecb3ace6a9 | ||
|
|
5f2b20a0da | ||
|
|
2158692291 | ||
|
|
feab2abe0c | ||
|
|
ae62a581a2 | ||
|
|
92dcbedf2a | ||
|
|
356b8acd5b | ||
|
|
5fd16b922c | ||
|
|
f1fc57830d | ||
|
|
8951973f02 | ||
|
|
2377b29d86 | ||
|
|
f9879824d2 | ||
|
|
184ce2cb72 | ||
|
|
2a2fb07c60 | ||
|
|
feb542fd2b | ||
|
|
d5295efd82 | ||
|
|
8156b51bda | ||
|
|
b04b56d240 | ||
|
|
3685786cb5 | ||
|
|
0d94306b28 | ||
|
|
895fdce1b7 | ||
|
|
29e0b9e5d2 | ||
|
|
3f717d4c5a | ||
|
|
4ce2436ac5 | ||
|
|
8ec735a419 | ||
|
|
c09bea29a2 | ||
|
|
97c5d2c7eb | ||
|
|
95b2e0ae62 | ||
|
|
6dd9a112fe | ||
|
|
95b428dcd6 | ||
|
|
d0c08fc8ef | ||
|
|
53452875da | ||
|
|
a6609833c2 | ||
|
|
6f68e11c4d | ||
|
|
fe8365e860 | ||
|
|
81188acac1 | ||
|
|
bdb5305d16 | ||
|
|
c4926f430a | ||
|
|
98ed12a2df | ||
|
|
b35c692d58 | ||
|
|
7657166bfa | ||
|
|
bed0e034aa | ||
|
|
0ecb16a2e7 | ||
|
|
a30adb1fe9 | ||
|
|
1db78af0ad | ||
|
|
0d2eb509e8 | ||
|
|
5bfcccbb2f | ||
|
|
7eafff082c | ||
|
|
8c51cb94b8 | ||
|
|
b0612b8632 | ||
|
|
36a44c9ca6 | ||
|
|
94048a0337 | ||
|
|
11166470aa | ||
|
|
de190ca8fa | ||
|
|
9471009b8b | ||
|
|
de165b5c55 | ||
|
|
99e4b08653 | ||
|
|
65648ac877 | ||
|
|
b8a859895b | ||
|
|
60fa9dcf13 | ||
|
|
9f7fa3709e | ||
|
|
1bf711826e | ||
|
|
ff6969d41c | ||
|
|
1e7f629b28 | ||
|
|
68ea718c77 | ||
|
|
1cd6e0af06 | ||
|
|
749025640f | ||
|
|
2e72cad591 | ||
|
|
7ce4ac3239 | ||
|
|
9d4f4bef5d | ||
|
|
3b1e4f538d | ||
|
|
8a73636f43 | ||
|
|
451ffad24a | ||
|
|
851082be9d | ||
|
|
bc3515d71e | ||
|
|
ed87192468 | ||
|
|
9a56b2f331 | ||
|
|
46470160a9 | ||
|
|
9dd4489049 | ||
|
|
7dac299edd | ||
|
|
a9c67f59fc | ||
|
|
c37c020cf0 | ||
|
|
4d93892249 | ||
|
|
b1764d0508 | ||
|
|
76218365fc | ||
|
|
aebaa8d4b7 | ||
|
|
235ab2056d | ||
|
|
7b7e43a185 | ||
|
|
8baaa9beb2 | ||
|
|
b9d1e70e04 | ||
|
|
b2150181b3 | ||
|
|
e8b5e9de02 | ||
|
|
9c68e62db3 | ||
|
|
73190958fa | ||
|
|
68fbb1a3ff | ||
|
|
ac036ff814 | ||
|
|
34bfc6840c | ||
|
|
805ea4a656 | ||
|
|
677f436bcb | ||
|
|
a44e88f53d | ||
|
|
3ad132d77d | ||
|
|
d0387bdf76 | ||
|
|
9f2577db66 | ||
|
|
ede0f1890f | ||
|
|
439af5e8f2 | ||
|
|
4bf82ec3e9 | ||
|
|
0e85d40f47 | ||
|
|
d62702f7dd | ||
|
|
d1bc5900fb | ||
|
|
cba8a055d5 | ||
|
|
8fd7979474 | ||
|
|
598cef948a | ||
|
|
51cadaf53b | ||
|
|
7715668e96 | ||
|
|
4c2c0dd1de | ||
|
|
2567e72bfe | ||
|
|
8665ca9acb | ||
|
|
fa16e1f957 | ||
|
|
83e2cab1b6 | ||
|
|
4ad5d7f291 | ||
|
|
51f48d3883 | ||
|
|
5cf59d7d70 | ||
|
|
39a441d118 | ||
|
|
adea2c0e81 | ||
|
|
5821e4c232 | ||
|
|
75b108c6cd | ||
|
|
66c80d4d9a | ||
|
|
cd83d7d7ea | ||
|
|
ee868203f6 | ||
|
|
add21457ec | ||
|
|
a43fa761c7 | ||
|
|
5dae591c79 | ||
|
|
e02954f396 | ||
|
|
2297ee2a1d | ||
|
|
0fec8d117b | ||
|
|
465ccdd2b2 | ||
|
|
6decba8a4a | ||
|
|
5a4fa40ab4 | ||
|
|
f3ff0b660b | ||
|
|
2a4889bc01 | ||
|
|
8dfd3579e4 | ||
|
|
4741791673 | ||
|
|
2edfa9bd75 | ||
|
|
8d37497eb7 | ||
|
|
f4dcb67caa | ||
|
|
d5774c6067 | ||
|
|
67007b1f5c | ||
|
|
9755833577 | ||
|
|
b7909de566 | ||
|
|
114ae4285d | ||
|
|
a22154e8ce | ||
|
|
f33ef73f43 | ||
|
|
55c879abc2 | ||
|
|
c941cb6989 | ||
|
|
f8c5960156 | ||
|
|
6f625cdfd9 | ||
|
|
11200677a0 | ||
|
|
f802d9d655 | ||
|
|
8c7409a24f | ||
|
|
db2dc823f2 | ||
|
|
ce26d1b86f | ||
|
|
6c5ae5fcaa | ||
|
|
45c35eae31 | ||
|
|
cb4ce9fdba | ||
|
|
d9916f520e | ||
|
|
0f33fd2588 | ||
|
|
5cc5f27420 | ||
|
|
feec6fedfa | ||
|
|
fb38708fad | ||
|
|
b82798bf49 | ||
|
|
37f0eca79f | ||
|
|
72bcc17799 | ||
|
|
b2524c1de0 | ||
|
|
0b3bd6313f | ||
|
|
afe075829a | ||
|
|
d008324c3e | ||
|
|
79634532fd | ||
|
|
95622bc44b | ||
|
|
197bc38b69 | ||
|
|
fe9a06185d | ||
|
|
8eb2a14737 | ||
|
|
7c7eba5f63 | ||
|
|
302f1402cf | ||
|
|
0ffe459a8b | ||
|
|
54310983a3 | ||
|
|
fcbe923770 | ||
|
|
b2a7d95922 | ||
|
|
7da45506d4 | ||
|
|
51445b0d99 | ||
|
|
712626ac1f | ||
|
|
c1d7f6e544 | ||
|
|
5a81c5de17 | ||
|
|
2faf2986cf | ||
|
|
8c2e058ced | ||
|
|
5a31c2c930 | ||
|
|
0931faeeb8 | ||
|
|
79ecc0ae38 | ||
|
|
2ba401dde1 | ||
|
|
7d93dc7d62 | ||
|
|
427d786ef1 | ||
|
|
2accf518c3 | ||
|
|
91e10e6a0a | ||
|
|
01f12c5161 | ||
|
|
62a367cd0c | ||
|
|
ceefbd1de1 | ||
|
|
3f713d878c | ||
|
|
bc35429670 | ||
|
|
5b563b8a16 | ||
|
|
35e25842be | ||
|
|
55ce39e39d | ||
|
|
9ab1df1c99 | ||
|
|
b23e56c3af | ||
|
|
604254257d | ||
|
|
d7c9fe7991 | ||
|
|
ebc9939498 | ||
|
|
3fc5f72074 | ||
|
|
625abf2acb | ||
|
|
0ad5ebde47 | ||
|
|
9a8c208a15 | ||
|
|
72223c2dea | ||
|
|
0b1f3466ec | ||
|
|
f9206c7a01 | ||
|
|
4feb31390d | ||
|
|
3c517e0739 | ||
|
|
4f1c8b0d53 | ||
|
|
68159dfa95 | ||
|
|
60d4ecda1a | ||
|
|
6843823efc | ||
|
|
8998b8ab17 | ||
|
|
728d30f360 | ||
|
|
90a96ef8e5 | ||
|
|
42de045263 | ||
|
|
801b29402d | ||
|
|
bb2ba21bbd | ||
|
|
7fcf709efe | ||
|
|
904d20b9b8 | ||
|
|
fa32829b2e | ||
|
|
968e80b3ad | ||
|
|
b721396340 | ||
|
|
4b2c1b18a9 | ||
|
|
06ce7abfb9 | ||
|
|
ab15782019 | ||
|
|
3548945202 | ||
|
|
5852877f4d | ||
|
|
e96527ab27 | ||
|
|
314b51db49 | ||
|
|
d3d64b38a5 | ||
|
|
9eb0fcd441 | ||
|
|
56a30aa7c4 | ||
|
|
def3c712c9 | ||
|
|
1589235b3f | ||
|
|
567b516bd0 | ||
|
|
1e8498b52c | ||
|
|
e3ef29bdfd | ||
|
|
ae4a43f406 | ||
|
|
391a436ed3 | ||
|
|
8f86c46f49 | ||
|
|
44710a8021 | ||
|
|
09c574bf30 | ||
|
|
41390e9142 | ||
|
|
dcc2c053f8 | ||
|
|
5ea45537d3 | ||
|
|
ae82bdc225 | ||
|
|
aeda504e64 | ||
|
|
225b0956a8 | ||
|
|
7a6f0ccc46 | ||
|
|
c51805fe69 | ||
|
|
2caa13fdec | ||
|
|
55970f0a92 | ||
|
|
6f873fa186 | ||
|
|
4adb8274ab | ||
|
|
a3783205b8 | ||
|
|
a98487634c | ||
|
|
3bd4795def | ||
|
|
5a3b47846a | ||
|
|
4b0793ebef | ||
|
|
f855d8ab16 | ||
|
|
c5ca3daab3 | ||
|
|
ec8bec32ba | ||
|
|
54aa594a70 | ||
|
|
15bc08390b | ||
|
|
8b16258627 | ||
|
|
6b5f1b4ade | ||
|
|
4de95e49e9 | ||
|
|
e19ea612f5 | ||
|
|
b84a5530be | ||
|
|
9fc988b1de | ||
|
|
c5b50c10f4 | ||
|
|
b487938ffb | ||
|
|
e754dbc563 | ||
|
|
24ed6f0ee2 | ||
|
|
333b9319b6 | ||
|
|
b16d74d55b | ||
|
|
c5f5d72611 | ||
|
|
d57dfbf225 | ||
|
|
31e09aba4b | ||
|
|
7314c8f36f | ||
|
|
05f20af9ed | ||
|
|
c591394004 | ||
|
|
b586318d4b | ||
|
|
9ac5572094 | ||
|
|
b1835561a8 | ||
|
|
6fc9b5a185 | ||
|
|
1258550695 | ||
|
|
0b6133efae | ||
|
|
d02d4bc76f | ||
|
|
43787a6b9e | ||
|
|
e33085a7b4 | ||
|
|
10616001df | ||
|
|
637c220475 | ||
|
|
e472249133 | ||
|
|
7b5df2feb4 | ||
|
|
4bedcf3949 | ||
|
|
cbe8cf3a05 | ||
|
|
48e43c3f9f | ||
|
|
03e36e7f1c | ||
|
|
e4d34c2943 | ||
|
|
ed468571d0 | ||
|
|
5292a03171 | ||
|
|
0043c5fbda | ||
|
|
3bbe9d9201 | ||
|
|
97db32fdf2 | ||
|
|
b599ab9f5b | ||
|
|
3aaf365e4a | ||
|
|
34589f8b06 | ||
|
|
b761e3d9c1 | ||
|
|
3247190a46 | ||
|
|
b9294ff994 | ||
|
|
e67118bfa9 | ||
|
|
4a949b2720 | ||
|
|
1e09f09bd6 | ||
|
|
a209a486aa | ||
|
|
02574cf5e0 | ||
|
|
d7ff52038d | ||
|
|
e2fa188ca3 | ||
|
|
90c873e37e | ||
|
|
03461ffa77 | ||
|
|
9245805c92 | ||
|
|
7ad1bd54ac | ||
|
|
4ad91b4553 | ||
|
|
8cf3b1ae02 | ||
|
|
47c753045c | ||
|
|
389a0d785c | ||
|
|
364cdb06df | ||
|
|
1b6c32c7ac | ||
|
|
3af720d93a | ||
|
|
babe9adc3d | ||
|
|
790a3daddd | ||
|
|
072d0ade88 | ||
|
|
f6fcb54a6e | ||
|
|
1ffad989f9 | ||
|
|
f1326620bc | ||
|
|
7e815444ff | ||
|
|
37bae62a4d | ||
|
|
085916dfe4 | ||
|
|
6394c380fc | ||
|
|
e08d8414b7 | ||
|
|
7e6e605cdb | ||
|
|
f9e0c4bc24 | ||
|
|
9dfe3db720 | ||
|
|
a709e4b19f | ||
|
|
971cd02142 | ||
|
|
f846fa8dbc | ||
|
|
df22285d15 | ||
|
|
0b3497e5a1 | ||
|
|
e9e3ba283c | ||
|
|
de0bd57622 | ||
|
|
109006db66 | ||
|
|
cb43f17efc | ||
|
|
d8da396c2f | ||
|
|
5fbc5edb15 | ||
|
|
17c734af11 | ||
|
|
226781af8e | ||
|
|
6c8f4c943a | ||
|
|
c37cfaf0e4 | ||
|
|
956a2ef5ce | ||
|
|
571293a34d | ||
|
|
6eaad1352a | ||
|
|
eec65f8721 | ||
|
|
25fc0c2f07 | ||
|
|
317ca24dcf | ||
|
|
366116ab1b | ||
|
|
520db48234 | ||
|
|
5aa80d8ea8 | ||
|
|
b48a41aaec | ||
|
|
986cd56662 | ||
|
|
1ecf642181 | ||
|
|
1f5e7dbaa9 | ||
|
|
030ba26c5e | ||
|
|
7668a093a9 | ||
|
|
534d45bccf | ||
|
|
c30caf11fe | ||
|
|
420f7aa7c3 | ||
|
|
77ca90779c | ||
|
|
a7488b389d | ||
|
|
016adbcb30 | ||
|
|
132466f03b | ||
|
|
b5496f0f48 | ||
|
|
4d7df9023e | ||
|
|
c6ba978069 | ||
|
|
7490106014 | ||
|
|
569a9454ad | ||
|
|
5040d73a8b | ||
|
|
1e57890d2e | ||
|
|
af964ac383 | ||
|
|
88c9df4577 | ||
|
|
74dd4ee979 | ||
|
|
8efd95fdbe | ||
|
|
d0846dcd11 | ||
|
|
9dc8234f4b | ||
|
|
fe18cd1806 | ||
|
|
2c0a891838 | ||
|
|
640d2a1bb4 | ||
|
|
bf691017fd | ||
|
|
b166401d4f | ||
|
|
2cf7ced1f2 | ||
|
|
e4a257b807 | ||
|
|
b8bb9f43e3 | ||
|
|
5b2f1e40aa | ||
|
|
2b92b7ab01 | ||
|
|
a77e378730 | ||
|
|
b3795d99b5 | ||
|
|
2d83a536b9 | ||
|
|
a5829a9b6b | ||
|
|
5a9fb5a3a7 | ||
|
|
4319ce9a7b | ||
|
|
e5551c489f | ||
|
|
21a562c6fd | ||
|
|
302f0a1860 | ||
|
|
3b591d241c | ||
|
|
238bd3df78 | ||
|
|
30b1b87fea |
@@ -1,43 +1,48 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:jsx-a11y/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"eslint-config-prettier"
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:import/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'eslint-config-prettier',
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"]
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
ignorePatterns: [
|
||||
"scripts/**/*",
|
||||
"plugin-runtime/**/*",
|
||||
"plugin-runtime-types/**/*",
|
||||
"src-tauri/**/*",
|
||||
"plugins/**/*",
|
||||
'scripts/**/*',
|
||||
'plugin-runtime/**/*',
|
||||
'plugin-runtime-types/**/*',
|
||||
'src-tauri/**/*',
|
||||
'plugins/**/*',
|
||||
'tailwind.config.cjs',
|
||||
'vite.config.ts',
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect"
|
||||
version: 'detect',
|
||||
},
|
||||
"import/resolver": {
|
||||
'import/resolver': {
|
||||
node: {
|
||||
paths: ["src-web"],
|
||||
extensions: [".ts", ".tsx"]
|
||||
}
|
||||
}
|
||||
paths: ['src-web'],
|
||||
extensions: ['.ts', '.tsx'],
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"jsx-a11y/no-autofocus": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"@typescript-eslint/consistent-type-imports": ["error", {
|
||||
prefer: "type-imports",
|
||||
disallowTypeAnnotations: true,
|
||||
fixStyle: "separate-type-imports"
|
||||
}]
|
||||
}
|
||||
'jsx-a11y/no-autofocus': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'import/no-unresolved': 'off',
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{
|
||||
prefer: 'type-imports',
|
||||
disallowTypeAnnotations: true,
|
||||
fixStyle: 'separate-type-imports',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
45
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
45
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Bug Report
|
||||
description: "Something isn't working properly in Yaak"
|
||||
title: "Short description"
|
||||
labels: ["bug", "needs triage"]
|
||||
assignees:
|
||||
- gschier
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report 🤗
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: Tell us what you see!
|
||||
value: "A bug happened!"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of our software are you running?
|
||||
placeholder: 2024.8.0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: What operating system are you on?
|
||||
multiple: false
|
||||
options:
|
||||
- macOS
|
||||
- Windows
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Feature Request, Question, etc.
|
||||
url: https://feedback.yaak.app
|
||||
about: Report all non-bugs to the feedback board 👉🏼
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -81,7 +81,9 @@ jobs:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install yaak CLI
|
||||
run: go install github.com/yaakapp/yaakcli@latest
|
||||
run: |
|
||||
npm install -g @yaakapp/cli
|
||||
yaakcli --version
|
||||
|
||||
- name: Run lint
|
||||
run: npm run lint
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Dev Desktop" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="start" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs>
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
12
DEVELOPMENT.md
Normal file
12
DEVELOPMENT.md
Normal file
@@ -0,0 +1,12 @@
|
||||
## Developer Setup
|
||||
|
||||
Development requires the following tools
|
||||
|
||||
- [Node.js](https://nodejs.org/en/download/package-manager)
|
||||
- [Rust](https://www.rust-lang.org/tools/install)
|
||||
|
||||
Then, you can run the app.
|
||||
|
||||
1. Checkout the [plugins](https://github.com/yaakapp/plugins) repository
|
||||
2. Run `YAAK_PLUGINS_DIR="..." npm run build` to generate an icon, fetch external binaries, and build local JS dependencies
|
||||
3. Run the desktop app in dev mode `npm start`
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Yaak
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
13
Makefile
13
Makefile
@@ -1,13 +0,0 @@
|
||||
.PHONY: sqlx-prepare, dev, migrate, build
|
||||
|
||||
sqlx-prepare:
|
||||
cd src-tauri && cargo sqlx prepare --database-url 'sqlite://db.sqlite'
|
||||
|
||||
dev:
|
||||
npm run tauri-dev
|
||||
|
||||
migrate:
|
||||
cd src-tauri && cargo sqlx migrate run --database-url 'sqlite://db.sqlite?mode=rw'
|
||||
|
||||
build:
|
||||
./node_modules/.bin/tauri build
|
||||
24
README.md
24
README.md
@@ -1,16 +1,16 @@
|
||||
# Yaak Network Toolkit
|
||||
# [Yaak API Client](https://yaak.app)
|
||||
|
||||
The most fun you'll ever have working with APIs.
|
||||
Yaak is a desktop API client for organizing and executing REST, GraphQL, and gRPC
|
||||
requests. It's built using [Tauri](https://tauri.app), Rust, and ReactJS.
|
||||
|
||||
## Common Commands
|
||||

|
||||
|
||||
```sh
|
||||
# Start dev app
|
||||
npm run tauri-dev
|
||||
## Feedback and Bug Reports
|
||||
|
||||
# Migration commands
|
||||
cd src-tauri
|
||||
cargo sqlx migrate add ${MIGRATION_NAME}
|
||||
cargo sqlx migrate run --database-url 'sqlite://db.sqlite?mode=rw'
|
||||
cargo sqlx prepare --database-url 'sqlite://db.sqlite'
|
||||
```
|
||||
Please [Create an Issue](https://github.com/yaakapp/app/issues/new) for bug reports and
|
||||
submit all other feedback to the [Feedback Board](https://feedback.yaak.app).
|
||||
|
||||
## Contribution Policy
|
||||
|
||||
Yaak open source, but currently only accepting contributions for bug fixes. See [
|
||||
`DEVELOPMENT.md`](DEVELOPMENT.md).
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB |
BIN
design/icon.png
BIN
design/icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 356 KiB |
Binary file not shown.
5130
package-lock.json
generated
5130
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
140
package.json
140
package.json
@@ -3,6 +3,10 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/yaakapp/app.git"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "npm run tauri-dev:desktop",
|
||||
"tauri-dev:desktop": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
|
||||
@@ -23,90 +27,92 @@
|
||||
"replace-version": "node scripts/replace-version.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.2.1",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-xml": "^6.0.2",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/search": "^6.2.3",
|
||||
"@lezer/generator": "^1.2.2",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.3",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
"@tailwindcss/container-queries": "^0.1.0",
|
||||
"@tanstack/react-query": "^5.45.1",
|
||||
"@tauri-apps/api": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-fs": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-os": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0-rc.0",
|
||||
"@tauri-apps/plugin-log": "^2.0.0-rc.0",
|
||||
"@yaakapp/api": "^0.1.4",
|
||||
"@codemirror/commands": "^6",
|
||||
"@codemirror/lang-javascript": "^6",
|
||||
"@codemirror/lang-json": "^6",
|
||||
"@codemirror/lang-xml": "^6",
|
||||
"@codemirror/language": "^6",
|
||||
"@codemirror/search": "^6",
|
||||
"@lezer/generator": "^1.7.1",
|
||||
"@lezer/highlight": "^1.2.1",
|
||||
"@lezer/lr": "^1.4.2",
|
||||
"@react-hook/resize-observer": "^2.0.2",
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tanstack/react-query": "^5.55.4",
|
||||
"@tauri-apps/api": "^2.0.0-rc.4",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0-rc.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-fs": "^2.0.0-rc.2",
|
||||
"@tauri-apps/plugin-log": "^2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-os": "^2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0-rc.1",
|
||||
"@yaakapp/api": "^0.1.17",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.3.2",
|
||||
"classnames": "^2.5.1",
|
||||
"cm6-graphql": "^0.0.9",
|
||||
"codemirror": "^6.0.1",
|
||||
"codemirror-json-schema": "^0.6.1",
|
||||
"date-fns": "^3.3.1",
|
||||
"codemirror-json-schema": "^0.7.8",
|
||||
"date-fns": "^3.6.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"fast-fuzzy": "^1.12.0",
|
||||
"focus-trap-react": "^10.1.1",
|
||||
"format-graphql": "^1.4.0",
|
||||
"framer-motion": "^9.0.4",
|
||||
"lucide-react": "^0.309.0",
|
||||
"mime": "^4.0.1",
|
||||
"focus-trap-react": "^10.2.3",
|
||||
"format-graphql": "^1.5.0",
|
||||
"framer-motion": "^11.5.4",
|
||||
"jotai": "^2.9.3",
|
||||
"lucide-react": "^0.439.0",
|
||||
"mime": "^4.0.4",
|
||||
"papaparse": "^5.4.1",
|
||||
"parse-color": "^1.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-helmet-async": "^1.3.0",
|
||||
"react-pdf": "^9.0.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-use": "^17.4.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-pdf": "^9.1.0",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-use": "^17.5.1",
|
||||
"slugify": "^1.6.6",
|
||||
"uuid": "^9.0.0",
|
||||
"xml-formatter": "^3.6.2"
|
||||
"uuid": "^10.0.0",
|
||||
"xml-formatter": "^3.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
|
||||
"@tanstack/react-query-devtools": "^5.45.1",
|
||||
"@tauri-apps/cli": "^2.0.0-rc.2",
|
||||
"@types/node": "^18.7.10",
|
||||
"@types/papaparse": "^5.3.7",
|
||||
"@types/parse-color": "^1.0.1",
|
||||
"@types/parse-json": "^4.0.0",
|
||||
"@types/react": "^18.0.31",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||
"@typescript-eslint/parser": "^7.0.2",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"@tanstack/react-query-devtools": "^5.55.4",
|
||||
"@tauri-apps/cli": "^2.0.0-rc.14",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/parse-color": "^1.0.3",
|
||||
"@types/parse-json": "^4.0.2",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
||||
"@typescript-eslint/parser": "^8.5.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"decompress": "^4.2.1",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"husky": "^8.0.3",
|
||||
"eslint": "^8",
|
||||
"eslint-config-prettier": "^8",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.0",
|
||||
"eslint-plugin-react": "^7.35.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"husky": "^9.1.5",
|
||||
"internal-ip": "^8.0.0",
|
||||
"lint-staged": "^15.0.2",
|
||||
"lint-staged": "^15.2.10",
|
||||
"nodejs-file-downloader": "^4.13.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-nesting": "^11.2.1",
|
||||
"prettier": "^2.8.4",
|
||||
"react-devtools": "^4.27.2",
|
||||
"rimraf": "^5.0.7",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-static-copy": "^1.0.5",
|
||||
"postcss": "^8.4.45",
|
||||
"postcss-nesting": "^13.0.0",
|
||||
"prettier": "^3.3.3",
|
||||
"react-devtools": "^5.3.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^5.4.4",
|
||||
"vite-plugin-static-copy": "^1.0.6",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-plugin-top-level-await": "^1.4.1"
|
||||
"vite-plugin-top-level-await": "^1.4.4"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": "eslint --cache --fix",
|
||||
|
||||
31
plugin-runtime-types/package-lock.json
generated
31
plugin-runtime-types/package-lock.json
generated
@@ -1,32 +1,34 @@
|
||||
{
|
||||
"name": "@yaakapp/api",
|
||||
"version": "0.1.0-beta.4",
|
||||
"version": "0.1.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@yaakapp/api",
|
||||
"version": "0.1.0-beta.4",
|
||||
"version": "0.1.17",
|
||||
"dependencies": {
|
||||
"@types/node": "^22.0.0"
|
||||
"@types/node": "^22.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.4"
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.0.tgz",
|
||||
"integrity": "sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==",
|
||||
"version": "22.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
|
||||
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.11.1"
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -36,9 +38,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.11.1",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.11.1.tgz",
|
||||
"integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ=="
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@yaakapp/api",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.17",
|
||||
"main": "lib/index.js",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"files": [
|
||||
@@ -11,9 +11,9 @@
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^22.0.0"
|
||||
"@types/node": "^22.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.4"
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HttpRequest } from "./HttpRequest";
|
||||
|
||||
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CallHttpRequestActionArgs } from "./CallHttpRequestActionArgs";
|
||||
|
||||
export type CallHttpRequestActionRequest = { key: string, pluginRefId: string, args: CallHttpRequestActionArgs, };
|
||||
4
plugin-runtime-types/src/gen/CallTemplateFunctionArgs.ts
Normal file
4
plugin-runtime-types/src/gen/CallTemplateFunctionArgs.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { RenderPurpose } from "./RenderPurpose";
|
||||
|
||||
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key: string]: string }, };
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { CallTemplateFunctionArgs } from "./CallTemplateFunctionArgs";
|
||||
|
||||
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type BootResponse = { name: string, version: string, capabilities: Array<string>, };
|
||||
export type CallTemplateFunctionResponse = { value: string | null, };
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type CookieExpires = { "AtUtc": string } | "SessionEnd";
|
||||
export type CopyTextRequest = { text: string, };
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type CookieDomain = { "HostOnly": string } | { "Suffix": string } | "NotPresent" | "Empty";
|
||||
export type FindHttpResponsesRequest = { requestId: string, limit: number | null, };
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HttpResponse } from "./HttpResponse";
|
||||
|
||||
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type GetHttpRequestActionsRequest = Record<string, never>;
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HttpRequestAction } from "./HttpRequestAction";
|
||||
|
||||
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type GetHttpRequestByIdRequest = { id: string, };
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HttpRequest } from "./HttpRequest";
|
||||
|
||||
export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { TemplateFunction } from "./TemplateFunction";
|
||||
|
||||
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
|
||||
@@ -1,4 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { GrpcEventType } from "./GrpcEventType";
|
||||
|
||||
export type GrpcEvent = { id: string, model: "grpc_event", workspaceId: string, requestId: string, connectionId: string, createdAt: string, content: string, eventType: GrpcEventType, metadata: { [key: string]: string }, status: number | null, error: string | null, };
|
||||
export type GrpcEvent = { id: string, model: "grpc_event", workspaceId: string, requestId: string, connectionId: string, createdAt: string, updatedAt: string, content: string, eventType: GrpcEventType, metadata: { [key: string]: string }, status: number | null, error: string | null, };
|
||||
|
||||
3
plugin-runtime-types/src/gen/HttpRequestAction.ts
Normal file
3
plugin-runtime-types/src/gen/HttpRequestAction.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type HttpRequestAction = { key: string, label: string, icon: string | null, };
|
||||
@@ -1,12 +1,30 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BootRequest } from "./BootRequest";
|
||||
import type { BootResponse } from "./BootResponse";
|
||||
import type { CallHttpRequestActionRequest } from "./CallHttpRequestActionRequest";
|
||||
import type { CallTemplateFunctionRequest } from "./CallTemplateFunctionRequest";
|
||||
import type { CallTemplateFunctionResponse } from "./CallTemplateFunctionResponse";
|
||||
import type { CopyTextRequest } from "./CopyTextRequest";
|
||||
import type { EmptyResponse } from "./EmptyResponse";
|
||||
import type { ExportHttpRequestRequest } from "./ExportHttpRequestRequest";
|
||||
import type { ExportHttpRequestResponse } from "./ExportHttpRequestResponse";
|
||||
import type { FilterRequest } from "./FilterRequest";
|
||||
import type { FilterResponse } from "./FilterResponse";
|
||||
import type { FindHttpResponsesRequest } from "./FindHttpResponsesRequest";
|
||||
import type { FindHttpResponsesResponse } from "./FindHttpResponsesResponse";
|
||||
import type { GetHttpRequestActionsRequest } from "./GetHttpRequestActionsRequest";
|
||||
import type { GetHttpRequestActionsResponse } from "./GetHttpRequestActionsResponse";
|
||||
import type { GetHttpRequestByIdRequest } from "./GetHttpRequestByIdRequest";
|
||||
import type { GetHttpRequestByIdResponse } from "./GetHttpRequestByIdResponse";
|
||||
import type { GetTemplateFunctionsResponse } from "./GetTemplateFunctionsResponse";
|
||||
import type { ImportRequest } from "./ImportRequest";
|
||||
import type { ImportResponse } from "./ImportResponse";
|
||||
import type { PluginBootRequest } from "./PluginBootRequest";
|
||||
import type { PluginBootResponse } from "./PluginBootResponse";
|
||||
import type { PluginReloadRequest } from "./PluginReloadRequest";
|
||||
import type { PluginReloadResponse } from "./PluginReloadResponse";
|
||||
import type { RenderHttpRequestRequest } from "./RenderHttpRequestRequest";
|
||||
import type { RenderHttpRequestResponse } from "./RenderHttpRequestResponse";
|
||||
import type { SendHttpRequestRequest } from "./SendHttpRequestRequest";
|
||||
import type { SendHttpRequestResponse } from "./SendHttpRequestResponse";
|
||||
import type { ShowToastRequest } from "./ShowToastRequest";
|
||||
|
||||
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "empty_response" } & EmptyResponse;
|
||||
export type InternalEventPayload = { "type": "boot_request" } & PluginBootRequest | { "type": "boot_response" } & PluginBootResponse | { "type": "reload_request" } & PluginReloadRequest | { "type": "reload_response" } & PluginReloadResponse | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & GetHttpRequestActionsRequest | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyResponse;
|
||||
|
||||
@@ -8,7 +8,8 @@ import type { GrpcRequest } from "./GrpcRequest";
|
||||
import type { HttpRequest } from "./HttpRequest";
|
||||
import type { HttpResponse } from "./HttpResponse";
|
||||
import type { KeyValue } from "./KeyValue";
|
||||
import type { Plugin } from "./Plugin";
|
||||
import type { Settings } from "./Settings";
|
||||
import type { Workspace } from "./Workspace";
|
||||
|
||||
export type Model = Environment | Folder | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | KeyValue | Workspace | CookieJar | Settings;
|
||||
export type Model = Environment | Folder | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | KeyValue | Workspace | CookieJar | Settings | Plugin;
|
||||
|
||||
3
plugin-runtime-types/src/gen/Plugin.ts
Normal file
3
plugin-runtime-types/src/gen/Plugin.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Plugin = { id: string, model: "plugin", createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, url: string | null, enabled: boolean, };
|
||||
3
plugin-runtime-types/src/gen/PluginBootRequest.ts
Normal file
3
plugin-runtime-types/src/gen/PluginBootRequest.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PluginBootRequest = { dir: string, };
|
||||
3
plugin-runtime-types/src/gen/PluginBootResponse.ts
Normal file
3
plugin-runtime-types/src/gen/PluginBootResponse.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PluginBootResponse = { name: string, version: string, capabilities: Array<string>, };
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type BootRequest = { dir: string, };
|
||||
export type PluginReloadRequest = {};
|
||||
3
plugin-runtime-types/src/gen/PluginReloadResponse.ts
Normal file
3
plugin-runtime-types/src/gen/PluginReloadResponse.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PluginReloadResponse = {};
|
||||
5
plugin-runtime-types/src/gen/RenderHttpRequestRequest.ts
Normal file
5
plugin-runtime-types/src/gen/RenderHttpRequestRequest.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HttpRequest } from "./HttpRequest";
|
||||
import type { RenderPurpose } from "./RenderPurpose";
|
||||
|
||||
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HttpRequest } from "./HttpRequest";
|
||||
|
||||
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
|
||||
3
plugin-runtime-types/src/gen/RenderPurpose.ts
Normal file
3
plugin-runtime-types/src/gen/RenderPurpose.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type RenderPurpose = "send" | "preview";
|
||||
4
plugin-runtime-types/src/gen/SendHttpRequestRequest.ts
Normal file
4
plugin-runtime-types/src/gen/SendHttpRequestRequest.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HttpRequest } from "./HttpRequest";
|
||||
|
||||
export type SendHttpRequestRequest = { httpRequest: HttpRequest, };
|
||||
4
plugin-runtime-types/src/gen/SendHttpRequestResponse.ts
Normal file
4
plugin-runtime-types/src/gen/SendHttpRequestResponse.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HttpResponse } from "./HttpResponse";
|
||||
|
||||
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Settings = { id: string, model: "settings", createdAt: string, updatedAt: string, theme: string, appearance: string, themeDark: string, themeLight: string, updateChannel: string, interfaceFontSize: number, interfaceScale: number, editorFontSize: number, editorSoftWrap: boolean, openWorkspaceNewWindow: boolean | null, };
|
||||
export type Settings = { id: string, model: "settings", createdAt: string, updatedAt: string, theme: string, appearance: string, themeDark: string, themeLight: string, updateChannel: string, interfaceFontSize: number, interfaceScale: number, editorFontSize: number, editorSoftWrap: boolean, telemetry: boolean, openWorkspaceNewWindow: boolean | null, };
|
||||
|
||||
4
plugin-runtime-types/src/gen/ShowToastRequest.ts
Normal file
4
plugin-runtime-types/src/gen/ShowToastRequest.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ToastVariant } from "./ToastVariant";
|
||||
|
||||
export type ShowToastRequest = { message: string, variant: ToastVariant, };
|
||||
4
plugin-runtime-types/src/gen/TemplateFunction.ts
Normal file
4
plugin-runtime-types/src/gen/TemplateFunction.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { TemplateFunctionArg } from "./TemplateFunctionArg";
|
||||
|
||||
export type TemplateFunction = { name: string, args: Array<TemplateFunctionArg>, };
|
||||
7
plugin-runtime-types/src/gen/TemplateFunctionArg.ts
Normal file
7
plugin-runtime-types/src/gen/TemplateFunctionArg.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { TemplateFunctionCheckboxArg } from "./TemplateFunctionCheckboxArg";
|
||||
import type { TemplateFunctionHttpRequestArg } from "./TemplateFunctionHttpRequestArg";
|
||||
import type { TemplateFunctionSelectArg } from "./TemplateFunctionSelectArg";
|
||||
import type { TemplateFunctionTextArg } from "./TemplateFunctionTextArg";
|
||||
|
||||
export type TemplateFunctionArg = { "type": "text" } & TemplateFunctionTextArg | { "type": "select" } & TemplateFunctionSelectArg | { "type": "checkbox" } & TemplateFunctionCheckboxArg | { "type": "http_request" } & TemplateFunctionHttpRequestArg;
|
||||
3
plugin-runtime-types/src/gen/TemplateFunctionBaseArg.ts
Normal file
3
plugin-runtime-types/src/gen/TemplateFunctionBaseArg.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type TemplateFunctionBaseArg = { name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type TemplateFunctionCheckboxArg = { name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type TemplateFunctionHttpRequestArg = { name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { TemplateFunctionSelectOption } from "./TemplateFunctionSelectOption";
|
||||
|
||||
export type TemplateFunctionSelectArg = { options: Array<TemplateFunctionSelectOption>, name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type TemplateFunctionSelectOption = { name: string, value: string, };
|
||||
3
plugin-runtime-types/src/gen/TemplateFunctionTextArg.ts
Normal file
3
plugin-runtime-types/src/gen/TemplateFunctionTextArg.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type TemplateFunctionTextArg = { placeholder?: string | null, name: string, optional?: boolean | null, label?: string | null, defaultValue?: string | null, };
|
||||
3
plugin-runtime-types/src/gen/ToastVariant.ts
Normal file
3
plugin-runtime-types/src/gen/ToastVariant.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ToastVariant = "custom" | "copied" | "success" | "info" | "warning" | "error";
|
||||
@@ -1,12 +1,17 @@
|
||||
export type * from './plugins';
|
||||
export type * from './themes';
|
||||
|
||||
export * from './gen/BootRequest';
|
||||
export * from './gen/BootResponse';
|
||||
// TODO: The next ts-rs release includes the ability to put everything in 1 file!
|
||||
export * from './gen/CallHttpRequestActionArgs';
|
||||
export * from './gen/CallHttpRequestActionRequest';
|
||||
export * from './gen/CallTemplateFunctionRequest';
|
||||
export * from './gen/CallTemplateFunctionResponse';
|
||||
export * from './gen/CallTemplateFunctionArgs';
|
||||
export * from './gen/Cookie';
|
||||
export * from './gen/CookieDomain';
|
||||
export * from './gen/CookieExpires';
|
||||
export * from './gen/CookieJar';
|
||||
export * from './gen/CopyTextRequest';
|
||||
export * from './gen/EmptyResponse';
|
||||
export * from './gen/Environment';
|
||||
export * from './gen/EnvironmentVariable';
|
||||
@@ -15,11 +20,18 @@ export * from './gen/ExportHttpRequestResponse';
|
||||
export * from './gen/FilterRequest';
|
||||
export * from './gen/FilterResponse';
|
||||
export * from './gen/Folder';
|
||||
export * from './gen/FindHttpResponsesRequest';
|
||||
export * from './gen/FindHttpResponsesResponse';
|
||||
export * from './gen/GetHttpRequestActionsResponse';
|
||||
export * from './gen/GetHttpRequestByIdRequest';
|
||||
export * from './gen/GetHttpRequestByIdResponse';
|
||||
export * from './gen/GetTemplateFunctionsResponse';
|
||||
export * from './gen/GrpcConnection';
|
||||
export * from './gen/GrpcEvent';
|
||||
export * from './gen/GrpcMetadataEntry';
|
||||
export * from './gen/GrpcRequest';
|
||||
export * from './gen/HttpRequest';
|
||||
export * from './gen/HttpRequestAction';
|
||||
export * from './gen/HttpRequestHeader';
|
||||
export * from './gen/HttpResponse';
|
||||
export * from './gen/HttpResponseHeader';
|
||||
@@ -31,5 +43,26 @@ export * from './gen/InternalEvent';
|
||||
export * from './gen/InternalEventPayload';
|
||||
export * from './gen/KeyValue';
|
||||
export * from './gen/Model';
|
||||
export * from './gen/PluginBootRequest';
|
||||
export * from './gen/PluginBootResponse';
|
||||
export * from './gen/PluginReloadRequest';
|
||||
export * from './gen/PluginReloadResponse';
|
||||
export * from './gen/RenderHttpRequestRequest';
|
||||
export * from './gen/RenderHttpRequestResponse';
|
||||
export * from './gen/RenderPurpose';
|
||||
export * from './gen/SendHttpRequestRequest';
|
||||
export * from './gen/SendHttpRequestResponse';
|
||||
export * from './gen/SendHttpRequestResponse';
|
||||
export * from './gen/Settings';
|
||||
export * from './gen/ShowToastRequest';
|
||||
export * from './gen/TemplateFunction';
|
||||
export * from './gen/TemplateFunctionArg';
|
||||
export * from './gen/TemplateFunctionBaseArg';
|
||||
export * from './gen/TemplateFunctionCheckboxArg';
|
||||
export * from './gen/TemplateFunctionHttpRequestArg';
|
||||
export * from './gen/TemplateFunctionSelectArg';
|
||||
export * from './gen/TemplateFunctionSelectOption';
|
||||
export * from './gen/TemplateFunctionTextArg';
|
||||
export * from './gen/ToastVariant';
|
||||
export * from './gen/Workspace';
|
||||
export * from './gen/Plugin';
|
||||
|
||||
26
plugin-runtime-types/src/plugins/Context.ts
Normal file
26
plugin-runtime-types/src/plugins/Context.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { FindHttpResponsesRequest } from '../gen/FindHttpResponsesRequest';
|
||||
import { FindHttpResponsesResponse } from '../gen/FindHttpResponsesResponse';
|
||||
import { GetHttpRequestByIdRequest } from '../gen/GetHttpRequestByIdRequest';
|
||||
import { GetHttpRequestByIdResponse } from '../gen/GetHttpRequestByIdResponse';
|
||||
import { RenderHttpRequestRequest } from '../gen/RenderHttpRequestRequest';
|
||||
import { RenderHttpRequestResponse } from '../gen/RenderHttpRequestResponse';
|
||||
import { SendHttpRequestRequest } from '../gen/SendHttpRequestRequest';
|
||||
import { SendHttpRequestResponse } from '../gen/SendHttpRequestResponse';
|
||||
import { ShowToastRequest } from '../gen/ShowToastRequest';
|
||||
|
||||
export type Context = {
|
||||
clipboard: {
|
||||
copyText(text: string): void;
|
||||
};
|
||||
toast: {
|
||||
show(args: ShowToastRequest): void;
|
||||
};
|
||||
httpRequest: {
|
||||
send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
|
||||
getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;
|
||||
render(args: RenderHttpRequestRequest): Promise<RenderHttpRequestResponse['httpRequest']>;
|
||||
};
|
||||
httpResponse: {
|
||||
find(args: FindHttpResponsesRequest): Promise<FindHttpResponsesResponse['httpResponses']>;
|
||||
};
|
||||
};
|
||||
@@ -1,13 +1,13 @@
|
||||
import { YaakContext } from './context';
|
||||
import { Context } from './Context';
|
||||
|
||||
export type FilterPluginResponse = string[];
|
||||
|
||||
export type FilterPlugin = {
|
||||
name: string;
|
||||
description?: string;
|
||||
canFilter(ctx: YaakContext, args: { mimeType: string }): Promise<boolean>;
|
||||
canFilter(ctx: Context, args: { mimeType: string }): Promise<boolean>;
|
||||
onFilter(
|
||||
ctx: YaakContext,
|
||||
ctx: Context,
|
||||
args: { payload: string; mimeType: string },
|
||||
): Promise<FilterPluginResponse>;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { CallHttpRequestActionArgs } from '../gen/CallHttpRequestActionArgs';
|
||||
import { HttpRequestAction } from '../gen/HttpRequestAction';
|
||||
import { Context } from './Context';
|
||||
|
||||
export type HttpRequestActionPlugin = HttpRequestAction & {
|
||||
onSelect(ctx: Context, args: CallHttpRequestActionArgs): Promise<void> | void;
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { Folder } from '../gen/Folder';
|
||||
import { HttpRequest } from '../gen/HttpRequest';
|
||||
import { Workspace } from '../gen/Workspace';
|
||||
import { AtLeast } from '../helpers';
|
||||
import { YaakContext } from './context';
|
||||
import { Context } from './Context';
|
||||
|
||||
export type ImportPluginResponse = null | {
|
||||
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
|
||||
@@ -15,5 +15,5 @@ export type ImportPluginResponse = null | {
|
||||
export type ImporterPlugin = {
|
||||
name: string;
|
||||
description?: string;
|
||||
onImport(ctx: YaakContext, args: { text: string }): Promise<ImportPluginResponse>;
|
||||
onImport(ctx: Context, args: { text: string }): Promise<ImportPluginResponse>;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
import { CallTemplateFunctionArgs } from '../gen/CallTemplateFunctionArgs';
|
||||
import { TemplateFunction } from '../gen/TemplateFunction';
|
||||
import { Context } from './Context';
|
||||
|
||||
export type TemplateFunctionPlugin = TemplateFunction & {
|
||||
onRender(ctx: Context, args: CallTemplateFunctionArgs): Promise<string | null>;
|
||||
};
|
||||
8
plugin-runtime-types/src/plugins/ThemePlugin.ts
Normal file
8
plugin-runtime-types/src/plugins/ThemePlugin.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Theme } from '../themes';
|
||||
import { Context } from './Context';
|
||||
|
||||
export type ThemePlugin = {
|
||||
name: string;
|
||||
description?: string;
|
||||
getTheme(ctx: Context, fileContents: string): Promise<Theme>;
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
import { HttpRequest } from '../gen/HttpRequest';
|
||||
import { HttpResponse } from '../gen/HttpResponse';
|
||||
|
||||
export type YaakContext = {
|
||||
metadata: {
|
||||
getVersion(): Promise<string>;
|
||||
};
|
||||
httpRequest: {
|
||||
send(id: string): Promise<HttpResponse>;
|
||||
getById(id: string): Promise<HttpRequest | null>;
|
||||
};
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { HttpRequest } from '../gen/HttpRequest';
|
||||
import { YaakContext } from './context';
|
||||
|
||||
export type HttpRequestActionPlugin = {
|
||||
key: string;
|
||||
label: string;
|
||||
onSelect(ctx: YaakContext, args: { httpRequest: HttpRequest }): void;
|
||||
};
|
||||
@@ -1,15 +1,18 @@
|
||||
import { OneOrMany } from '../helpers';
|
||||
import { FilterPlugin } from './filter';
|
||||
import { HttpRequestActionPlugin } from './httpRequestAction';
|
||||
import { ImporterPlugin } from './import';
|
||||
import { ThemePlugin } from './theme';
|
||||
import { FilterPlugin } from './FilterPlugin';
|
||||
import { HttpRequestActionPlugin } from './HttpRequestActionPlugin';
|
||||
import { ImporterPlugin } from './ImporterPlugin';
|
||||
import { TemplateFunctionPlugin } from './TemplateFunctionPlugin';
|
||||
import { ThemePlugin } from './ThemePlugin';
|
||||
|
||||
export type { Context } from './Context';
|
||||
|
||||
/**
|
||||
* The global structure of a Yaak plugin
|
||||
*/
|
||||
export type YaakPlugin = {
|
||||
importer?: OneOrMany<ImporterPlugin>;
|
||||
theme?: OneOrMany<ThemePlugin>;
|
||||
filter?: OneOrMany<FilterPlugin>;
|
||||
httpRequestAction?: OneOrMany<HttpRequestActionPlugin>;
|
||||
export type PluginDefinition = {
|
||||
importer?: ImporterPlugin;
|
||||
theme?: ThemePlugin;
|
||||
filter?: FilterPlugin;
|
||||
httpRequestActions?: HttpRequestActionPlugin[];
|
||||
templateFunctions?: TemplateFunctionPlugin[];
|
||||
};
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Theme } from '../themes';
|
||||
import { YaakContext } from './context';
|
||||
|
||||
export type ThemePlugin = {
|
||||
name: string;
|
||||
description?: string;
|
||||
getTheme(ctx: YaakContext, fileContents: string): Promise<Theme>;
|
||||
};
|
||||
492
plugin-runtime/package-lock.json
generated
492
plugin-runtime/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@
|
||||
"intercept-stdout": "^0.1.2",
|
||||
"long": "^5.2.3",
|
||||
"nice-grpc": "^2.1.9",
|
||||
"protobufjs": "^7.3.2"
|
||||
"protobufjs": "^7.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/intercept-stdout": "^0.1.3",
|
||||
@@ -19,7 +19,7 @@
|
||||
"nodemon": "^3.1.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-proto": "^1.180.0",
|
||||
"typescript": "^5.5.2"
|
||||
"ts-proto": "^2.2.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,30 +4,35 @@ import { Worker } from 'node:worker_threads';
|
||||
import { EventChannel } from './EventChannel';
|
||||
|
||||
export class PluginHandle {
|
||||
readonly #worker: Worker;
|
||||
#worker: Worker;
|
||||
|
||||
constructor(
|
||||
readonly pluginDir: string,
|
||||
readonly pluginRefId: string,
|
||||
readonly events: EventChannel,
|
||||
) {
|
||||
const workerPath = process.env.YAAK_WORKER_PATH ?? path.join(__dirname, 'index.worker.cjs');
|
||||
this.#worker = new Worker(workerPath, {
|
||||
workerData: {
|
||||
pluginDir,
|
||||
pluginRefId,
|
||||
},
|
||||
});
|
||||
|
||||
this.#worker.on('message', (e) => this.events.emit(e));
|
||||
this.#worker.on('error', this.#handleError.bind(this));
|
||||
this.#worker.on('exit', this.#handleExit.bind(this));
|
||||
this.#worker = this.#createWorker();
|
||||
}
|
||||
|
||||
sendToWorker(event: InternalEvent) {
|
||||
this.#worker.postMessage(event);
|
||||
}
|
||||
|
||||
#createWorker(): Worker {
|
||||
const workerPath = process.env.YAAK_WORKER_PATH ?? path.join(__dirname, 'index.worker.cjs');
|
||||
const worker = new Worker(workerPath, {
|
||||
workerData: { pluginDir: this.pluginDir, pluginRefId: this.pluginRefId },
|
||||
});
|
||||
|
||||
worker.on('message', (e) => this.events.emit(e));
|
||||
worker.on('error', this.#handleError.bind(this));
|
||||
worker.on('exit', this.#handleExit.bind(this));
|
||||
|
||||
console.log('Created plugin worker for ', this.pluginDir);
|
||||
|
||||
return worker;
|
||||
}
|
||||
|
||||
async #handleError(err: Error) {
|
||||
console.error('Plugin errored', this.pluginDir, err);
|
||||
}
|
||||
@@ -36,7 +41,7 @@ export class PluginHandle {
|
||||
if (code === 0) {
|
||||
console.log('Plugin exited successfully', this.pluginDir);
|
||||
} else {
|
||||
console.log('Plugin exited with error', code, this.pluginDir);
|
||||
console.log('Plugin exited with status', code, this.pluginDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,392 +1,33 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v1.180.0
|
||||
// protoc-gen-ts_proto v2.2.0
|
||||
// protoc v3.19.1
|
||||
// source: plugins/runtime.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
import { type CallContext, type CallOptions } from "nice-grpc-common";
|
||||
import * as _m0 from "protobufjs/minimal";
|
||||
|
||||
export const protobufPackage = "yaak.plugins.runtime";
|
||||
|
||||
export interface PluginInfo {
|
||||
plugin: string;
|
||||
}
|
||||
|
||||
export interface HookResponse {
|
||||
info: PluginInfo | undefined;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface HookImportRequest {
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface HookResponseFilterRequest {
|
||||
filter: string;
|
||||
body: string;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
export interface HookExportRequest {
|
||||
request: string;
|
||||
}
|
||||
|
||||
export interface EventStreamEvent {
|
||||
event: string;
|
||||
}
|
||||
|
||||
function createBasePluginInfo(): PluginInfo {
|
||||
return { plugin: "" };
|
||||
}
|
||||
|
||||
export const PluginInfo = {
|
||||
encode(message: PluginInfo, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
if (message.plugin !== "") {
|
||||
writer.uint32(10).string(message.plugin);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): PluginInfo {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBasePluginInfo();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.plugin = reader.string();
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): PluginInfo {
|
||||
return { plugin: isSet(object.plugin) ? globalThis.String(object.plugin) : "" };
|
||||
},
|
||||
|
||||
toJSON(message: PluginInfo): unknown {
|
||||
const obj: any = {};
|
||||
if (message.plugin !== "") {
|
||||
obj.plugin = message.plugin;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create(base?: DeepPartial<PluginInfo>): PluginInfo {
|
||||
return PluginInfo.fromPartial(base ?? {});
|
||||
},
|
||||
fromPartial(object: DeepPartial<PluginInfo>): PluginInfo {
|
||||
const message = createBasePluginInfo();
|
||||
message.plugin = object.plugin ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseHookResponse(): HookResponse {
|
||||
return { info: undefined, data: "" };
|
||||
}
|
||||
|
||||
export const HookResponse = {
|
||||
encode(message: HookResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
if (message.info !== undefined) {
|
||||
PluginInfo.encode(message.info, writer.uint32(10).fork()).ldelim();
|
||||
}
|
||||
if (message.data !== "") {
|
||||
writer.uint32(18).string(message.data);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): HookResponse {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseHookResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.info = PluginInfo.decode(reader, reader.uint32());
|
||||
continue;
|
||||
case 2:
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.data = reader.string();
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): HookResponse {
|
||||
return {
|
||||
info: isSet(object.info) ? PluginInfo.fromJSON(object.info) : undefined,
|
||||
data: isSet(object.data) ? globalThis.String(object.data) : "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: HookResponse): unknown {
|
||||
const obj: any = {};
|
||||
if (message.info !== undefined) {
|
||||
obj.info = PluginInfo.toJSON(message.info);
|
||||
}
|
||||
if (message.data !== "") {
|
||||
obj.data = message.data;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create(base?: DeepPartial<HookResponse>): HookResponse {
|
||||
return HookResponse.fromPartial(base ?? {});
|
||||
},
|
||||
fromPartial(object: DeepPartial<HookResponse>): HookResponse {
|
||||
const message = createBaseHookResponse();
|
||||
message.info = (object.info !== undefined && object.info !== null)
|
||||
? PluginInfo.fromPartial(object.info)
|
||||
: undefined;
|
||||
message.data = object.data ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseHookImportRequest(): HookImportRequest {
|
||||
return { data: "" };
|
||||
}
|
||||
|
||||
export const HookImportRequest = {
|
||||
encode(message: HookImportRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
if (message.data !== "") {
|
||||
writer.uint32(10).string(message.data);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): HookImportRequest {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseHookImportRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.data = reader.string();
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): HookImportRequest {
|
||||
return { data: isSet(object.data) ? globalThis.String(object.data) : "" };
|
||||
},
|
||||
|
||||
toJSON(message: HookImportRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.data !== "") {
|
||||
obj.data = message.data;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create(base?: DeepPartial<HookImportRequest>): HookImportRequest {
|
||||
return HookImportRequest.fromPartial(base ?? {});
|
||||
},
|
||||
fromPartial(object: DeepPartial<HookImportRequest>): HookImportRequest {
|
||||
const message = createBaseHookImportRequest();
|
||||
message.data = object.data ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseHookResponseFilterRequest(): HookResponseFilterRequest {
|
||||
return { filter: "", body: "", contentType: "" };
|
||||
}
|
||||
|
||||
export const HookResponseFilterRequest = {
|
||||
encode(message: HookResponseFilterRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
if (message.filter !== "") {
|
||||
writer.uint32(10).string(message.filter);
|
||||
}
|
||||
if (message.body !== "") {
|
||||
writer.uint32(18).string(message.body);
|
||||
}
|
||||
if (message.contentType !== "") {
|
||||
writer.uint32(26).string(message.contentType);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): HookResponseFilterRequest {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseHookResponseFilterRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.filter = reader.string();
|
||||
continue;
|
||||
case 2:
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.body = reader.string();
|
||||
continue;
|
||||
case 3:
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.contentType = reader.string();
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): HookResponseFilterRequest {
|
||||
return {
|
||||
filter: isSet(object.filter) ? globalThis.String(object.filter) : "",
|
||||
body: isSet(object.body) ? globalThis.String(object.body) : "",
|
||||
contentType: isSet(object.contentType) ? globalThis.String(object.contentType) : "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: HookResponseFilterRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.filter !== "") {
|
||||
obj.filter = message.filter;
|
||||
}
|
||||
if (message.body !== "") {
|
||||
obj.body = message.body;
|
||||
}
|
||||
if (message.contentType !== "") {
|
||||
obj.contentType = message.contentType;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create(base?: DeepPartial<HookResponseFilterRequest>): HookResponseFilterRequest {
|
||||
return HookResponseFilterRequest.fromPartial(base ?? {});
|
||||
},
|
||||
fromPartial(object: DeepPartial<HookResponseFilterRequest>): HookResponseFilterRequest {
|
||||
const message = createBaseHookResponseFilterRequest();
|
||||
message.filter = object.filter ?? "";
|
||||
message.body = object.body ?? "";
|
||||
message.contentType = object.contentType ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseHookExportRequest(): HookExportRequest {
|
||||
return { request: "" };
|
||||
}
|
||||
|
||||
export const HookExportRequest = {
|
||||
encode(message: HookExportRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
if (message.request !== "") {
|
||||
writer.uint32(10).string(message.request);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): HookExportRequest {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseHookExportRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.request = reader.string();
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): HookExportRequest {
|
||||
return { request: isSet(object.request) ? globalThis.String(object.request) : "" };
|
||||
},
|
||||
|
||||
toJSON(message: HookExportRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.request !== "") {
|
||||
obj.request = message.request;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create(base?: DeepPartial<HookExportRequest>): HookExportRequest {
|
||||
return HookExportRequest.fromPartial(base ?? {});
|
||||
},
|
||||
fromPartial(object: DeepPartial<HookExportRequest>): HookExportRequest {
|
||||
const message = createBaseHookExportRequest();
|
||||
message.request = object.request ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
function createBaseEventStreamEvent(): EventStreamEvent {
|
||||
return { event: "" };
|
||||
}
|
||||
|
||||
export const EventStreamEvent = {
|
||||
encode(message: EventStreamEvent, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const EventStreamEvent: MessageFns<EventStreamEvent> = {
|
||||
encode(message: EventStreamEvent, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.event !== "") {
|
||||
writer.uint32(10).string(message.event);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): EventStreamEvent {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): EventStreamEvent {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseEventStreamEvent();
|
||||
while (reader.pos < end) {
|
||||
@@ -403,7 +44,7 @@ export const EventStreamEvent = {
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
@@ -473,3 +114,12 @@ function isSet(value: any): boolean {
|
||||
}
|
||||
|
||||
export type ServerStreamingMethodResult<Response> = { [Symbol.asyncIterator](): AsyncIterator<Response, void> };
|
||||
|
||||
export interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create(base?: DeepPartial<T>): T;
|
||||
fromPartial(object: DeepPartial<T>): T;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
import { ImportResponse, InternalEvent, InternalEventPayload } from '@yaakapp/api';
|
||||
import {
|
||||
Context,
|
||||
FindHttpResponsesResponse,
|
||||
GetHttpRequestByIdResponse,
|
||||
HttpRequestAction,
|
||||
ImportResponse,
|
||||
InternalEvent,
|
||||
InternalEventPayload,
|
||||
RenderHttpRequestResponse,
|
||||
SendHttpRequestResponse,
|
||||
TemplateFunction,
|
||||
} from '@yaakapp/api';
|
||||
import { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/HttpRequestActionPlugin';
|
||||
import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin';
|
||||
import interceptStdout from 'intercept-stdout';
|
||||
import * as console from 'node:console';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { Stats, readFileSync, statSync, watch } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as util from 'node:util';
|
||||
import { parentPort, workerData } from 'node:worker_threads';
|
||||
|
||||
new Promise<void>(async (resolve, reject) => {
|
||||
const { pluginDir /*, pluginRefId*/ } = workerData;
|
||||
async function initialize() {
|
||||
const { pluginDir, pluginRefId } = workerData;
|
||||
const pathPkg = path.join(pluginDir, 'package.json');
|
||||
|
||||
// NOTE: Use POSIX join because require() needs forward slash
|
||||
const pathMod = path.posix.join(pluginDir, 'build', 'index.js');
|
||||
|
||||
let pkg: { [x: string]: any };
|
||||
try {
|
||||
pkg = JSON.parse(readFileSync(pathPkg, 'utf8'));
|
||||
} catch (err) {
|
||||
// TODO: Do something better here
|
||||
reject(err);
|
||||
return;
|
||||
async function importModule() {
|
||||
const id = require.resolve(pathMod);
|
||||
delete require.cache[id];
|
||||
return require(id);
|
||||
}
|
||||
|
||||
const pkg = JSON.parse(readFileSync(pathPkg, 'utf8'));
|
||||
|
||||
prefixStdout(`[plugin][${pkg.name}] %s`);
|
||||
|
||||
const mod = (await import(pathMod)).default ?? {};
|
||||
let mod = await importModule();
|
||||
|
||||
const capabilities: string[] = [];
|
||||
if (typeof mod.pluginHookExport === 'function') capabilities.push('export');
|
||||
@@ -33,10 +43,106 @@ new Promise<void>(async (resolve, reject) => {
|
||||
|
||||
console.log('Plugin initialized', pkg.name, capabilities, Object.keys(mod));
|
||||
|
||||
// Message comes into the plugin to be processed
|
||||
parentPort!.on('message', async ({ payload, pluginRefId, id: replyId }: InternalEvent) => {
|
||||
console.log(`Received ${payload.type}`);
|
||||
function buildEventToSend(
|
||||
payload: InternalEventPayload,
|
||||
replyId: string | null = null,
|
||||
): InternalEvent {
|
||||
return { pluginRefId, id: genId(), replyId, payload };
|
||||
}
|
||||
|
||||
function sendEmpty(replyId: string | null = null): string {
|
||||
return sendPayload({ type: 'empty_response' }, replyId);
|
||||
}
|
||||
|
||||
function sendPayload(payload: InternalEventPayload, replyId: string | null): string {
|
||||
const event = buildEventToSend(payload, replyId);
|
||||
sendEvent(event);
|
||||
return event.id;
|
||||
}
|
||||
|
||||
function sendEvent(event: InternalEvent) {
|
||||
if (event.payload.type !== 'empty_response') {
|
||||
console.log('Sending event to app', event.id, event.payload.type);
|
||||
}
|
||||
parentPort!.postMessage(event);
|
||||
}
|
||||
|
||||
async function sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
|
||||
payload: InternalEventPayload,
|
||||
): Promise<T> {
|
||||
// 1. Build event to send
|
||||
const eventToSend = buildEventToSend(payload, null);
|
||||
|
||||
// 2. Spawn listener in background
|
||||
const promise = new Promise<InternalEventPayload>(async (resolve) => {
|
||||
const cb = (event: InternalEvent) => {
|
||||
if (event.replyId === eventToSend.id) {
|
||||
parentPort!.off('message', cb); // Unlisten, now that we're done
|
||||
resolve(event.payload); // Not type-safe but oh well
|
||||
}
|
||||
};
|
||||
parentPort!.on('message', cb);
|
||||
});
|
||||
|
||||
// 3. Send the event after we start listening (to prevent race)
|
||||
sendEvent(eventToSend);
|
||||
|
||||
// 4. Return the listener promise
|
||||
return promise as unknown as Promise<T>;
|
||||
}
|
||||
|
||||
async function reloadModule() {
|
||||
mod = await importModule();
|
||||
}
|
||||
|
||||
// Reload plugin if JS or package.json changes
|
||||
const cb = async () => {
|
||||
await reloadModule();
|
||||
return sendPayload({ type: 'reload_response' }, null);
|
||||
};
|
||||
|
||||
watchFile(pathMod, cb);
|
||||
watchFile(pathPkg, cb);
|
||||
|
||||
const ctx: Context = {
|
||||
clipboard: {
|
||||
async copyText(text) {
|
||||
await sendAndWaitForReply({ type: 'copy_text_request', text });
|
||||
},
|
||||
},
|
||||
toast: {
|
||||
async show(args) {
|
||||
await sendAndWaitForReply({ type: 'show_toast_request', ...args });
|
||||
},
|
||||
},
|
||||
httpResponse: {
|
||||
async find(args) {
|
||||
const payload = { type: 'find_http_responses_request', ...args } as const;
|
||||
const { httpResponses } = await sendAndWaitForReply<FindHttpResponsesResponse>(payload);
|
||||
return httpResponses;
|
||||
},
|
||||
},
|
||||
httpRequest: {
|
||||
async getById(args) {
|
||||
const payload = { type: 'get_http_request_by_id_request', ...args } as const;
|
||||
const { httpRequest } = await sendAndWaitForReply<GetHttpRequestByIdResponse>(payload);
|
||||
return httpRequest;
|
||||
},
|
||||
async send(args) {
|
||||
const payload = { type: 'send_http_request_request', ...args } as const;
|
||||
const { httpResponse } = await sendAndWaitForReply<SendHttpRequestResponse>(payload);
|
||||
return httpResponse;
|
||||
},
|
||||
async render(args) {
|
||||
const payload = { type: 'render_http_request_request', ...args } as const;
|
||||
const result = await sendAndWaitForReply<RenderHttpRequestResponse>(payload);
|
||||
return result.httpRequest;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Message comes into the plugin to be processed
|
||||
parentPort!.on('message', async ({ payload, id: replyId }: InternalEvent) => {
|
||||
try {
|
||||
if (payload.type === 'boot_request') {
|
||||
const payload: InternalEventPayload = {
|
||||
@@ -45,18 +151,18 @@ new Promise<void>(async (resolve, reject) => {
|
||||
version: pkg.version,
|
||||
capabilities,
|
||||
};
|
||||
sendToServer({ id: genId(), pluginRefId, replyId, payload });
|
||||
sendPayload(payload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'import_request' && typeof mod.pluginHookImport === 'function') {
|
||||
const reply: ImportResponse | null = await mod.pluginHookImport({}, payload.content);
|
||||
const reply: ImportResponse | null = await mod.pluginHookImport(ctx, payload.content);
|
||||
if (reply != null) {
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'import_response',
|
||||
resources: reply?.resources,
|
||||
};
|
||||
sendToServer({ id: genId(), pluginRefId, replyId, payload: replyPayload });
|
||||
sendPayload(replyPayload, replyId);
|
||||
return;
|
||||
} else {
|
||||
// Continue, to send back an empty reply
|
||||
@@ -67,47 +173,109 @@ new Promise<void>(async (resolve, reject) => {
|
||||
payload.type === 'export_http_request_request' &&
|
||||
typeof mod.pluginHookExport === 'function'
|
||||
) {
|
||||
const reply: string = await mod.pluginHookExport({}, payload.httpRequest);
|
||||
const reply: string = await mod.pluginHookExport(ctx, payload.httpRequest);
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'export_http_request_response',
|
||||
content: reply,
|
||||
};
|
||||
sendToServer({ id: genId(), pluginRefId, replyId, payload: replyPayload });
|
||||
sendPayload(replyPayload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.type === 'filter_request' && typeof mod.pluginHookResponseFilter === 'function') {
|
||||
const reply: string = await mod.pluginHookResponseFilter(
|
||||
{},
|
||||
{ filter: payload.filter, body: payload.content },
|
||||
);
|
||||
const reply: string = await mod.pluginHookResponseFilter(ctx, {
|
||||
filter: payload.filter,
|
||||
body: payload.content,
|
||||
});
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'filter_response',
|
||||
content: reply,
|
||||
};
|
||||
sendToServer({ id: genId(), pluginRefId, replyId, payload: replyPayload });
|
||||
sendPayload(replyPayload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'get_http_request_actions_request' &&
|
||||
Array.isArray(mod.plugin?.httpRequestActions)
|
||||
) {
|
||||
const reply: HttpRequestAction[] = mod.plugin.httpRequestActions.map(
|
||||
(a: HttpRequestActionPlugin) => ({
|
||||
...a,
|
||||
// Add everything except onSelect
|
||||
onSelect: undefined,
|
||||
}),
|
||||
);
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_http_request_actions_response',
|
||||
pluginRefId,
|
||||
actions: reply,
|
||||
};
|
||||
sendPayload(replyPayload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'get_template_functions_request' &&
|
||||
Array.isArray(mod.plugin?.templateFunctions)
|
||||
) {
|
||||
const reply: TemplateFunction[] = mod.plugin.templateFunctions.map(
|
||||
(a: TemplateFunctionPlugin) => ({
|
||||
...a,
|
||||
// Add everything except render
|
||||
onRender: undefined,
|
||||
}),
|
||||
);
|
||||
const replyPayload: InternalEventPayload = {
|
||||
type: 'get_template_functions_response',
|
||||
pluginRefId,
|
||||
functions: reply,
|
||||
};
|
||||
sendPayload(replyPayload, replyId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'call_http_request_action_request' &&
|
||||
Array.isArray(mod.plugin?.httpRequestActions)
|
||||
) {
|
||||
const action = mod.plugin.httpRequestActions.find((a) => a.key === payload.key);
|
||||
if (typeof action?.onSelect === 'function') {
|
||||
await action.onSelect(ctx, payload.args);
|
||||
sendEmpty(replyId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
payload.type === 'call_template_function_request' &&
|
||||
Array.isArray(mod.plugin?.templateFunctions)
|
||||
) {
|
||||
const action = mod.plugin.templateFunctions.find((a) => a.name === payload.name);
|
||||
if (typeof action?.onRender === 'function') {
|
||||
const result = await action.onRender(ctx, payload.args);
|
||||
sendPayload({ type: 'call_template_function_response', value: result ?? null }, replyId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.type === 'reload_request') {
|
||||
await reloadModule();
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Plugin call threw exception', payload.type, err);
|
||||
// TODO: Return errors to server
|
||||
}
|
||||
|
||||
// No matches, so send back an empty response so the caller doesn't block forever
|
||||
const id = genId();
|
||||
console.log('Sending nothing back to', id, { replyId });
|
||||
sendToServer({ id, pluginRefId, replyId, payload: { type: 'empty_response' } });
|
||||
sendEmpty(replyId);
|
||||
});
|
||||
}
|
||||
|
||||
resolve();
|
||||
}).catch((err) => {
|
||||
initialize().catch((err) => {
|
||||
console.log('failed to boot plugin', err);
|
||||
});
|
||||
|
||||
function sendToServer(e: InternalEvent) {
|
||||
parentPort!.postMessage(e);
|
||||
}
|
||||
|
||||
function genId(len = 5): string {
|
||||
const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let id = '';
|
||||
@@ -131,3 +299,23 @@ function prefixStdout(s: string) {
|
||||
return newText.trimEnd();
|
||||
});
|
||||
}
|
||||
|
||||
const watchedFiles: Record<string, Stats> = {};
|
||||
|
||||
/**
|
||||
* Watch a file and trigger callback on change.
|
||||
*
|
||||
* We also track the stat for each file because fs.watch will
|
||||
* trigger a "change" event when the access date changes
|
||||
*/
|
||||
function watchFile(filepath: string, cb: (filepath: string) => void) {
|
||||
watch(filepath, (_event, _name) => {
|
||||
const stat = statSync(filepath);
|
||||
if (stat.mtimeMs !== watchedFiles[filepath]?.mtimeMs) {
|
||||
cb(filepath);
|
||||
} else {
|
||||
console.log('SKIPPING SAME FILE STAT', filepath, stat);
|
||||
}
|
||||
watchedFiles[filepath] = stat;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,29 +3,7 @@ syntax = "proto3";
|
||||
package yaak.plugins.runtime;
|
||||
|
||||
service PluginRuntime {
|
||||
rpc EventStream (stream EventStreamEvent) returns (stream EventStreamEvent);}
|
||||
|
||||
message PluginInfo {
|
||||
string plugin = 1;
|
||||
}
|
||||
|
||||
message HookResponse {
|
||||
PluginInfo info = 1;
|
||||
string data = 2;
|
||||
}
|
||||
|
||||
message HookImportRequest {
|
||||
string data = 1;
|
||||
}
|
||||
|
||||
message HookResponseFilterRequest {
|
||||
string filter = 1;
|
||||
string body = 2;
|
||||
string contentType = 3;
|
||||
}
|
||||
|
||||
message HookExportRequest {
|
||||
string request = 1;
|
||||
rpc EventStream (stream EventStreamEvent) returns (stream EventStreamEvent);
|
||||
}
|
||||
|
||||
message EventStreamEvent {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
const {readdirSync, cpSync} = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const {execSync} = require("node:child_process");
|
||||
const { readdirSync, cpSync } = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execSync } = require('node:child_process');
|
||||
const pluginsDir = process.env.YAAK_PLUGINS_DIR;
|
||||
if (!pluginsDir) {
|
||||
console.log("YAAK_PLUGINS_DIR is not set");
|
||||
console.log('YAAK_PLUGINS_DIR is not set');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Installing Yaak plugins dependencies', pluginsDir);
|
||||
execSync('npm ci', {cwd: pluginsDir});
|
||||
execSync('npm ci', { cwd: pluginsDir });
|
||||
console.log('Building Yaak plugins', pluginsDir);
|
||||
execSync('npm run build', {cwd: pluginsDir});
|
||||
execSync('npm run build', { cwd: pluginsDir });
|
||||
|
||||
console.log('Copying Yaak plugins to', pluginsDir);
|
||||
|
||||
|
||||
498
src-tauri/Cargo.lock
generated
498
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
[workspace]
|
||||
members = ["grpc", "templates", "yaak_plugin_runtime", "yaak_models"]
|
||||
|
||||
members = ["yaak_grpc", "yaak_templates", "yaak_plugin_runtime", "yaak_models"]
|
||||
|
||||
[package]
|
||||
name = "yaak-app"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
authors = ["Gregory Schier"]
|
||||
|
||||
# Produce a library for mobile support
|
||||
[lib]
|
||||
@@ -20,15 +20,16 @@ tauri-build = { version = "2.0.0-rc.0", features = [] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2.7"
|
||||
cocoa = "0.25.0"
|
||||
cocoa = "0.26.0"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
|
||||
|
||||
[dependencies]
|
||||
grpc = { path = "./grpc" }
|
||||
templates = { path = "./templates" }
|
||||
yaak_plugin_runtime = { path = "yaak_plugin_runtime" }
|
||||
yaak_grpc = { path = "yaak_grpc" }
|
||||
yaak_templates = { path = "yaak_templates" }
|
||||
yaak_plugin_runtime = { workspace = true }
|
||||
yaak_models = { workspace = true }
|
||||
anyhow = "1.0.86"
|
||||
base64 = "0.22.0"
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
@@ -45,21 +46,22 @@ serde_json = { version = "1.0.116", features = ["raw_value"] }
|
||||
serde_yaml = "0.9.34"
|
||||
tauri = { workspace = true }
|
||||
tauri-plugin-shell = { workspace = true }
|
||||
tauri-plugin-clipboard-manager = "2.0.0-rc.0"
|
||||
tauri-plugin-dialog = "2.0.0-rc.0"
|
||||
tauri-plugin-fs = "2.0.0-rc.0"
|
||||
tauri-plugin-log = { version = "2.0.0-rc.0", features = ["colored"] }
|
||||
tauri-plugin-os = "2.0.0-rc.0"
|
||||
tauri-plugin-updater = "2.0.0-rc.0"
|
||||
tauri-plugin-window-state = "2.0.0-rc.0"
|
||||
tauri-plugin-clipboard-manager = "2.1.0-beta.7"
|
||||
tauri-plugin-dialog = "2.0.0-rc.7"
|
||||
tauri-plugin-fs = "2.0.0-rc.5"
|
||||
tauri-plugin-log = { version = "2.0.0-rc.2", features = ["colored"] }
|
||||
tauri-plugin-os = "2.0.0-rc.1"
|
||||
tauri-plugin-updater = "2.0.0-rc.3"
|
||||
tauri-plugin-window-state = "2.0.0-rc.4"
|
||||
tokio = { version = "1.36.0", features = ["sync"] }
|
||||
tokio-stream = "0.1.15"
|
||||
yaak_models = {workspace = true}
|
||||
uuid = "1.7.0"
|
||||
thiserror = "1.0.61"
|
||||
mime_guess = "2.0.5"
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
[workspace.dependencies]
|
||||
yaak_models = { path = "yaak_models" }
|
||||
tauri = { version = "2.0.0-rc.2", features = ["devtools", "protocol-asset"] }
|
||||
tauri-plugin-shell = "2.0.0-rc.0"
|
||||
yaak_plugin_runtime = { path = "yaak_plugin_runtime" }
|
||||
tauri-plugin-shell = "2.0.0-rc.3"
|
||||
tauri = { version = "2.0.0-rc.12", features = ["devtools", "protocol-asset"] }
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"core:event:allow-listen",
|
||||
"core:event:allow-unlisten",
|
||||
"os:allow-os-type",
|
||||
"clipboard-manager:allow-clear",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"clipboard-manager:allow-read-text",
|
||||
"dialog:allow-open",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-is-fullscreen","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-toggle-maximize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-start-dragging","core:window:allow-unmaximize","core:window:allow-theme","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
|
||||
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-is-fullscreen","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-toggle-maximize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-start-dragging","core:window:allow-unmaximize","core:window:allow-theme","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2
src-tauri/migrations/20240814013812_fix-env-model.sql
Normal file
2
src-tauri/migrations/20240814013812_fix-env-model.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE environments DROP COLUMN model;
|
||||
ALTER TABLE environments ADD COLUMN model TEXT DEFAULT 'environment';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE settings ADD COLUMN telemetry BOOLEAN DEFAULT TRUE;
|
||||
12
src-tauri/migrations/20240829131004_plugins.sql
Normal file
12
src-tauri/migrations/20240829131004_plugins.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE plugins
|
||||
(
|
||||
id TEXT NOT NULL
|
||||
PRIMARY KEY,
|
||||
model TEXT DEFAULT 'plugin' NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
checked_at DATETIME NULL,
|
||||
enabled BOOLEAN NOT NULL,
|
||||
directory TEXT NULL NOT NULL,
|
||||
url TEXT NULL
|
||||
);
|
||||
@@ -3,9 +3,9 @@ use std::fmt::Display;
|
||||
use log::{debug, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri::{Manager, Runtime, WebviewWindow};
|
||||
|
||||
use yaak_models::queries::{generate_id, get_key_value_int, get_key_value_string, set_key_value_int, set_key_value_string};
|
||||
use yaak_models::queries::{generate_id, get_key_value_int, get_key_value_string, get_or_create_settings, set_key_value_int, set_key_value_string};
|
||||
|
||||
use crate::is_dev;
|
||||
|
||||
@@ -28,6 +28,7 @@ pub enum AnalyticsResource {
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
KeyValue,
|
||||
Plugin,
|
||||
Setting,
|
||||
Sidebar,
|
||||
Theme,
|
||||
@@ -36,7 +37,7 @@ pub enum AnalyticsResource {
|
||||
|
||||
impl AnalyticsResource {
|
||||
pub fn from_str(s: &str) -> serde_json::Result<AnalyticsResource> {
|
||||
return serde_json::from_str(format!("\"{s}\"").as_str());
|
||||
serde_json::from_str(format!("\"{s}\"").as_str())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +75,7 @@ pub enum AnalyticsAction {
|
||||
|
||||
impl AnalyticsAction {
|
||||
pub fn from_str(s: &str) -> serde_json::Result<AnalyticsAction> {
|
||||
return serde_json::from_str(format!("\"{s}\"").as_str());
|
||||
serde_json::from_str(format!("\"{s}\"").as_str())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,19 +97,18 @@ pub struct LaunchEventInfo {
|
||||
pub num_launches: i32,
|
||||
}
|
||||
|
||||
pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo {
|
||||
pub async fn track_launch_event<R: Runtime>(w: &WebviewWindow<R>) -> LaunchEventInfo {
|
||||
let last_tracked_version_key = "last_tracked_version";
|
||||
|
||||
let mut info = LaunchEventInfo::default();
|
||||
|
||||
info.num_launches = get_num_launches(app).await + 1;
|
||||
info.previous_version =
|
||||
get_key_value_string(app, NAMESPACE, last_tracked_version_key, "").await;
|
||||
info.current_version = app.package_info().version.to_string();
|
||||
info.num_launches = get_num_launches(w).await + 1;
|
||||
info.previous_version = get_key_value_string(w, NAMESPACE, last_tracked_version_key, "").await;
|
||||
info.current_version = w.package_info().version.to_string();
|
||||
|
||||
if info.previous_version.is_empty() {
|
||||
track_event(
|
||||
app,
|
||||
w,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::LaunchFirst,
|
||||
None,
|
||||
@@ -118,7 +118,7 @@ pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo {
|
||||
info.launched_after_update = info.current_version != info.previous_version;
|
||||
if info.launched_after_update {
|
||||
track_event(
|
||||
app,
|
||||
w,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::LaunchUpdate,
|
||||
Some(json!({ NUM_LAUNCHES_KEY: info.num_launches })),
|
||||
@@ -129,7 +129,7 @@ pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo {
|
||||
|
||||
// Track a launch event in all cases
|
||||
track_event(
|
||||
app,
|
||||
w,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Launch,
|
||||
Some(json!({ NUM_LAUNCHES_KEY: info.num_launches })),
|
||||
@@ -139,27 +139,28 @@ pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo {
|
||||
// Update key values
|
||||
|
||||
set_key_value_string(
|
||||
app,
|
||||
w,
|
||||
NAMESPACE,
|
||||
last_tracked_version_key,
|
||||
info.current_version.as_str(),
|
||||
)
|
||||
.await;
|
||||
set_key_value_int(app, NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches).await;
|
||||
set_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches).await;
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
pub async fn track_event(
|
||||
app_handle: &AppHandle,
|
||||
pub async fn track_event<R: Runtime>(
|
||||
w: &WebviewWindow<R>,
|
||||
resource: AnalyticsResource,
|
||||
action: AnalyticsAction,
|
||||
attributes: Option<Value>,
|
||||
) {
|
||||
let id = get_id(app_handle).await;
|
||||
|
||||
let id = get_id(w).await;
|
||||
let event = format!("{}.{}", resource, action);
|
||||
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
|
||||
let info = app_handle.package_info();
|
||||
let info = w.app_handle().package_info();
|
||||
let tz = datetime::sys_timezone().unwrap_or("unknown".to_string());
|
||||
let site = match is_dev() {
|
||||
true => "site_TkHWjoXwZPq3HfhERb",
|
||||
@@ -177,7 +178,7 @@ pub async fn track_event(
|
||||
("v", info.version.clone().to_string()),
|
||||
("os", get_os().to_string()),
|
||||
("tz", tz),
|
||||
("xy", get_window_size(app_handle)),
|
||||
("xy", get_window_size(w)),
|
||||
];
|
||||
let req = reqwest::Client::builder()
|
||||
.build()
|
||||
@@ -185,9 +186,15 @@ pub async fn track_event(
|
||||
.get(format!("{base_url}/t/e"))
|
||||
.query(¶ms);
|
||||
|
||||
let settings = get_or_create_settings(w).await;
|
||||
if !settings.telemetry {
|
||||
info!("Track event (disabled): {}", event);
|
||||
return
|
||||
}
|
||||
|
||||
// Disable analytics actual sending in dev
|
||||
if is_dev() {
|
||||
debug!("track: {} {}", event, attributes_json);
|
||||
debug!("Track event: {} {}", event, attributes_json);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -208,13 +215,8 @@ fn get_os() -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_window_size(app_handle: &AppHandle) -> String {
|
||||
let window = match app_handle.webview_windows().into_values().next() {
|
||||
Some(w) => w,
|
||||
None => return "unknown".to_string(),
|
||||
};
|
||||
|
||||
let current_monitor = match window.current_monitor() {
|
||||
fn get_window_size<R: Runtime>(w: &WebviewWindow<R>) -> String {
|
||||
let current_monitor = match w.current_monitor() {
|
||||
Ok(Some(m)) => m,
|
||||
_ => return "unknown".to_string(),
|
||||
};
|
||||
@@ -231,17 +233,17 @@ fn get_window_size(app_handle: &AppHandle) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_id(app_handle: &AppHandle) -> String {
|
||||
let id = get_key_value_string(app_handle, "analytics", "id", "").await;
|
||||
async fn get_id<R: Runtime>(w: &WebviewWindow<R>) -> String {
|
||||
let id = get_key_value_string(w, "analytics", "id", "").await;
|
||||
if id.is_empty() {
|
||||
let new_id = generate_id();
|
||||
set_key_value_string(app_handle, "analytics", "id", new_id.as_str()).await;
|
||||
set_key_value_string(w, "analytics", "id", new_id.as_str()).await;
|
||||
new_id
|
||||
} else {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_num_launches(app: &AppHandle) -> i32 {
|
||||
get_key_value_int(app, NAMESPACE, NUM_LAUNCHES_KEY, 0).await
|
||||
pub async fn get_num_launches<R: Runtime>(w: &WebviewWindow<R>) -> i32 {
|
||||
get_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, 0).await
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::collections::HashMap;
|
||||
|
||||
use KeyAndValueRef::{Ascii, Binary};
|
||||
|
||||
use grpc::{KeyAndValueRef, MetadataMap};
|
||||
use yaak_grpc::{KeyAndValueRef, MetadataMap};
|
||||
|
||||
pub fn metadata_to_map(metadata: MetadataMap) -> HashMap<String, String> {
|
||||
let mut entries = HashMap::new();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::Write;
|
||||
@@ -6,38 +7,43 @@ use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::render::variables_from_environment;
|
||||
use crate::{render, response_err};
|
||||
use crate::render::render_http_request;
|
||||
use crate::response_err;
|
||||
use crate::template_callback::PluginTemplateCallback;
|
||||
use base64::Engine;
|
||||
use http::header::{ACCEPT, USER_AGENT};
|
||||
use http::{HeaderMap, HeaderName, HeaderValue};
|
||||
use log::{error, info, warn};
|
||||
use log::{error, warn};
|
||||
use mime_guess::Mime;
|
||||
use reqwest::redirect::Policy;
|
||||
use reqwest::Method;
|
||||
use reqwest::{multipart, Url};
|
||||
use tauri::{Manager, WebviewWindow};
|
||||
use serde_json::Value;
|
||||
use tauri::{Manager, Runtime, WebviewWindow};
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::watch::Receiver;
|
||||
use yaak_models::models::{Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader};
|
||||
use yaak_models::models::{
|
||||
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader, HttpUrlParameter,
|
||||
};
|
||||
use yaak_models::queries::{get_workspace, update_response_if_id, upsert_cookie_jar};
|
||||
|
||||
pub async fn send_http_request(
|
||||
window: &WebviewWindow,
|
||||
request: HttpRequest,
|
||||
pub async fn send_http_request<R: Runtime>(
|
||||
window: &WebviewWindow<R>,
|
||||
request: &HttpRequest,
|
||||
response: &HttpResponse,
|
||||
environment: Option<Environment>,
|
||||
cookie_jar: Option<CookieJar>,
|
||||
download_path: Option<PathBuf>,
|
||||
cancel_rx: &mut Receiver<bool>,
|
||||
) -> Result<HttpResponse, String> {
|
||||
let environment_ref = environment.as_ref();
|
||||
let workspace = get_workspace(window, &request.workspace_id)
|
||||
.await
|
||||
.expect("Failed to get Workspace");
|
||||
let vars = variables_from_environment(&workspace, environment_ref);
|
||||
let cb = &*window.app_handle().state::<PluginTemplateCallback>();
|
||||
let cb = cb.for_send();
|
||||
let rendered_request =
|
||||
render_http_request(&request, &workspace, environment.as_ref(), &cb).await;
|
||||
|
||||
let mut url_string = render::render(&request.url, &vars);
|
||||
let mut url_string = rendered_request.url;
|
||||
|
||||
url_string = ensure_proto(&url_string);
|
||||
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
|
||||
@@ -90,6 +96,23 @@ pub async fn send_http_request(
|
||||
|
||||
let client = client_builder.build().expect("Failed to build client");
|
||||
|
||||
// Render query parameters
|
||||
let mut query_params = Vec::new();
|
||||
for p in rendered_request.url_parameters {
|
||||
if !p.enabled || p.name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Replace path parameters with values from URL parameters
|
||||
let old_url_string = url_string.clone();
|
||||
url_string = replace_path_placeholder(&p, url_string.as_str());
|
||||
|
||||
// Treat as regular param if wasn't used as path param
|
||||
if old_url_string == url_string {
|
||||
query_params.push((p.name, p.value));
|
||||
}
|
||||
}
|
||||
|
||||
let uri = match http::Uri::from_str(url_string.as_str()) {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
@@ -114,9 +137,9 @@ pub async fn send_http_request(
|
||||
}
|
||||
};
|
||||
|
||||
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
|
||||
let m = Method::from_bytes(rendered_request.method.to_uppercase().as_bytes())
|
||||
.expect("Failed to create method");
|
||||
let mut request_builder = client.request(m, url);
|
||||
let mut request_builder = client.request(m, url).query(&query_params);
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(USER_AGENT, HeaderValue::from_static("yaak"));
|
||||
@@ -137,7 +160,7 @@ pub async fn send_http_request(
|
||||
// );
|
||||
// }
|
||||
|
||||
for h in request.headers {
|
||||
for h in rendered_request.headers {
|
||||
if h.name.is_empty() && h.value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -146,17 +169,14 @@ pub async fn send_http_request(
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = render::render(&h.name, &vars);
|
||||
let value = render::render(&h.value, &vars);
|
||||
|
||||
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
|
||||
let header_name = match HeaderName::from_bytes(h.name.as_bytes()) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
error!("Failed to create header name: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let header_value = match HeaderValue::from_str(value.as_str()) {
|
||||
let header_value = match HeaderValue::from_str(h.value.as_str()) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
error!("Failed to create header value: {}", e);
|
||||
@@ -167,33 +187,34 @@ pub async fn send_http_request(
|
||||
headers.insert(header_name, header_value);
|
||||
}
|
||||
|
||||
if let Some(b) = &request.authentication_type {
|
||||
if let Some(b) = &rendered_request.authentication_type {
|
||||
let empty_value = &serde_json::to_value("").unwrap();
|
||||
let a = request.authentication;
|
||||
let a = rendered_request.authentication;
|
||||
|
||||
if b == "basic" {
|
||||
let raw_username = a
|
||||
let username = a
|
||||
.get("username")
|
||||
.unwrap_or(empty_value)
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
let raw_password = a
|
||||
.unwrap_or_default();
|
||||
let password = a
|
||||
.get("password")
|
||||
.unwrap_or(empty_value)
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
let username = render::render(raw_username, &vars);
|
||||
let password = render::render(raw_password, &vars);
|
||||
.unwrap_or_default();
|
||||
|
||||
let auth = format!("{username}:{password}");
|
||||
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(auth);
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&format!("Basic {}", encoded)).unwrap(),
|
||||
);
|
||||
} else if b == "bearer" {
|
||||
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
|
||||
let token = render::render(raw_token, &vars);
|
||||
let token = a
|
||||
.get("token")
|
||||
.unwrap_or(empty_value)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
|
||||
@@ -201,57 +222,30 @@ pub async fn send_http_request(
|
||||
}
|
||||
}
|
||||
|
||||
let mut query_params = Vec::new();
|
||||
for p in request.url_parameters {
|
||||
if !p.enabled || p.name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
query_params.push((
|
||||
render::render(&p.name, &vars),
|
||||
render::render(&p.value, &vars),
|
||||
));
|
||||
}
|
||||
request_builder = request_builder.query(&query_params);
|
||||
|
||||
if let Some(body_type) = &request.body_type {
|
||||
let empty_string = &serde_json::to_value("").unwrap();
|
||||
let empty_bool = &serde_json::to_value(false).unwrap();
|
||||
let request_body = request.body;
|
||||
|
||||
let request_body = rendered_request.body;
|
||||
if let Some(body_type) = &rendered_request.body_type {
|
||||
if request_body.contains_key("text") {
|
||||
let raw_text = request_body
|
||||
.get("text")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
let body = render::render(raw_text, &vars);
|
||||
request_builder = request_builder.body(body);
|
||||
let body = get_str_h(&request_body, "text");
|
||||
request_builder = request_builder.body(body.to_owned());
|
||||
} else if body_type == "application/x-www-form-urlencoded"
|
||||
&& request_body.contains_key("form")
|
||||
{
|
||||
let mut form_params = Vec::new();
|
||||
let form = request_body.get("form");
|
||||
if let Some(f) = form {
|
||||
for p in f.as_array().unwrap_or(&Vec::new()) {
|
||||
let enabled = p
|
||||
.get("enabled")
|
||||
.unwrap_or(empty_bool)
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
let name = p
|
||||
.get("name")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
if !enabled || name.is_empty() {
|
||||
continue;
|
||||
match f.as_array() {
|
||||
None => {}
|
||||
Some(a) => {
|
||||
for p in a {
|
||||
let enabled = get_bool(p, "enabled");
|
||||
let name = get_str(p, "name");
|
||||
if !enabled || name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let value = get_str(p, "value");
|
||||
form_params.push((name, value));
|
||||
}
|
||||
}
|
||||
let value = p
|
||||
.get("value")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
form_params.push((render::render(name, &vars), render::render(value, &vars)));
|
||||
}
|
||||
}
|
||||
request_builder = request_builder.form(&form_params);
|
||||
@@ -273,77 +267,59 @@ pub async fn send_http_request(
|
||||
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
|
||||
let mut multipart_form = multipart::Form::new();
|
||||
if let Some(form_definition) = request_body.get("form") {
|
||||
for p in form_definition.as_array().unwrap_or(&Vec::new()) {
|
||||
let enabled = p
|
||||
.get("enabled")
|
||||
.unwrap_or(empty_bool)
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
let name_raw = p
|
||||
.get("name")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
match form_definition.as_array() {
|
||||
None => {}
|
||||
Some(fd) => {
|
||||
for p in fd {
|
||||
let enabled = get_bool(p, "enabled");
|
||||
let name = get_str(p, "name").to_string();
|
||||
|
||||
if !enabled || name_raw.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_path = p
|
||||
.get("file")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
let value_raw = p
|
||||
.get("value")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
|
||||
let name = render::render(name_raw, &vars);
|
||||
let mut part = if file_path.is_empty() {
|
||||
multipart::Part::text(render::render(value_raw, &vars))
|
||||
} else {
|
||||
match fs::read(file_path) {
|
||||
Ok(f) => multipart::Part::bytes(f),
|
||||
Err(e) => {
|
||||
return response_err(response, e.to_string(), window).await;
|
||||
if !enabled || name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_path = get_str(p, "file").to_owned();
|
||||
let value = get_str(p, "value").to_owned();
|
||||
|
||||
let mut part = if file_path.is_empty() {
|
||||
multipart::Part::text(value.clone())
|
||||
} else {
|
||||
match fs::read(file_path.clone()) {
|
||||
Ok(f) => multipart::Part::bytes(f),
|
||||
Err(e) => {
|
||||
return response_err(response, e.to_string(), window).await;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let content_type = get_str(p, "contentType");
|
||||
|
||||
// Set or guess mimetype
|
||||
if !content_type.is_empty() {
|
||||
part = part.mime_str(content_type).map_err(|e| e.to_string())?;
|
||||
} else if !file_path.is_empty() {
|
||||
let default_mime =
|
||||
Mime::from_str("application/octet-stream").unwrap();
|
||||
let mime =
|
||||
mime_guess::from_path(file_path.clone()).first_or(default_mime);
|
||||
part = part
|
||||
.mime_str(mime.essence_str())
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
// Set file path if not empty
|
||||
if !file_path.is_empty() {
|
||||
let filename = PathBuf::from(file_path)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
part = part.file_name(filename);
|
||||
}
|
||||
|
||||
multipart_form = multipart_form.part(name, part);
|
||||
}
|
||||
};
|
||||
|
||||
let ct_raw = p
|
||||
.get("contentType")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Set or guess mimetype
|
||||
if !ct_raw.is_empty() {
|
||||
let content_type = render::render(ct_raw, &vars);
|
||||
part = part
|
||||
.mime_str(content_type.as_str())
|
||||
.map_err(|e| e.to_string())?;
|
||||
} else if !file_path.is_empty() {
|
||||
let default_mime = Mime::from_str("application/octet-stream").unwrap();
|
||||
let mime = mime_guess::from_path(file_path).first_or(default_mime);
|
||||
part = part
|
||||
.mime_str(mime.essence_str())
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
// Set fil path if not empty
|
||||
if !file_path.is_empty() {
|
||||
let filename = PathBuf::from(file_path)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
part = part.file_name(filename);
|
||||
}
|
||||
|
||||
multipart_form = multipart_form.part(name, part);
|
||||
}
|
||||
}
|
||||
headers.remove("Content-Type"); // reqwest will add this automatically
|
||||
@@ -442,16 +418,6 @@ pub async fn send_http_request(
|
||||
.await
|
||||
.expect("Failed to update response");
|
||||
|
||||
// Copy response to the download path, if specified
|
||||
match (download_path, response.body_path.clone()) {
|
||||
(Some(dl_path), Some(body_path)) => {
|
||||
info!("Downloading response body to {}", dl_path.display());
|
||||
fs::copy(body_path, dl_path)
|
||||
.expect("Failed to copy file for response download");
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
// Add cookie store if specified
|
||||
if let Some((cookie_store, mut cookie_jar)) = maybe_cookie_manager {
|
||||
// let cookies = response_headers.get_all(SET_COOKIE).iter().map(|h| {
|
||||
@@ -466,7 +432,8 @@ pub async fn send_http_request(
|
||||
.unwrap()
|
||||
.iter_any()
|
||||
.map(|c| {
|
||||
let json_cookie = serde_json::to_value(&c).expect("Failed to serialize cookie");
|
||||
let json_cookie =
|
||||
serde_json::to_value(&c).expect("Failed to serialize cookie");
|
||||
serde_json::from_value(json_cookie).expect("Failed to deserialize cookie")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -504,3 +471,140 @@ fn ensure_proto(url_str: &str) -> String {
|
||||
|
||||
format!("http://{url_str}")
|
||||
}
|
||||
|
||||
fn get_bool(v: &Value, key: &str) -> bool {
|
||||
match v.get(key) {
|
||||
None => false,
|
||||
Some(v) => v.as_bool().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_str<'a>(v: &'a Value, key: &str) -> &'a str {
|
||||
match v.get(key) {
|
||||
None => "",
|
||||
Some(v) => v.as_str().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_str_h<'a>(v: &'a HashMap<String, Value>, key: &str) -> &'a str {
|
||||
match v.get(key) {
|
||||
None => "",
|
||||
Some(v) => v.as_str().unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_path_placeholder(p: &HttpUrlParameter, url: &str) -> String {
|
||||
if !p.enabled {
|
||||
return url.to_string();
|
||||
}
|
||||
|
||||
let re = regex::Regex::new(format!("(/){}([/?#]|$)", p.name).as_str()).unwrap();
|
||||
let result = re
|
||||
.replace_all(url, |cap: ®ex::Captures| {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
cap[1].to_string(),
|
||||
urlencoding::encode(p.value.as_str()),
|
||||
cap[2].to_string()
|
||||
)
|
||||
})
|
||||
.into_owned();
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::http_request::replace_path_placeholder;
|
||||
use yaak_models::models::HttpUrlParameter;
|
||||
|
||||
#[test]
|
||||
fn placeholder_middle() {
|
||||
let p = HttpUrlParameter {
|
||||
name: ":foo".into(),
|
||||
value: "xxx".into(),
|
||||
enabled: true,
|
||||
};
|
||||
assert_eq!(
|
||||
replace_path_placeholder(&p, "https://example.com/:foo/bar"),
|
||||
"https://example.com/xxx/bar",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_end() {
|
||||
let p = HttpUrlParameter {
|
||||
name: ":foo".into(),
|
||||
value: "xxx".into(),
|
||||
enabled: true,
|
||||
};
|
||||
assert_eq!(
|
||||
replace_path_placeholder(&p, "https://example.com/:foo"),
|
||||
"https://example.com/xxx",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_query() {
|
||||
let p = HttpUrlParameter {
|
||||
name: ":foo".into(),
|
||||
value: "xxx".into(),
|
||||
enabled: true,
|
||||
};
|
||||
assert_eq!(
|
||||
replace_path_placeholder(&p, "https://example.com/:foo?:foo"),
|
||||
"https://example.com/xxx?:foo",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_missing() {
|
||||
let p = HttpUrlParameter {
|
||||
enabled: true,
|
||||
name: "".to_string(),
|
||||
value: "".to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
replace_path_placeholder(&p, "https://example.com/:missing"),
|
||||
"https://example.com/:missing",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_disabled() {
|
||||
let p = HttpUrlParameter {
|
||||
enabled: false,
|
||||
name: ":foo".to_string(),
|
||||
value: "xxx".to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
replace_path_placeholder(&p, "https://example.com/:foo"),
|
||||
"https://example.com/:foo",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_prefix() {
|
||||
let p = HttpUrlParameter {
|
||||
name: ":foo".into(),
|
||||
value: "xxx".into(),
|
||||
enabled: true,
|
||||
};
|
||||
assert_eq!(
|
||||
replace_path_placeholder(&p, "https://example.com/:foooo"),
|
||||
"https://example.com/:foooo",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_encode() {
|
||||
let p = HttpUrlParameter {
|
||||
name: ":foo".into(),
|
||||
value: "Hello World".into(),
|
||||
enabled: true,
|
||||
};
|
||||
assert_eq!(
|
||||
replace_path_placeholder(&p, "https://example.com/:foo"),
|
||||
"https://example.com/Hello%20World",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
extern crate core;
|
||||
#[cfg(target_os = "macos")]
|
||||
#[macro_use]
|
||||
extern crate objc;
|
||||
|
||||
use std::collections::HashMap;
|
||||
@@ -12,21 +11,23 @@ use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use base64::Engine;
|
||||
use chrono::Utc;
|
||||
use fern::colors::ColoredLevelConfig;
|
||||
use log::{debug, error, info, warn};
|
||||
use rand::random;
|
||||
use serde_json::{json, Value};
|
||||
use tauri::Listener;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
use tauri::{AppHandle, Emitter, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow};
|
||||
use tauri::{Listener, Runtime};
|
||||
use tauri::{Manager, WindowEvent};
|
||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||
use tauri_plugin_log::{fern, Target, TargetKind};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use ::grpc::manager::{DynamicMessage, GrpcHandle};
|
||||
use ::grpc::{deserialize_message, serialize_message, Code, ServiceDefinition};
|
||||
use yaak_grpc::manager::{DynamicMessage, GrpcHandle};
|
||||
use yaak_grpc::{deserialize_message, serialize_message, Code, ServiceDefinition};
|
||||
use yaak_plugin_runtime::manager::PluginManager;
|
||||
|
||||
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
||||
@@ -34,27 +35,35 @@ use crate::export_resources::{get_workspace_export_resources, WorkspaceExportRes
|
||||
use crate::grpc::metadata_to_map;
|
||||
use crate::http_request::send_http_request;
|
||||
use crate::notifications::YaakNotifier;
|
||||
use crate::render::{render_request, variables_from_environment};
|
||||
use crate::render::{render_grpc_request, render_http_request, render_template};
|
||||
use crate::template_callback::PluginTemplateCallback;
|
||||
use crate::updates::{UpdateMode, YaakUpdater};
|
||||
use crate::window_menu::app_menu;
|
||||
use yaak_models::models::{
|
||||
CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType,
|
||||
GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Settings, Workspace,
|
||||
GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Plugin, Settings, Workspace,
|
||||
};
|
||||
use yaak_models::queries::{
|
||||
cancel_pending_grpc_connections, cancel_pending_responses, create_http_response,
|
||||
cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response,
|
||||
delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment,
|
||||
delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request,
|
||||
delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request,
|
||||
generate_model_id, get_cookie_jar, get_environment, get_folder, get_grpc_connection,
|
||||
get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
|
||||
get_or_create_settings, get_workspace, list_cookie_jars, list_environments, list_folders,
|
||||
list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests,
|
||||
list_responses, list_workspaces, set_key_value_raw, update_response_if_id, update_settings,
|
||||
upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
|
||||
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace,
|
||||
get_or_create_settings, get_plugin, get_workspace, list_cookie_jars, list_environments,
|
||||
list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests,
|
||||
list_http_responses, list_plugins, list_workspaces, set_key_value_raw, update_response_if_id,
|
||||
update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
|
||||
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
|
||||
};
|
||||
use yaak_plugin_runtime::events::FilterResponse;
|
||||
use yaak_plugin_runtime::events::{
|
||||
CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
|
||||
GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse,
|
||||
InternalEvent, InternalEventPayload, PluginBootResponse, RenderHttpRequestResponse,
|
||||
SendHttpRequestResponse, ShowToastRequest, ToastVariant,
|
||||
};
|
||||
use yaak_plugin_runtime::handle::PluginHandle;
|
||||
use yaak_templates::{Parser, Tokens};
|
||||
|
||||
mod analytics;
|
||||
mod export_resources;
|
||||
@@ -64,13 +73,16 @@ mod notifications;
|
||||
mod render;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod tauri_plugin_mac_window;
|
||||
mod template_fns;
|
||||
mod template_callback;
|
||||
mod updates;
|
||||
mod window_menu;
|
||||
|
||||
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
|
||||
const DEFAULT_WINDOW_HEIGHT: f64 = 600.0;
|
||||
|
||||
const MIN_WINDOW_WIDTH: f64 = 300.0;
|
||||
const MIN_WINDOW_HEIGHT: f64 = 300.0;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
struct AppMetaData {
|
||||
@@ -94,13 +106,55 @@ async fn cmd_metadata(app_handle: AppHandle) -> Result<AppMetaData, ()> {
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_parse_template(template: &str) -> Result<Tokens, String> {
|
||||
Ok(Parser::new(template).parse())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_template_tokens_to_string(tokens: Tokens) -> Result<String, String> {
|
||||
Ok(tokens.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_render_template(
|
||||
window: WebviewWindow,
|
||||
template: &str,
|
||||
workspace_id: &str,
|
||||
environment_id: Option<&str>,
|
||||
) -> Result<String, String> {
|
||||
let environment = match environment_id {
|
||||
Some(id) => Some(
|
||||
get_environment(&window, id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
let workspace = get_workspace(&window, &workspace_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rendered = render_template(
|
||||
window.app_handle(),
|
||||
template,
|
||||
&workspace,
|
||||
environment.as_ref(),
|
||||
)
|
||||
.await;
|
||||
Ok(rendered)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_dismiss_notification(
|
||||
app: AppHandle,
|
||||
window: WebviewWindow,
|
||||
notification_id: &str,
|
||||
yaak_notifier: State<'_, Mutex<YaakNotifier>>,
|
||||
) -> Result<(), String> {
|
||||
yaak_notifier.lock().await.seen(&app, notification_id).await
|
||||
yaak_notifier
|
||||
.lock()
|
||||
.await
|
||||
.seen(&window, notification_id)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -135,23 +189,28 @@ async fn cmd_grpc_go(
|
||||
request_id: &str,
|
||||
environment_id: Option<&str>,
|
||||
proto_files: Vec<String>,
|
||||
w: WebviewWindow,
|
||||
window: WebviewWindow,
|
||||
grpc_handle: State<'_, Mutex<GrpcHandle>>,
|
||||
) -> Result<String, String> {
|
||||
let req = get_grpc_request(&w, request_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let environment = match environment_id {
|
||||
Some(id) => Some(get_environment(&w, id).await.map_err(|e| e.to_string())?),
|
||||
Some(id) => Some(
|
||||
get_environment(&window, id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
let workspace = get_workspace(&w, &req.workspace_id)
|
||||
let req = get_grpc_request(&window, request_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let workspace = get_workspace(&window, &req.workspace_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let req =
|
||||
render_grpc_request(window.app_handle(), &req, &workspace, environment.as_ref()).await;
|
||||
let mut metadata = HashMap::new();
|
||||
let vars = variables_from_environment(&workspace, environment.as_ref());
|
||||
|
||||
// Add rest of metadata
|
||||
// Add the rest of metadata
|
||||
for h in req.clone().metadata {
|
||||
if h.name.is_empty() && h.value.is_empty() {
|
||||
continue;
|
||||
@@ -161,10 +220,7 @@ async fn cmd_grpc_go(
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = render::render(&h.name, &vars);
|
||||
let value = render::render(&h.value, &vars);
|
||||
|
||||
metadata.insert(name, value);
|
||||
metadata.insert(h.name, h.value);
|
||||
}
|
||||
|
||||
if let Some(b) = &req.authentication_type {
|
||||
@@ -173,25 +229,22 @@ async fn cmd_grpc_go(
|
||||
let a = req.authentication;
|
||||
|
||||
if b == "basic" {
|
||||
let raw_username = a
|
||||
let username = a
|
||||
.get("username")
|
||||
.unwrap_or(empty_value)
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
let raw_password = a
|
||||
let password = a
|
||||
.get("password")
|
||||
.unwrap_or(empty_value)
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
let username = render::render(raw_username, &vars);
|
||||
let password = render::render(raw_password, &vars);
|
||||
|
||||
let auth = format!("{username}:{password}");
|
||||
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
|
||||
metadata.insert("Authorization".to_string(), format!("Basic {}", encoded));
|
||||
} else if b == "bearer" {
|
||||
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
|
||||
let token = render::render(raw_token, &vars);
|
||||
let token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
|
||||
metadata.insert("Authorization".to_string(), format!("Bearer {token}"));
|
||||
}
|
||||
}
|
||||
@@ -199,7 +252,7 @@ async fn cmd_grpc_go(
|
||||
let conn = {
|
||||
let req = req.clone();
|
||||
upsert_grpc_connection(
|
||||
&w,
|
||||
&window,
|
||||
&GrpcConnection {
|
||||
workspace_id: req.workspace_id,
|
||||
request_id: req.id,
|
||||
@@ -256,7 +309,7 @@ async fn cmd_grpc_go(
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
upsert_grpc_connection(
|
||||
&w,
|
||||
&window,
|
||||
&GrpcConnection {
|
||||
elapsed: start.elapsed().as_millis() as i32,
|
||||
error: Some(err.clone()),
|
||||
@@ -282,10 +335,9 @@ async fn cmd_grpc_go(
|
||||
|
||||
let cb = {
|
||||
let cancelled_rx = cancelled_rx.clone();
|
||||
let w = w.clone();
|
||||
let w = window.clone();
|
||||
let base_msg = base_msg.clone();
|
||||
let method_desc = method_desc.clone();
|
||||
let vars = vars.clone();
|
||||
|
||||
move |ev: tauri::Event| {
|
||||
if *cancelled_rx.borrow() {
|
||||
@@ -305,11 +357,10 @@ async fn cmd_grpc_go(
|
||||
};
|
||||
|
||||
match serde_json::from_str::<IncomingMsg>(ev.payload()) {
|
||||
Ok(IncomingMsg::Message(raw_msg)) => {
|
||||
Ok(IncomingMsg::Message(msg)) => {
|
||||
let w = w.clone();
|
||||
let base_msg = base_msg.clone();
|
||||
let method_desc = method_desc.clone();
|
||||
let msg = render::render(raw_msg.as_str(), &vars);
|
||||
let d_msg: DynamicMessage = match deserialize_message(msg.as_str(), method_desc)
|
||||
{
|
||||
Ok(d_msg) => d_msg,
|
||||
@@ -355,19 +406,17 @@ async fn cmd_grpc_go(
|
||||
}
|
||||
}
|
||||
};
|
||||
let event_handler = w.listen_any(format!("grpc_client_msg_{}", conn.id).as_str(), cb);
|
||||
let event_handler = window.listen_any(format!("grpc_client_msg_{}", conn.id).as_str(), cb);
|
||||
|
||||
let grpc_listen = {
|
||||
let w = w.clone();
|
||||
let w = window.clone();
|
||||
let base_event = base_msg.clone();
|
||||
let req = req.clone();
|
||||
let vars = vars.clone();
|
||||
let raw_msg = if req.message.is_empty() {
|
||||
let msg = if req.message.is_empty() {
|
||||
"{}".to_string()
|
||||
} else {
|
||||
req.message
|
||||
};
|
||||
let msg = render::render(&raw_msg, &vars);
|
||||
|
||||
upsert_grpc_event(
|
||||
&w,
|
||||
@@ -603,7 +652,7 @@ async fn cmd_grpc_go(
|
||||
{
|
||||
let conn_id = conn_id.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let w = w.clone();
|
||||
let w = window.clone();
|
||||
tokio::select! {
|
||||
_ = grpc_listen => {
|
||||
let events = list_grpc_events(&w, &conn_id)
|
||||
@@ -687,11 +736,10 @@ async fn cmd_send_ephemeral_request(
|
||||
|
||||
send_http_request(
|
||||
&window,
|
||||
request,
|
||||
&request,
|
||||
&response,
|
||||
environment,
|
||||
cookie_jar,
|
||||
None,
|
||||
&mut cancel_rx,
|
||||
)
|
||||
.await
|
||||
@@ -701,7 +749,7 @@ async fn cmd_send_ephemeral_request(
|
||||
async fn cmd_filter_response(
|
||||
w: WebviewWindow,
|
||||
response_id: &str,
|
||||
plugin_manager: State<'_, Mutex<PluginManager>>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
filter: &str,
|
||||
) -> Result<FilterResponse, String> {
|
||||
let response = get_http_response(&w, response_id)
|
||||
@@ -724,9 +772,7 @@ async fn cmd_filter_response(
|
||||
|
||||
// TODO: Have plugins register their own content type (regex?)
|
||||
plugin_manager
|
||||
.lock()
|
||||
.await
|
||||
.run_filter(filter, &body, &content_type)
|
||||
.filter_data(filter, &body, &content_type)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -734,16 +780,14 @@ async fn cmd_filter_response(
|
||||
#[tauri::command]
|
||||
async fn cmd_import_data(
|
||||
w: WebviewWindow,
|
||||
plugin_manager: State<'_, Mutex<PluginManager>>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
file_path: &str,
|
||||
) -> Result<WorkspaceExportResources, String> {
|
||||
let file =
|
||||
read_to_string(file_path).unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
|
||||
let file_contents = file.as_str();
|
||||
let (import_result, plugin_name) = plugin_manager
|
||||
.lock()
|
||||
.await
|
||||
.run_import(file_contents)
|
||||
.import_data(file_contents)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -803,16 +847,33 @@ async fn cmd_import_data(
|
||||
imported_resources.environments.len()
|
||||
);
|
||||
|
||||
for mut v in resources.folders {
|
||||
v.id = maybe_gen_id(v.id.as_str(), ModelType::TypeFolder, &mut id_map);
|
||||
v.workspace_id = maybe_gen_id(
|
||||
v.workspace_id.as_str(),
|
||||
ModelType::TypeWorkspace,
|
||||
&mut id_map,
|
||||
);
|
||||
v.folder_id = maybe_gen_id_opt(v.folder_id, ModelType::TypeFolder, &mut id_map);
|
||||
let x = upsert_folder(&w, v).await.map_err(|e| e.to_string())?;
|
||||
imported_resources.folders.push(x.clone());
|
||||
// Folders can foreign-key to themselves, so we need to import from
|
||||
// the top of the tree to the bottom to avoid foreign key conflicts.
|
||||
// We do this by looping until we've imported them all, only importing if:
|
||||
// - The parent folder has been imported
|
||||
// - The folder hasn't already been imported
|
||||
// The loop exits when imported.len == to_import.len
|
||||
while imported_resources.folders.len() < resources.folders.len() {
|
||||
for mut v in resources.folders.clone() {
|
||||
v.id = maybe_gen_id(v.id.as_str(), ModelType::TypeFolder, &mut id_map);
|
||||
v.workspace_id = maybe_gen_id(
|
||||
v.workspace_id.as_str(),
|
||||
ModelType::TypeWorkspace,
|
||||
&mut id_map,
|
||||
);
|
||||
v.folder_id = maybe_gen_id_opt(v.folder_id, ModelType::TypeFolder, &mut id_map);
|
||||
if let Some(fid) = v.folder_id.clone() {
|
||||
let imported_parent = imported_resources.folders.iter().find(|f| f.id == fid);
|
||||
if imported_parent.is_none() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(_) = imported_resources.folders.iter().find(|f| f.id == v.id) {
|
||||
continue;
|
||||
}
|
||||
let x = upsert_folder(&w, v).await.map_err(|e| e.to_string())?;
|
||||
imported_resources.folders.push(x.clone());
|
||||
}
|
||||
}
|
||||
info!("Imported {} folders", imported_resources.folders.len());
|
||||
|
||||
@@ -853,7 +914,7 @@ async fn cmd_import_data(
|
||||
);
|
||||
|
||||
analytics::track_event(
|
||||
&w.app_handle(),
|
||||
&w,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Import,
|
||||
Some(json!({ "plugin": plugin_name })),
|
||||
@@ -864,49 +925,52 @@ async fn cmd_import_data(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_request_to_curl(
|
||||
app: AppHandle,
|
||||
request_id: &str,
|
||||
plugin_manager: State<'_, Mutex<PluginManager>>,
|
||||
environment_id: Option<&str>,
|
||||
) -> Result<String, String> {
|
||||
let request = get_http_request(&app, request_id)
|
||||
async fn cmd_http_request_actions(
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> Result<Vec<GetHttpRequestActionsResponse>, String> {
|
||||
plugin_manager
|
||||
.get_http_request_actions()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let environment = match environment_id {
|
||||
Some(id) => Some(get_environment(&app, id).await.map_err(|e| e.to_string())?),
|
||||
None => None,
|
||||
};
|
||||
let workspace = get_workspace(&app, &request.workspace_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rendered = render_request(&request, &workspace, environment.as_ref());
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
let import_response = plugin_manager
|
||||
.lock()
|
||||
#[tauri::command]
|
||||
async fn cmd_template_functions(
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> Result<Vec<GetTemplateFunctionsResponse>, String> {
|
||||
plugin_manager
|
||||
.get_template_functions()
|
||||
.await
|
||||
.run_export_curl(&rendered)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_call_http_request_action(
|
||||
req: CallHttpRequestActionRequest,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> Result<(), String> {
|
||||
plugin_manager
|
||||
.call_http_request_action(req)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(import_response.content)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_curl_to_request(
|
||||
command: &str,
|
||||
plugin_manager: State<'_, Mutex<PluginManager>>,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
workspace_id: &str,
|
||||
w: WebviewWindow,
|
||||
) -> Result<HttpRequest, String> {
|
||||
let (import_result, plugin_name) = plugin_manager
|
||||
.lock()
|
||||
.await
|
||||
.run_import(command)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let (import_result, plugin_name) = {
|
||||
plugin_manager
|
||||
.import_data(command)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
};
|
||||
|
||||
analytics::track_event(
|
||||
&w.app_handle(),
|
||||
&w,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Import,
|
||||
Some(json!({ "plugin": plugin_name })),
|
||||
@@ -947,7 +1011,7 @@ async fn cmd_export_data(
|
||||
f.sync_all().expect("Failed to sync");
|
||||
|
||||
analytics::track_event(
|
||||
&window.app_handle(),
|
||||
&window,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Export,
|
||||
None,
|
||||
@@ -984,7 +1048,6 @@ async fn cmd_send_http_request(
|
||||
window: WebviewWindow,
|
||||
environment_id: Option<&str>,
|
||||
cookie_jar_id: Option<&str>,
|
||||
download_dir: Option<&str>,
|
||||
// NOTE: We receive the entire request because to account for the race
|
||||
// condition where the user may have just edited a field before sending
|
||||
// that has not yet been saved in the DB.
|
||||
@@ -1010,28 +1073,9 @@ async fn cmd_send_http_request(
|
||||
None => None,
|
||||
};
|
||||
|
||||
let response = create_http_response(
|
||||
&window,
|
||||
&request.id,
|
||||
0,
|
||||
0,
|
||||
"",
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create response");
|
||||
|
||||
let download_path = if let Some(p) = download_dir {
|
||||
Some(std::path::Path::new(p).to_path_buf())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let response = create_default_http_response(&window, &request.id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false);
|
||||
window.listen_any(
|
||||
@@ -1043,20 +1087,19 @@ async fn cmd_send_http_request(
|
||||
|
||||
send_http_request(
|
||||
&window,
|
||||
request.clone(),
|
||||
&request,
|
||||
&response,
|
||||
environment,
|
||||
cookie_jar,
|
||||
download_path,
|
||||
&mut cancel_rx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn response_err(
|
||||
async fn response_err<R: Runtime>(
|
||||
response: &HttpResponse,
|
||||
error: String,
|
||||
w: &WebviewWindow,
|
||||
w: &WebviewWindow<R>,
|
||||
) -> Result<HttpResponse, String> {
|
||||
warn!("Failed to send request: {}", error);
|
||||
let mut response = response.clone();
|
||||
@@ -1080,7 +1123,7 @@ async fn cmd_track_event(
|
||||
AnalyticsAction::from_str(action),
|
||||
) {
|
||||
(Ok(resource), Ok(action)) => {
|
||||
analytics::track_event(&window.app_handle(), resource, action, attributes).await;
|
||||
analytics::track_event(&window, resource, action, attributes).await;
|
||||
}
|
||||
(r, a) => {
|
||||
error!(
|
||||
@@ -1128,6 +1171,24 @@ async fn cmd_create_workspace(name: &str, w: WebviewWindow) -> Result<Workspace,
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_create_plugin(
|
||||
directory: &str,
|
||||
url: Option<String>,
|
||||
w: WebviewWindow,
|
||||
) -> Result<Plugin, String> {
|
||||
upsert_plugin(
|
||||
&w,
|
||||
Plugin {
|
||||
directory: directory.into(),
|
||||
url,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_update_cookie_jar(
|
||||
cookie_jar: CookieJar,
|
||||
@@ -1371,10 +1432,9 @@ async fn cmd_list_grpc_requests(
|
||||
workspace_id: &str,
|
||||
w: WebviewWindow,
|
||||
) -> Result<Vec<GrpcRequest>, String> {
|
||||
let requests = list_grpc_requests(&w, workspace_id)
|
||||
list_grpc_requests(&w, workspace_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(requests)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1382,11 +1442,9 @@ async fn cmd_list_http_requests(
|
||||
workspace_id: &str,
|
||||
w: WebviewWindow,
|
||||
) -> Result<Vec<HttpRequest>, String> {
|
||||
let requests = list_http_requests(&w, workspace_id)
|
||||
list_http_requests(&w, workspace_id)
|
||||
.await
|
||||
.expect("Failed to find requests");
|
||||
// .map_err(|e| e.to_string())
|
||||
Ok(requests)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1394,11 +1452,33 @@ async fn cmd_list_environments(
|
||||
workspace_id: &str,
|
||||
w: WebviewWindow,
|
||||
) -> Result<Vec<Environment>, String> {
|
||||
let environments = list_environments(&w, workspace_id)
|
||||
list_environments(&w, workspace_id)
|
||||
.await
|
||||
.expect("Failed to find environments");
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
Ok(environments)
|
||||
#[tauri::command]
|
||||
async fn cmd_list_plugins(w: WebviewWindow) -> Result<Vec<Plugin>, String> {
|
||||
list_plugins(&w).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_reload_plugins(plugin_manager: State<'_, PluginManager>) -> Result<(), String> {
|
||||
plugin_manager.reload_all().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_plugin_info(
|
||||
id: &str,
|
||||
w: WebviewWindow,
|
||||
plugin_manager: State<'_, PluginManager>,
|
||||
) -> Result<PluginBootResponse, String> {
|
||||
let plugin = get_plugin(&w, id).await.map_err(|e| e.to_string())?;
|
||||
plugin_manager
|
||||
.get_plugin_info(plugin.directory.as_str())
|
||||
.await
|
||||
.ok_or("Failed to find plugin info".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1475,7 +1555,7 @@ async fn cmd_list_http_responses(
|
||||
limit: Option<i64>,
|
||||
w: WebviewWindow,
|
||||
) -> Result<Vec<HttpResponse>, String> {
|
||||
list_responses(&w, request_id, limit)
|
||||
list_http_responses(&w, request_id, limit)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -1635,19 +1715,24 @@ pub fn run() {
|
||||
let grpc_handle = GrpcHandle::new(&app.app_handle());
|
||||
app.manage(Mutex::new(grpc_handle));
|
||||
|
||||
// Add plugin manager
|
||||
let grpc_handle = GrpcHandle::new(&app.app_handle());
|
||||
app.manage(Mutex::new(grpc_handle));
|
||||
// Plugin template callback
|
||||
let plugin_cb = PluginTemplateCallback::new(app.app_handle().clone());
|
||||
app.manage(plugin_cb);
|
||||
|
||||
let app_handle = app.app_handle().clone();
|
||||
monitor_plugin_events(&app_handle);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
cmd_call_http_request_action,
|
||||
cmd_check_for_updates,
|
||||
cmd_create_cookie_jar,
|
||||
cmd_create_environment,
|
||||
cmd_create_folder,
|
||||
cmd_create_grpc_request,
|
||||
cmd_create_http_request,
|
||||
cmd_create_plugin,
|
||||
cmd_create_workspace,
|
||||
cmd_curl_to_request,
|
||||
cmd_delete_all_grpc_connections,
|
||||
@@ -1660,6 +1745,7 @@ pub fn run() {
|
||||
cmd_delete_http_request,
|
||||
cmd_delete_http_response,
|
||||
cmd_delete_workspace,
|
||||
cmd_dismiss_notification,
|
||||
cmd_duplicate_grpc_request,
|
||||
cmd_duplicate_http_request,
|
||||
cmd_export_data,
|
||||
@@ -1674,6 +1760,7 @@ pub fn run() {
|
||||
cmd_get_workspace,
|
||||
cmd_grpc_go,
|
||||
cmd_grpc_reflect,
|
||||
cmd_http_request_actions,
|
||||
cmd_import_data,
|
||||
cmd_list_cookie_jars,
|
||||
cmd_list_environments,
|
||||
@@ -1683,17 +1770,22 @@ pub fn run() {
|
||||
cmd_list_grpc_requests,
|
||||
cmd_list_http_requests,
|
||||
cmd_list_http_responses,
|
||||
cmd_list_plugins,
|
||||
cmd_list_workspaces,
|
||||
cmd_metadata,
|
||||
cmd_new_nested_window,
|
||||
cmd_new_window,
|
||||
cmd_request_to_curl,
|
||||
cmd_dismiss_notification,
|
||||
cmd_parse_template,
|
||||
cmd_plugin_info,
|
||||
cmd_reload_plugins,
|
||||
cmd_render_template,
|
||||
cmd_save_response,
|
||||
cmd_send_ephemeral_request,
|
||||
cmd_send_http_request,
|
||||
cmd_set_key_value,
|
||||
cmd_set_update_mode,
|
||||
cmd_template_functions,
|
||||
cmd_template_tokens_to_string,
|
||||
cmd_track_event,
|
||||
cmd_update_cookie_jar,
|
||||
cmd_update_environment,
|
||||
@@ -1715,10 +1807,9 @@ pub fn run() {
|
||||
.run(|app_handle, event| {
|
||||
match event {
|
||||
RunEvent::Ready => {
|
||||
create_window(app_handle, "/");
|
||||
let h = app_handle.clone();
|
||||
let w = create_window(app_handle, "/");
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let info = analytics::track_launch_event(&h).await;
|
||||
let info = analytics::track_launch_event(&w).await;
|
||||
debug!("Launched Yaak {:?}", info);
|
||||
});
|
||||
|
||||
@@ -1743,10 +1834,12 @@ pub fn run() {
|
||||
|
||||
let h = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let windows = h.webview_windows();
|
||||
let w = windows.values().next().unwrap();
|
||||
tokio::time::sleep(Duration::from_millis(4000)).await;
|
||||
let val: State<'_, Mutex<YaakNotifier>> = h.state();
|
||||
let val: State<'_, Mutex<YaakNotifier>> = w.state();
|
||||
let mut n = val.lock().await;
|
||||
if let Err(e) = n.check(&h).await {
|
||||
if let Err(e) = n.check(&w).await {
|
||||
warn!("Failed to check for notifications {}", e)
|
||||
}
|
||||
});
|
||||
@@ -1785,6 +1878,7 @@ fn create_nested_window(
|
||||
.title(title)
|
||||
.parent(&window)
|
||||
.unwrap()
|
||||
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
|
||||
.inner_size(DEFAULT_WINDOW_WIDTH * 0.7, DEFAULT_WINDOW_HEIGHT * 0.9);
|
||||
|
||||
// Add macOS-only things
|
||||
@@ -1795,7 +1889,7 @@ fn create_nested_window(
|
||||
.title_bar_style(TitleBarStyle::Overlay);
|
||||
}
|
||||
|
||||
// Add non-MacOS things
|
||||
// Add non-macOS things
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
win_builder = win_builder.decorations(false);
|
||||
@@ -1828,7 +1922,7 @@ fn create_window(handle: &AppHandle, url: &str) -> WebviewWindow {
|
||||
100.0 + random::<f64>() * 30.0,
|
||||
100.0 + random::<f64>() * 30.0,
|
||||
)
|
||||
.min_inner_size(300.0, 300.0)
|
||||
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)
|
||||
.title(handle.package_info().name.to_string());
|
||||
|
||||
// Add macOS-only things
|
||||
@@ -1854,7 +1948,8 @@ fn create_window(handle: &AppHandle, url: &str) -> WebviewWindow {
|
||||
return;
|
||||
}
|
||||
|
||||
match event.id().0.as_str() {
|
||||
let event_id = event.id().0.as_str();
|
||||
match event_id {
|
||||
"quit" => exit(0),
|
||||
"close" => w.close().unwrap(),
|
||||
"zoom_reset" => w.emit("zoom_reset", true).unwrap(),
|
||||
@@ -1905,3 +2000,192 @@ fn safe_uri(endpoint: &str) -> String {
|
||||
format!("http://{}", endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
|
||||
let app_handle = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let plugin_manager: State<'_, PluginManager> = app_handle.state();
|
||||
let (_rx_id, mut rx) = plugin_manager.subscribe().await;
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
let app_handle = app_handle.clone();
|
||||
let plugin = plugin_manager
|
||||
.get_plugin(event.plugin_ref_id.as_str())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// We might have recursive back-and-forth calls between app and plugin, so we don't
|
||||
// want to block here
|
||||
tauri::async_runtime::spawn(async move {
|
||||
handle_plugin_event(&app_handle, &event, &plugin).await;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn handle_plugin_event<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
event: &InternalEvent,
|
||||
plugin_handle: &PluginHandle,
|
||||
) {
|
||||
// info!("Got event to app {}", event.id);
|
||||
let response_event: Option<InternalEventPayload> = match event.clone().payload {
|
||||
InternalEventPayload::CopyTextRequest(req) => {
|
||||
app_handle
|
||||
.clipboard()
|
||||
.write_text(req.text.as_str())
|
||||
.expect("Failed to write text to clipboard");
|
||||
None
|
||||
}
|
||||
InternalEventPayload::ShowToastRequest(req) => {
|
||||
app_handle
|
||||
.emit("show_toast", req)
|
||||
.expect("Failed to emit show_toast");
|
||||
None
|
||||
}
|
||||
InternalEventPayload::FindHttpResponsesRequest(req) => {
|
||||
let http_responses = list_http_responses(
|
||||
app_handle,
|
||||
req.request_id.as_str(),
|
||||
req.limit.map(|l| l as i64),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
Some(InternalEventPayload::FindHttpResponsesResponse(
|
||||
FindHttpResponsesResponse { http_responses },
|
||||
))
|
||||
}
|
||||
InternalEventPayload::GetHttpRequestByIdRequest(req) => {
|
||||
let http_request = get_http_request(app_handle, req.id.as_str()).await.ok();
|
||||
Some(InternalEventPayload::GetHttpRequestByIdResponse(
|
||||
GetHttpRequestByIdResponse { http_request },
|
||||
))
|
||||
}
|
||||
InternalEventPayload::RenderHttpRequestRequest(req) => {
|
||||
let w = get_focused_window_no_lock(app_handle).expect("No focused window");
|
||||
let workspace = get_workspace(app_handle, req.http_request.workspace_id.as_str())
|
||||
.await
|
||||
.expect("Failed to get workspace for request");
|
||||
|
||||
let url = w.url().unwrap();
|
||||
let mut query_pairs = url.query_pairs();
|
||||
let environment_id = query_pairs
|
||||
.find(|(k, _v)| k == "environment_id")
|
||||
.map(|(_k, v)| v.to_string());
|
||||
let environment = match environment_id {
|
||||
None => None,
|
||||
Some(id) => get_environment(&w, id.as_str()).await.ok(),
|
||||
};
|
||||
let cb = &*app_handle.state::<PluginTemplateCallback>();
|
||||
let rendered_http_request =
|
||||
render_http_request(&req.http_request, &workspace, environment.as_ref(), cb).await;
|
||||
Some(InternalEventPayload::RenderHttpRequestResponse(
|
||||
RenderHttpRequestResponse {
|
||||
http_request: rendered_http_request,
|
||||
},
|
||||
))
|
||||
}
|
||||
InternalEventPayload::ReloadResponse(_) => {
|
||||
let w = get_focused_window_no_lock(app_handle).expect("No focused window");
|
||||
let plugins = list_plugins(&w).await.unwrap();
|
||||
for plugin in plugins {
|
||||
if plugin.directory != plugin_handle.dir {
|
||||
continue;
|
||||
}
|
||||
|
||||
upsert_plugin(
|
||||
&w,
|
||||
Plugin {
|
||||
// TODO: Add reloaded_at field to use instead
|
||||
updated_at: Utc::now().naive_utc(),
|
||||
..plugin
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let plugin_name = plugin_handle.info().await.unwrap().name;
|
||||
let toast_event = plugin_handle.build_event_to_send(
|
||||
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
|
||||
message: format!("Reloaded plugin {}", plugin_name),
|
||||
variant: ToastVariant::Info,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
|
||||
None
|
||||
}
|
||||
InternalEventPayload::SendHttpRequestRequest(req) => {
|
||||
let w = get_focused_window_no_lock(app_handle).expect("No focused window");
|
||||
let url = w.url().unwrap();
|
||||
let mut query_pairs = url.query_pairs();
|
||||
|
||||
let cookie_jar_id = query_pairs
|
||||
.find(|(k, _v)| k == "cookie_jar_id")
|
||||
.map(|(_k, v)| v.to_string());
|
||||
let cookie_jar = match cookie_jar_id {
|
||||
None => None,
|
||||
Some(id) => get_cookie_jar(app_handle, id.as_str()).await.ok(),
|
||||
};
|
||||
|
||||
let environment_id = query_pairs
|
||||
.find(|(k, _v)| k == "environment_id")
|
||||
.map(|(_k, v)| v.to_string());
|
||||
let environment = match environment_id {
|
||||
None => None,
|
||||
Some(id) => get_environment(app_handle, id.as_str()).await.ok(),
|
||||
};
|
||||
|
||||
let resp = create_default_http_response(&w, req.http_request.id.as_str())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = send_http_request(
|
||||
&w,
|
||||
&req.http_request,
|
||||
&resp,
|
||||
environment,
|
||||
cookie_jar,
|
||||
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel
|
||||
)
|
||||
.await;
|
||||
|
||||
let http_response = match result {
|
||||
Ok(r) => r,
|
||||
Err(_e) => return,
|
||||
};
|
||||
|
||||
Some(InternalEventPayload::SendHttpRequestResponse(
|
||||
SendHttpRequestResponse { http_response },
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(e) = response_event {
|
||||
let plugin_manager: State<'_, PluginManager> = app_handle.state();
|
||||
if let Err(e) = plugin_manager.reply(&event, &e).await {
|
||||
warn!("Failed to reply to plugin manager: {:?}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// app_handle.get_focused_window locks, so this one is a non-locking version, safe for use in async context
|
||||
fn get_focused_window_no_lock<R: Runtime>(app_handle: &AppHandle<R>) -> Option<WebviewWindow<R>> {
|
||||
// TODO: Getting the focused window doesn't seem to work on Windows, so
|
||||
// we'll need to pass the window label into plugin events instead.
|
||||
if app_handle.webview_windows().len() == 1 {
|
||||
let w = app_handle
|
||||
.webview_windows()
|
||||
.iter()
|
||||
.next()
|
||||
.map(|w| w.1.clone());
|
||||
return w;
|
||||
}
|
||||
|
||||
app_handle
|
||||
.webview_windows()
|
||||
.iter()
|
||||
.find(|w| w.1.is_focused().unwrap_or(false))
|
||||
.map(|w| w.1.clone())
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use chrono::{DateTime, Duration, Utc};
|
||||
use log::debug;
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tauri::{Emitter, Manager, Runtime, WebviewWindow};
|
||||
use yaak_models::queries::{get_key_value_raw, set_key_value_raw};
|
||||
|
||||
// Check for updates every hour
|
||||
@@ -42,16 +42,16 @@ impl YaakNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn seen(&mut self, app: &AppHandle, id: &str) -> Result<(), String> {
|
||||
let mut seen = get_kv(app).await?;
|
||||
pub async fn seen<R: Runtime>(&mut self, w: &WebviewWindow<R>, id: &str) -> Result<(), String> {
|
||||
let mut seen = get_kv(w).await?;
|
||||
seen.push(id.to_string());
|
||||
debug!("Marked notification as seen {}", id);
|
||||
let seen_json = serde_json::to_string(&seen).map_err(|e| e.to_string())?;
|
||||
set_key_value_raw(app, KV_NAMESPACE, KV_KEY, seen_json.as_str()).await;
|
||||
set_key_value_raw(w, KV_NAMESPACE, KV_KEY, seen_json.as_str()).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn check(&mut self, app: &AppHandle) -> Result<(), String> {
|
||||
pub async fn check<R: Runtime>(&mut self, w: &WebviewWindow<R>) -> Result<(), String> {
|
||||
let ignore_check = self.last_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
|
||||
|
||||
if ignore_check {
|
||||
@@ -60,8 +60,8 @@ impl YaakNotifier {
|
||||
|
||||
self.last_check = SystemTime::now();
|
||||
|
||||
let num_launches = get_num_launches(app).await;
|
||||
let info = app.package_info().clone();
|
||||
let num_launches = get_num_launches(w).await;
|
||||
let info = w.app_handle().package_info().clone();
|
||||
let req = reqwest::Client::default()
|
||||
.request(Method::GET, "https://notify.yaak.app/notifications")
|
||||
.query(&[
|
||||
@@ -80,21 +80,21 @@ impl YaakNotifier {
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let age = notification.timestamp.signed_duration_since(Utc::now());
|
||||
let seen = get_kv(app).await?;
|
||||
let seen = get_kv(w).await?;
|
||||
if seen.contains(¬ification.id) || (age > Duration::days(2)) {
|
||||
debug!("Already seen notification {}", notification.id);
|
||||
return Ok(());
|
||||
}
|
||||
debug!("Got notification {:?}", notification);
|
||||
|
||||
let _ = app.emit("notification", notification.clone());
|
||||
let _ = w.emit("notification", notification.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_kv(app: &AppHandle) -> Result<Vec<String>, String> {
|
||||
match get_key_value_raw(app, "notifications", "seen").await {
|
||||
async fn get_kv<R: Runtime>(w: &WebviewWindow<R>) -> Result<Vec<String>, String> {
|
||||
match get_key_value_raw(w, "notifications", "seen").await {
|
||||
None => Ok(Vec::new()),
|
||||
Some(v) => serde_json::from_str(&v.value).map_err(|e| e.to_string()),
|
||||
}
|
||||
|
||||
@@ -1,71 +1,113 @@
|
||||
use crate::template_callback::PluginTemplateCallback;
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::collections::HashMap;
|
||||
use serde_json::Value;
|
||||
use crate::template_fns::timestamp;
|
||||
use templates::parse_and_render;
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
use yaak_models::models::{
|
||||
Environment, EnvironmentVariable, HttpRequest, HttpRequestHeader, HttpUrlParameter, Workspace,
|
||||
Environment, EnvironmentVariable, GrpcMetadataEntry, GrpcRequest, HttpRequest,
|
||||
HttpRequestHeader, HttpUrlParameter, Workspace,
|
||||
};
|
||||
use yaak_templates::{parse_and_render, TemplateCallback};
|
||||
|
||||
pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -> HttpRequest {
|
||||
let r = r.clone();
|
||||
let vars = &variables_from_environment(w, e);
|
||||
pub async fn render_template<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
template: &str,
|
||||
w: &Workspace,
|
||||
e: Option<&Environment>,
|
||||
) -> String {
|
||||
let cb = &*app_handle.state::<PluginTemplateCallback>();
|
||||
let vars = &variables_from_environment(w, e, cb).await;
|
||||
render(template, vars, cb).await
|
||||
}
|
||||
|
||||
HttpRequest {
|
||||
url: render(r.url.as_str(), vars),
|
||||
url_parameters: r
|
||||
.url_parameters
|
||||
.iter()
|
||||
.map(|p| HttpUrlParameter {
|
||||
enabled: p.enabled,
|
||||
name: render(p.name.as_str(), vars),
|
||||
value: render(p.value.as_str(), vars),
|
||||
})
|
||||
.collect::<Vec<HttpUrlParameter>>(),
|
||||
headers: r
|
||||
.headers
|
||||
.iter()
|
||||
.map(|p| HttpRequestHeader {
|
||||
enabled: p.enabled,
|
||||
name: render(p.name.as_str(), vars),
|
||||
value: render(p.value.as_str(), vars),
|
||||
})
|
||||
.collect::<Vec<HttpRequestHeader>>(),
|
||||
body: r
|
||||
.body
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
let v = if v.is_string() {
|
||||
render(v.as_str().unwrap(), vars)
|
||||
} else {
|
||||
v.to_string()
|
||||
};
|
||||
(render(k, vars), Value::from(v))
|
||||
})
|
||||
.collect::<HashMap<String, Value>>(),
|
||||
authentication: r
|
||||
.authentication
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
let v = if v.is_string() {
|
||||
render(v.as_str().unwrap(), vars)
|
||||
} else {
|
||||
v.to_string()
|
||||
};
|
||||
(render(k, vars), Value::from(v))
|
||||
})
|
||||
.collect::<HashMap<String, Value>>(),
|
||||
..r
|
||||
pub async fn render_grpc_request<R: Runtime>(
|
||||
app_handle: &AppHandle<R>,
|
||||
r: &GrpcRequest,
|
||||
w: &Workspace,
|
||||
e: Option<&Environment>,
|
||||
) -> GrpcRequest {
|
||||
let cb = &*app_handle.state::<PluginTemplateCallback>();
|
||||
let vars = &variables_from_environment(w, e, cb).await;
|
||||
|
||||
let mut metadata = Vec::new();
|
||||
for p in r.metadata.clone() {
|
||||
metadata.push(GrpcMetadataEntry {
|
||||
enabled: p.enabled,
|
||||
name: render(p.name.as_str(), vars, cb).await,
|
||||
value: render(p.value.as_str(), vars, cb).await,
|
||||
})
|
||||
}
|
||||
|
||||
let mut authentication = HashMap::new();
|
||||
for (k, v) in r.authentication.clone() {
|
||||
authentication.insert(k, render_json_value(v, vars, cb).await);
|
||||
}
|
||||
|
||||
let url = render(r.url.as_str(), vars, cb).await;
|
||||
|
||||
GrpcRequest {
|
||||
url,
|
||||
metadata,
|
||||
authentication,
|
||||
..r.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recursively_render_variables<'s>(
|
||||
pub async fn render_http_request(
|
||||
r: &HttpRequest,
|
||||
w: &Workspace,
|
||||
e: Option<&Environment>,
|
||||
cb: &PluginTemplateCallback,
|
||||
) -> HttpRequest {
|
||||
let vars = &variables_from_environment(w, e, cb).await;
|
||||
|
||||
let mut url_parameters = Vec::new();
|
||||
for p in r.url_parameters.clone() {
|
||||
url_parameters.push(HttpUrlParameter {
|
||||
enabled: p.enabled,
|
||||
name: render(p.name.as_str(), vars, cb).await,
|
||||
value: render(p.value.as_str(), vars, cb).await,
|
||||
})
|
||||
}
|
||||
|
||||
let mut headers = Vec::new();
|
||||
for p in r.headers.clone() {
|
||||
headers.push(HttpRequestHeader {
|
||||
enabled: p.enabled,
|
||||
name: render(p.name.as_str(), vars, cb).await,
|
||||
value: render(p.value.as_str(), vars, cb).await,
|
||||
})
|
||||
}
|
||||
|
||||
let mut body = HashMap::new();
|
||||
for (k, v) in r.body.clone() {
|
||||
body.insert(k, render_json_value(v, vars, cb).await);
|
||||
}
|
||||
|
||||
let mut authentication = HashMap::new();
|
||||
for (k, v) in r.authentication.clone() {
|
||||
authentication.insert(k, render_json_value(v, vars, cb).await);
|
||||
}
|
||||
|
||||
let url = render(r.url.clone().as_str(), vars, cb).await;
|
||||
HttpRequest {
|
||||
url,
|
||||
url_parameters,
|
||||
headers,
|
||||
body,
|
||||
authentication,
|
||||
..r.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn recursively_render_variables<'s, T: TemplateCallback>(
|
||||
m: &HashMap<String, String>,
|
||||
render_count: usize,
|
||||
cb: &T,
|
||||
) -> HashMap<String, String> {
|
||||
let mut did_render = false;
|
||||
let mut new_map = m.clone();
|
||||
for (k, v) in m.clone() {
|
||||
let rendered = render(v.as_str(), m);
|
||||
let rendered = Box::pin(render(v.as_str(), m, cb)).await;
|
||||
if rendered != v {
|
||||
did_render = true
|
||||
}
|
||||
@@ -73,15 +115,16 @@ pub fn recursively_render_variables<'s>(
|
||||
}
|
||||
|
||||
if did_render && render_count <= 3 {
|
||||
new_map = recursively_render_variables(&new_map, render_count + 1);
|
||||
new_map = Box::pin(recursively_render_variables(&new_map, render_count + 1, cb)).await;
|
||||
}
|
||||
|
||||
new_map
|
||||
}
|
||||
|
||||
pub fn variables_from_environment(
|
||||
pub async fn variables_from_environment<T: TemplateCallback>(
|
||||
workspace: &Workspace,
|
||||
environment: Option<&Environment>,
|
||||
cb: &T,
|
||||
) -> HashMap<String, String> {
|
||||
let mut variables = HashMap::new();
|
||||
variables = add_variable_to_map(variables, &workspace.variables);
|
||||
@@ -90,18 +133,15 @@ pub fn variables_from_environment(
|
||||
variables = add_variable_to_map(variables, &e.variables);
|
||||
}
|
||||
|
||||
recursively_render_variables(&variables, 0)
|
||||
recursively_render_variables(&variables, 0, cb).await
|
||||
}
|
||||
|
||||
pub fn render(template: &str, vars: &HashMap<String, String>) -> String {
|
||||
parse_and_render(template, vars, Some(template_callback))
|
||||
}
|
||||
|
||||
fn template_callback(name: &str, args: HashMap<String, String>) -> Result<String, String> {
|
||||
match name {
|
||||
"timestamp" => timestamp(args),
|
||||
_ => Err(format!("Unknown template function {name}")),
|
||||
}
|
||||
pub async fn render<T: TemplateCallback>(
|
||||
template: &str,
|
||||
vars: &HashMap<String, String>,
|
||||
cb: &T,
|
||||
) -> String {
|
||||
parse_and_render(template, vars, cb).await
|
||||
}
|
||||
|
||||
fn add_variable_to_map(
|
||||
@@ -120,3 +160,103 @@ fn add_variable_to_map(
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
pub async fn render_json_value<T: TemplateCallback>(
|
||||
v: Value,
|
||||
vars: &HashMap<String, String>,
|
||||
cb: &T,
|
||||
) -> Value {
|
||||
match v {
|
||||
Value::String(s) => json!(render(s.as_str(), vars, cb).await),
|
||||
Value::Array(a) => {
|
||||
let mut new_a = Vec::new();
|
||||
for v in a {
|
||||
new_a.push(Box::pin(render_json_value(v, vars, cb)).await)
|
||||
}
|
||||
json!(new_a)
|
||||
}
|
||||
Value::Object(o) => {
|
||||
let mut new_o = Map::new();
|
||||
for (k, v) in o {
|
||||
let key = Box::pin(render(k.as_str(), vars, cb)).await;
|
||||
let value = Box::pin(render_json_value(v, vars, cb)).await;
|
||||
new_o.insert(key, value);
|
||||
}
|
||||
json!(new_o)
|
||||
}
|
||||
v => v,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use yaak_templates::TemplateCallback;
|
||||
|
||||
struct EmptyCB {}
|
||||
|
||||
impl TemplateCallback for EmptyCB {
|
||||
async fn run(
|
||||
&self,
|
||||
_fn_name: &str,
|
||||
_args: HashMap<String, String>,
|
||||
) -> Result<String, String> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn render_json_value_string() {
|
||||
let v = json!("${[a]}");
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert("a".to_string(), "aaa".to_string());
|
||||
|
||||
let result = super::render_json_value(v, &vars, &EmptyCB {}).await;
|
||||
assert_eq!(result, json!("aaa"))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn render_json_value_array() {
|
||||
let v = json!(["${[a]}", "${[a]}"]);
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert("a".to_string(), "aaa".to_string());
|
||||
|
||||
let result = super::render_json_value(v, &vars, &EmptyCB {}).await;
|
||||
assert_eq!(result, json!(["aaa", "aaa"]))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn render_json_value_object() {
|
||||
let v = json!({"${[a]}": "${[a]}"});
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert("a".to_string(), "aaa".to_string());
|
||||
|
||||
let result = super::render_json_value(v, &vars, &EmptyCB {}).await;
|
||||
assert_eq!(result, json!({"aaa": "aaa"}))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn render_json_value_nested() {
|
||||
let v = json!([
|
||||
123,
|
||||
{"${[a]}": "${[a]}"},
|
||||
null,
|
||||
"${[a]}",
|
||||
false,
|
||||
{"x": ["${[a]}"]}
|
||||
]);
|
||||
let mut vars = HashMap::new();
|
||||
vars.insert("a".to_string(), "aaa".to_string());
|
||||
|
||||
let result = super::render_json_value(v, &vars, &EmptyCB {}).await;
|
||||
assert_eq!(result, json!([
|
||||
123,
|
||||
{"aaa": "aaa"},
|
||||
null,
|
||||
"aaa",
|
||||
false,
|
||||
{"x": ["aaa"]}
|
||||
]))
|
||||
}
|
||||
}
|
||||
|
||||
69
src-tauri/src/template_callback.rs
Normal file
69
src-tauri/src/template_callback.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use std::collections::HashMap;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use yaak_plugin_runtime::events::{RenderPurpose, TemplateFunctionArg};
|
||||
use yaak_plugin_runtime::manager::PluginManager;
|
||||
use yaak_templates::TemplateCallback;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginTemplateCallback {
|
||||
app_handle: AppHandle,
|
||||
purpose: RenderPurpose,
|
||||
}
|
||||
|
||||
impl PluginTemplateCallback {
|
||||
pub fn new(app_handle: AppHandle) -> PluginTemplateCallback {
|
||||
PluginTemplateCallback {
|
||||
app_handle,
|
||||
purpose: RenderPurpose::Preview,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_send(&self) -> PluginTemplateCallback {
|
||||
let mut v = self.clone();
|
||||
v.purpose = RenderPurpose::Send;
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
impl TemplateCallback for PluginTemplateCallback {
|
||||
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String, String> {
|
||||
// The beta named the function `Response` but was changed in stable.
|
||||
// Keep this here for a while because there's no easy way to migrate
|
||||
let fn_name = if fn_name == "Response" {
|
||||
"response"
|
||||
} else {
|
||||
fn_name
|
||||
};
|
||||
|
||||
let plugin_manager = self.app_handle.state::<PluginManager>();
|
||||
let function = plugin_manager
|
||||
.get_template_functions()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.iter()
|
||||
.flat_map(|f| f.functions.clone())
|
||||
.find(|f| f.name == fn_name)
|
||||
.ok_or("")?;
|
||||
|
||||
let mut args_with_defaults = args.clone();
|
||||
|
||||
// Fill in default values for all args
|
||||
for a_def in function.args {
|
||||
let base = match a_def {
|
||||
TemplateFunctionArg::Text(a) => a.base,
|
||||
TemplateFunctionArg::Select(a) => a.base,
|
||||
TemplateFunctionArg::Checkbox(a) => a.base,
|
||||
TemplateFunctionArg::HttpRequest(a) => a.base,
|
||||
};
|
||||
if let None = args_with_defaults.get(base.name.as_str()) {
|
||||
args_with_defaults.insert(base.name, base.default_value.unwrap_or_default());
|
||||
}
|
||||
}
|
||||
|
||||
let resp = plugin_manager
|
||||
.call_template_function(fn_name, args_with_defaults, self.purpose.clone())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(resp.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn timestamp(args: HashMap<String, String>) -> Result<String, String> {
|
||||
let from = args.get("from").map(|v| v.as_str()).unwrap_or("now");
|
||||
let format = args.get("format").map(|v| v.as_str()).unwrap_or("rfc3339");
|
||||
|
||||
let dt = match from {
|
||||
"now" => {
|
||||
let now = Utc::now();
|
||||
now
|
||||
}
|
||||
_ => {
|
||||
let json_from = serde_json::to_string(from).unwrap_or_default();
|
||||
let now: DateTime<Utc> = match serde_json::from_str(json_from.as_str()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => return Err(e.to_string()),
|
||||
};
|
||||
now
|
||||
}
|
||||
};
|
||||
|
||||
let result = match format {
|
||||
"rfc3339" => dt.to_rfc3339(),
|
||||
"unix" => dt.timestamp().to_string(),
|
||||
"unix_millis" => dt.timestamp_millis().to_string(),
|
||||
_ => "".to_string(),
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// Test it
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::template_fns::timestamp;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn timestamp_empty() {
|
||||
let args = HashMap::new();
|
||||
assert_ne!(timestamp(args), Ok("".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timestamp_from() {
|
||||
let mut args = HashMap::new();
|
||||
args.insert("from".to_string(), "2024-07-31T14:16:41.983Z".to_string());
|
||||
assert_eq!(
|
||||
timestamp(args),
|
||||
Ok("2024-07-31T14:16:41.983+00:00".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timestamp_format_unix() {
|
||||
let mut args = HashMap::new();
|
||||
args.insert("from".to_string(), "2024-07-31T14:16:41.983Z".to_string());
|
||||
args.insert("format".to_string(), "unix".to_string());
|
||||
assert_eq!(timestamp(args), Ok("1722435401".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timestamp_format_unix_millis() {
|
||||
let mut args = HashMap::new();
|
||||
args.insert("from".to_string(), "2024-07-31T14:16:41.983Z".to_string());
|
||||
args.insert("format".to_string(), "unix_millis".to_string());
|
||||
assert_eq!(timestamp(args), Ok("1722435401983".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,17 @@ use std::fmt::{Display, Formatter};
|
||||
use std::time::SystemTime;
|
||||
|
||||
use log::info;
|
||||
use tauri::AppHandle;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
use tokio::task::block_in_place;
|
||||
use yaak_plugin_runtime::manager::PluginManager;
|
||||
|
||||
use crate::is_dev;
|
||||
|
||||
// Check for updates every 3 hours
|
||||
const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60 * 3;
|
||||
const MAX_UPDATE_CHECK_HOURS_STABLE: u64 = 12;
|
||||
const MAX_UPDATE_CHECK_HOURS_BETA: u64 = 3;
|
||||
const MAX_UPDATE_CHECK_HOURS_ALPHA: u64 = 1;
|
||||
|
||||
// Create updater struct
|
||||
pub struct YaakUpdater {
|
||||
@@ -49,6 +52,7 @@ impl YaakUpdater {
|
||||
last_update_check: SystemTime::UNIX_EPOCH,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn force_check(
|
||||
&mut self,
|
||||
app_handle: &AppHandle,
|
||||
@@ -58,8 +62,22 @@ impl YaakUpdater {
|
||||
|
||||
info!("Checking for updates mode={}", mode);
|
||||
|
||||
let h = app_handle.clone();
|
||||
let update_check_result = app_handle
|
||||
.updater_builder()
|
||||
.on_before_exit(move || {
|
||||
// Kill plugin manager before exit or NSIS installer will fail to replace sidecar
|
||||
// while it's running.
|
||||
// NOTE: This is only called on Windows
|
||||
let h = h.clone();
|
||||
block_in_place(|| {
|
||||
tauri::async_runtime::block_on(async move {
|
||||
info!("Shutting down plugin manager before update");
|
||||
let plugin_manager = h.state::<PluginManager>();
|
||||
plugin_manager.cleanup().await;
|
||||
});
|
||||
});
|
||||
})
|
||||
.header("X-Update-Mode", mode.to_string())?
|
||||
.build()?
|
||||
.check()
|
||||
@@ -112,8 +130,13 @@ impl YaakUpdater {
|
||||
app_handle: &AppHandle,
|
||||
mode: UpdateMode,
|
||||
) -> Result<bool, tauri_plugin_updater::Error> {
|
||||
let ignore_check =
|
||||
self.last_update_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
|
||||
let update_period_seconds = match mode {
|
||||
UpdateMode::Stable => MAX_UPDATE_CHECK_HOURS_STABLE,
|
||||
UpdateMode::Beta => MAX_UPDATE_CHECK_HOURS_BETA,
|
||||
UpdateMode::Alpha => MAX_UPDATE_CHECK_HOURS_ALPHA,
|
||||
} * (60 * 60);
|
||||
let seconds_since_last_check = self.last_update_check.elapsed().unwrap().as_secs();
|
||||
let ignore_check = seconds_since_last_check < update_period_seconds;
|
||||
if ignore_check {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"publisher": "Yaak",
|
||||
"license": "MIT",
|
||||
"copyright": "Yaak",
|
||||
"homepage": "https://yaak.app",
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"externalBin": [
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
[package]
|
||||
name = "templates"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.22"
|
||||
@@ -1,494 +0,0 @@
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct FnArg {
|
||||
pub name: String,
|
||||
pub value: Val,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum Val {
|
||||
Str(String),
|
||||
Var(String),
|
||||
Fn { name: String, args: Vec<FnArg> },
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum Token {
|
||||
Raw(String),
|
||||
Tag(Val),
|
||||
Eof,
|
||||
}
|
||||
|
||||
// Template Syntax
|
||||
//
|
||||
// ${[ my_var ]}
|
||||
// ${[ my_fn() ]}
|
||||
// ${[ my_fn(my_var) ]}
|
||||
// ${[ my_fn(my_var, "A String") ]}
|
||||
|
||||
// default
|
||||
#[derive(Default)]
|
||||
pub struct Parser {
|
||||
tokens: Vec<Token>,
|
||||
chars: Vec<char>,
|
||||
pos: usize,
|
||||
curr_text: String,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn new(text: &str) -> Parser {
|
||||
Parser {
|
||||
chars: text.chars().collect(),
|
||||
..Parser::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(&mut self) -> Vec<Token> {
|
||||
let start_pos = self.pos;
|
||||
|
||||
while self.pos < self.chars.len() {
|
||||
if self.match_str("${[") {
|
||||
let start_curr = self.pos;
|
||||
if let Some(t) = self.parse_tag() {
|
||||
self.push_token(t);
|
||||
} else {
|
||||
self.pos = start_curr;
|
||||
self.curr_text += "${[";
|
||||
}
|
||||
} else {
|
||||
let ch = self.next_char();
|
||||
self.curr_text.push(ch);
|
||||
}
|
||||
|
||||
if start_pos == self.pos {
|
||||
panic!("Parser stuck!");
|
||||
}
|
||||
}
|
||||
|
||||
self.push_token(Token::Eof);
|
||||
self.tokens.clone()
|
||||
}
|
||||
|
||||
fn parse_tag(&mut self) -> Option<Token> {
|
||||
// Parse up to first identifier
|
||||
// ${[ my_var...
|
||||
self.skip_whitespace();
|
||||
|
||||
let val = match self.parse_value() {
|
||||
Some(v) => v,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
// Parse to closing tag
|
||||
// ${[ my_var(a, b, c) ]}
|
||||
self.skip_whitespace();
|
||||
if !self.match_str("]}") {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Token::Tag(val))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn debug_pos(&self, x: &str) {
|
||||
println!(
|
||||
r#"Position: {x}: text[{}]='{}' → "{}" → {:?}"#,
|
||||
self.pos,
|
||||
self.chars[self.pos],
|
||||
self.chars.iter().collect::<String>(),
|
||||
self.tokens,
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_value(&mut self) -> Option<Val> {
|
||||
if let Some((name, args)) = self.parse_fn() {
|
||||
Some(Val::Fn { name, args })
|
||||
} else if let Some(v) = self.parse_ident() {
|
||||
Some(Val::Var(v))
|
||||
} else if let Some(v) = self.parse_string() {
|
||||
Some(Val::Str(v))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_fn(&mut self) -> Option<(String, Vec<FnArg>)> {
|
||||
let start_pos = self.pos;
|
||||
|
||||
let name = match self.parse_ident() {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
self.pos = start_pos;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let args = match self.parse_fn_args() {
|
||||
Some(args) => args,
|
||||
None => {
|
||||
self.pos = start_pos;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some((name, args))
|
||||
}
|
||||
|
||||
fn parse_fn_args(&mut self) -> Option<Vec<FnArg>> {
|
||||
if !self.match_str("(") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start_pos = self.pos;
|
||||
|
||||
let mut args: Vec<FnArg> = Vec::new();
|
||||
|
||||
// Fn closed immediately
|
||||
self.skip_whitespace();
|
||||
if self.match_str(")") {
|
||||
return Some(args)
|
||||
}
|
||||
|
||||
while self.pos < self.chars.len() {
|
||||
self.skip_whitespace();
|
||||
|
||||
let name = self.parse_ident();
|
||||
self.skip_whitespace();
|
||||
self.match_str("=");
|
||||
self.skip_whitespace();
|
||||
let value = self.parse_value();
|
||||
self.skip_whitespace();
|
||||
|
||||
if let (Some(name), Some(value)) = (name.clone(), value.clone()) {
|
||||
args.push(FnArg { name, value });
|
||||
} else {
|
||||
// Didn't find valid thing, so return
|
||||
self.pos = start_pos;
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.match_str(")") {
|
||||
break;
|
||||
}
|
||||
|
||||
self.skip_whitespace();
|
||||
|
||||
// If we don't find a comma, that's bad
|
||||
if !args.is_empty() && !self.match_str(",") {
|
||||
self.pos = start_pos;
|
||||
return None;
|
||||
}
|
||||
|
||||
if start_pos == self.pos {
|
||||
panic!("Parser stuck!");
|
||||
}
|
||||
}
|
||||
|
||||
return Some(args);
|
||||
}
|
||||
|
||||
fn parse_ident(&mut self) -> Option<String> {
|
||||
let start_pos = self.pos;
|
||||
|
||||
let mut text = String::new();
|
||||
while self.pos < self.chars.len() {
|
||||
let ch = self.peek_char();
|
||||
if ch.is_alphanumeric() || ch == '_' {
|
||||
text.push(ch);
|
||||
self.pos += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
if start_pos == self.pos {
|
||||
panic!("Parser stuck!");
|
||||
}
|
||||
}
|
||||
|
||||
if text.is_empty() {
|
||||
self.pos = start_pos;
|
||||
return None;
|
||||
}
|
||||
|
||||
return Some(text);
|
||||
}
|
||||
|
||||
fn parse_string(&mut self) -> Option<String> {
|
||||
let start_pos = self.pos;
|
||||
|
||||
let mut text = String::new();
|
||||
if !self.match_str("\"") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut found_closing = false;
|
||||
while self.pos < self.chars.len() {
|
||||
let ch = self.next_char();
|
||||
match ch {
|
||||
'\\' => {
|
||||
text.push(self.next_char());
|
||||
}
|
||||
'"' => {
|
||||
found_closing = true;
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
text.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
if start_pos == self.pos {
|
||||
panic!("Parser stuck!");
|
||||
}
|
||||
}
|
||||
|
||||
if !found_closing {
|
||||
self.pos = start_pos;
|
||||
return None;
|
||||
}
|
||||
|
||||
return Some(text);
|
||||
}
|
||||
|
||||
fn skip_whitespace(&mut self) {
|
||||
while self.pos < self.chars.len() {
|
||||
if self.peek_char().is_whitespace() {
|
||||
self.pos += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_char(&mut self) -> char {
|
||||
let ch = self.peek_char();
|
||||
|
||||
self.pos += 1;
|
||||
ch
|
||||
}
|
||||
|
||||
fn peek_char(&self) -> char {
|
||||
let ch = self.chars[self.pos];
|
||||
ch
|
||||
}
|
||||
|
||||
fn push_token(&mut self, token: Token) {
|
||||
// Push any text we've accumulated
|
||||
if !self.curr_text.is_empty() {
|
||||
let text_token = Token::Raw(self.curr_text.clone());
|
||||
self.tokens.push(text_token);
|
||||
self.curr_text.clear();
|
||||
}
|
||||
|
||||
self.tokens.push(token);
|
||||
}
|
||||
|
||||
fn match_str(&mut self, value: &str) -> bool {
|
||||
if self.pos + value.len() > self.chars.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let cmp = self.chars[self.pos..self.pos + value.len()]
|
||||
.iter()
|
||||
.collect::<String>();
|
||||
|
||||
if cmp == value {
|
||||
// We have a match, so advance the current index
|
||||
self.pos += value.len();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::*;
|
||||
|
||||
#[test]
|
||||
fn var_simple() {
|
||||
let mut p = Parser::new("${[ foo ]}");
|
||||
assert_eq!(
|
||||
p.parse(),
|
||||
vec![Token::Tag(Val::Var("foo".into())), Token::Eof]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn var_multiple_names_invalid() {
|
||||
let mut p = Parser::new("${[ foo bar ]}");
|
||||
assert_eq!(
|
||||
p.parse(),
|
||||
vec![Token::Raw("${[ foo bar ]}".into()), Token::Eof]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tag_string() {
|
||||
let mut p = Parser::new(r#"${[ "foo \"bar\" baz" ]}"#);
|
||||
assert_eq!(
|
||||
p.parse(),
|
||||
vec![Token::Tag(Val::Str(r#"foo "bar" baz"#.into())), Token::Eof]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn var_surrounded() {
|
||||
let mut p = Parser::new("Hello ${[ foo ]}!");
|
||||
assert_eq!(
|
||||
p.parse(),
|
||||
vec![
|
||||
Token::Raw("Hello ".to_string()),
|
||||
Token::Tag(Val::Var("foo".into())),
|
||||
Token::Raw("!".to_string()),
|
||||
Token::Eof,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_simple() {
|
||||
let mut p = Parser::new("${[ foo() ]}");
|
||||
assert_eq!(
|
||||
p.parse(),
|
||||
vec![
|
||||
Token::Tag(Val::Fn {
|
||||
name: "foo".into(),
|
||||
args: Vec::new(),
|
||||
}),
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_ident_arg() {
|
||||
let mut p = Parser::new("${[ foo(a=bar) ]}");
|
||||
assert_eq!(
|
||||
p.parse(),
|
||||
vec![
|
||||
Token::Tag(Val::Fn {
|
||||
name: "foo".into(),
|
||||
args: vec![FnArg {
|
||||
name: "a".into(),
|
||||
value: Val::Var("bar".into())
|
||||
}],
|
||||
}),
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_ident_args() {
|
||||
let mut p = Parser::new("${[ foo(a=bar,b = baz, c =qux ) ]}");
|
||||
assert_eq!(
|
||||
p.parse(),
|
||||
vec![
|
||||
Token::Tag(Val::Fn {
|
||||
name: "foo".into(),
|
||||
args: vec![
|
||||
FnArg {
|
||||
name: "a".into(),
|
||||
value: Val::Var("bar".into())
|
||||
},
|
||||
FnArg {
|
||||
name: "b".into(),
|
||||
value: Val::Var("baz".into())
|
||||
},
|
||||
FnArg {
|
||||
name: "c".into(),
|
||||
value: Val::Var("qux".into())
|
||||
},
|
||||
],
|
||||
}),
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_mixed_args() {
|
||||
let mut p = Parser::new(r#"${[ foo(aaa=bar,bb="baz \"hi\"", c=qux ) ]}"#);
|
||||
assert_eq!(
|
||||
p.parse(),
|
||||
vec![
|
||||
Token::Tag(Val::Fn {
|
||||
name: "foo".into(),
|
||||
args: vec![
|
||||
FnArg {
|
||||
name: "aaa".into(),
|
||||
value: Val::Var("bar".into())
|
||||
},
|
||||
FnArg {
|
||||
name: "bb".into(),
|
||||
value: Val::Str(r#"baz "hi""#.into())
|
||||
},
|
||||
FnArg {
|
||||
name: "c".into(),
|
||||
value: Val::Var("qux".into())
|
||||
},
|
||||
],
|
||||
}),
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_nested() {
|
||||
let mut p = Parser::new("${[ foo(b=bar()) ]}");
|
||||
assert_eq!(
|
||||
p.parse(),
|
||||
vec![
|
||||
Token::Tag(Val::Fn {
|
||||
name: "foo".into(),
|
||||
args: vec![FnArg {
|
||||
name: "b".into(),
|
||||
value: Val::Fn {
|
||||
name: "bar".into(),
|
||||
args: vec![],
|
||||
}
|
||||
}],
|
||||
}),
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_nested_args() {
|
||||
let mut p = Parser::new(r#"${[ outer(a=inner(a=foo, b="i"), c="o") ]}"#);
|
||||
assert_eq!(
|
||||
p.parse(),
|
||||
vec![
|
||||
Token::Tag(Val::Fn {
|
||||
name: "outer".into(),
|
||||
args: vec![
|
||||
FnArg {
|
||||
name: "a".into(),
|
||||
value: Val::Fn {
|
||||
name: "inner".into(),
|
||||
args: vec![
|
||||
FnArg {
|
||||
name: "a".into(),
|
||||
value: Val::Var("foo".into())
|
||||
},
|
||||
FnArg {
|
||||
name: "b".into(),
|
||||
value: Val::Str("i".into()),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
FnArg {
|
||||
name: "c".into(),
|
||||
value: Val::Str("o".into())
|
||||
},
|
||||
],
|
||||
}),
|
||||
Token::Eof
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
use crate::{FnArg, Parser, Token, Val};
|
||||
use log::warn;
|
||||
use std::collections::HashMap;
|
||||
|
||||
type TemplateCallback = fn(name: &str, args: HashMap<String, String>) -> Result<String, String>;
|
||||
|
||||
pub fn parse_and_render(
|
||||
template: &str,
|
||||
vars: &HashMap<String, String>,
|
||||
cb: Option<TemplateCallback>,
|
||||
) -> String {
|
||||
let mut p = Parser::new(template);
|
||||
let tokens = p.parse();
|
||||
render(tokens, vars, cb)
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
tokens: Vec<Token>,
|
||||
vars: &HashMap<String, String>,
|
||||
cb: Option<TemplateCallback>,
|
||||
) -> String {
|
||||
let mut doc_str: Vec<String> = Vec::new();
|
||||
|
||||
for t in tokens {
|
||||
match t {
|
||||
Token::Raw(s) => doc_str.push(s),
|
||||
Token::Tag(val) => doc_str.push(render_tag(val, &vars, cb)),
|
||||
Token::Eof => {}
|
||||
}
|
||||
}
|
||||
|
||||
return doc_str.join("");
|
||||
}
|
||||
|
||||
fn render_tag(val: Val, vars: &HashMap<String, String>, cb: Option<TemplateCallback>) -> String {
|
||||
match val {
|
||||
Val::Str(s) => s.into(),
|
||||
Val::Var(name) => match vars.get(name.as_str()) {
|
||||
Some(v) => v.to_string(),
|
||||
None => "".into(),
|
||||
},
|
||||
Val::Fn { name, args } => {
|
||||
let empty = "".to_string();
|
||||
let resolved_args = args
|
||||
.iter()
|
||||
.map(|a| match a {
|
||||
FnArg {
|
||||
name,
|
||||
value: Val::Str(s),
|
||||
} => (name.to_string(), s.to_string()),
|
||||
FnArg {
|
||||
name,
|
||||
value: Val::Var(i),
|
||||
} => (
|
||||
name.to_string(),
|
||||
vars.get(i.as_str()).unwrap_or(&empty).to_string(),
|
||||
),
|
||||
FnArg { name, value: val } => {
|
||||
(name.to_string(), render_tag(val.clone(), vars, cb))
|
||||
}
|
||||
})
|
||||
.collect::<HashMap<String, String>>();
|
||||
match cb {
|
||||
Some(cb) => match cb(name.as_str(), resolved_args.clone()) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
warn!("Failed to run template callback {}({:?}): {}", name, resolved_args, e);
|
||||
"".to_string()
|
||||
}
|
||||
},
|
||||
None => "".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[test]
|
||||
fn render_empty() {
|
||||
let template = "";
|
||||
let vars = HashMap::new();
|
||||
let result = "";
|
||||
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_text_only() {
|
||||
let template = "Hello World!";
|
||||
let vars = HashMap::new();
|
||||
let result = "Hello World!";
|
||||
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_simple() {
|
||||
let template = "${[ foo ]}";
|
||||
let vars = HashMap::from([("foo".to_string(), "bar".to_string())]);
|
||||
let result = "bar";
|
||||
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_surrounded() {
|
||||
let template = "hello ${[ word ]} world!";
|
||||
let vars = HashMap::from([("word".to_string(), "cruel".to_string())]);
|
||||
let result = "hello cruel world!";
|
||||
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_valid_fn() {
|
||||
let vars = HashMap::new();
|
||||
let template = r#"${[ say_hello(a="John", b="Kate") ]}"#;
|
||||
let result = r#"say_hello: 2, Some("John") Some("Kate")"#;
|
||||
|
||||
fn cb(name: &str, args: HashMap<String, String>) -> Result<String, String> {
|
||||
Ok(format!(
|
||||
"{name}: {}, {:?} {:?}",
|
||||
args.len(),
|
||||
args.get("a"),
|
||||
args.get("b")
|
||||
))
|
||||
}
|
||||
assert_eq!(parse_and_render(template, &vars, Some(cb)), result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_nested_fn() {
|
||||
let vars = HashMap::new();
|
||||
let template = r#"${[ upper(foo=secret()) ]}"#;
|
||||
let result = r#"ABC"#;
|
||||
fn cb(name: &str, args: HashMap<String, String>) -> Result<String, String> {
|
||||
Ok(match name {
|
||||
"secret" => "abc".to_string(),
|
||||
"upper" => args["foo"].to_string().to_uppercase(),
|
||||
_ => "".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
parse_and_render(template, &vars, Some(cb)),
|
||||
result.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_fn_err() {
|
||||
let vars = HashMap::new();
|
||||
let template = r#"${[ error() ]}"#;
|
||||
let result = r#""#;
|
||||
fn cb(_name: &str, _args: HashMap<String, String>) -> Result<String, String> {
|
||||
Err("Failed to do it!".to_string())
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
parse_and_render(template, &vars, Some(cb)),
|
||||
result.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "grpc"
|
||||
name = "yaak_grpc"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user