Compare commits

...

402 Commits

Author SHA1 Message Date
Gregory Schier
aeda72f13e Fix plugin (again) 2023-11-08 13:33:15 -08:00
Gregory Schier
83aa9041cb Bundle plugin in Git 2023-11-08 13:06:49 -08:00
Gregory Schier
d51913509d Move plugins back 2023-11-08 12:34:14 -08:00
Gregory Schier
5106f28ba5 Fix permissions 2023-11-08 12:02:17 -08:00
Gregory Schier
0c55c6eaab Bump version 2023-11-08 10:12:01 -08:00
Gregory Schier
b0edbd19c8 Tweak theme 2023-11-08 10:11:29 -08:00
Gregory Schier
7630db79b7 Tweak theme 2023-11-08 09:56:13 -08:00
Gregory Schier
55a7b82567 Track screen size, os, and version 2023-11-08 09:49:29 -08:00
Gregory Schier
b5cb46918a Rust analytics and a few tweaks 2023-11-08 09:13:32 -08:00
Gregory Schier
a793ece1a5 Add basic analytics 2023-11-07 09:53:59 -08:00
Gregory Schier
0f6e4b641a Cancel responses on startup 2023-11-06 13:06:15 -08:00
Gregory Schier
5ac5fab0c6 Send all in a folder 2023-11-06 10:53:39 -08:00
Gregory Schier
8030a8a235 Rework workspace header 2023-11-06 10:42:59 -08:00
Gregory Schier
d98426cad3 Fix sidebar focus max recursion 2023-11-06 10:40:02 -08:00
Gregory Schier
06034a8fc4 Save after formatting GraphQL (Closes #9) 2023-11-06 07:20:47 -08:00
Gregory Schier
1ee9f9bb51 Move plugins back to root dir 2023-11-06 07:18:53 -08:00
Gregory Schier
4b99d1405e Persist sidebar collapsed state (Closes #10) 2023-11-06 07:18:42 -08:00
Gregory Schier
8480e52195 Vite to bundle insomnia plugin 2023-11-05 22:13:22 -08:00
Gregory Schier
243e65a992 Better import conversion 2023-11-05 14:46:08 -08:00
Gregory Schier
b82304a233 Basic import of request body and bearer auth 2023-11-05 14:35:25 -08:00
Gregory Schier
f7a4ea9735 Recursive Insomnia import! 2023-11-05 13:33:23 -08:00
Gregory Schier
33d1a84ecd Remove numbers from recent requests 2023-11-04 16:19:21 -07:00
Gregory Schier
f4a071ee05 Fix drop marker indent 2023-11-04 11:16:41 -07:00
Gregory Schier
e26ba0f9d0 Folder actions 2023-11-04 10:48:18 -07:00
Gregory Schier
b4e2a12375 Folder chevron icon 2023-11-03 23:10:44 -07:00
Gregory Schier
5e7aacd31a Fix arrow navigation for nested sidebar 2023-11-03 17:29:33 -07:00
Gregory Schier
00718df49e Folder-based drag-n-drop complete! 2023-11-03 16:29:21 -07:00
Gregory Schier
bb9025ab07 Sidebar ordering 95% done! 2023-11-03 15:02:17 -07:00
Gregory Schier
867f3908ed Nested sidebar ordering almost working 2023-11-03 14:08:46 -07:00
Gregory Schier
30e1ecac39 Add folder model 2023-11-03 07:49:44 -07:00
Gregory Schier
7eb2abe9b2 Even better focus state 2023-11-02 22:23:21 -07:00
Gregory Schier
a5ac8fa035 Remove focus on env sidebar buttons 2023-11-02 22:18:13 -07:00
Gregory Schier
dd705de155 Rearrange menus 2023-11-02 21:41:35 -07:00
Gregory Schier
b15cdec701 Refactor environment edit dialog 2023-11-02 20:38:33 -07:00
Gregory Schier
a99a36b5cc Base environments fully working 2023-11-02 18:43:39 -07:00
Gregory Schier
e0b0e3d781 Initial "plugin" system with importer (#7) 2023-11-02 18:08:43 -07:00
Gregory Schier
98a4834d4f Fix cursor color in single-line mode 2023-10-31 15:10:33 -07:00
Gregory Schier
32b135dbaf Fix sending of ephemeral requests 2023-10-30 08:24:49 -07:00
Gregory Schier
0fc8d12a06 Fix GQL introspection and bearer auth templating 2023-10-30 08:07:34 -07:00
Gregory Schier
3c2bdab101 Fix button styles 2023-10-30 07:27:27 -07:00
Gregory Schier
8b5d7ae3ed Fix editor stale callbacks and recent item deletion 2023-10-30 07:07:14 -07:00
Gregory Schier
51949f4fbf Refactored some core UI 2023-10-30 06:35:52 -07:00
Gregory Schier
6013cd2329 Plugin module loading 2023-10-29 20:50:23 -07:00
Gregory Schier
eba28ade48 Bump version 2023-10-29 17:22:27 -07:00
Gregory Schier
44af1ddc8a Fix sidebar scroll 2023-10-29 17:19:03 -07:00
Gregory Schier
63c0d09df8 A bit more playing with JS runtime 2023-10-29 17:05:48 -07:00
Gregory Schier
f305633d94 Initial "Hello World" for plugins 2023-10-29 16:43:28 -07:00
Gregory Schier
13155f8591 Fix request creation 2023-10-29 12:05:05 -07:00
Gregory Schier
f2ac97aa62 Restore recent environment on workspace change
Fixes #6
2023-10-29 11:32:55 -07:00
Gregory Schier
18eb0027a1 Fix var complete and env dialog actions 2023-10-29 11:18:55 -07:00
Gregory Schier
9e2803fcfb Remove broken key/value enter/backspace logic 2023-10-29 10:45:05 -07:00
Gregory Schier
705e30b6e0 Delete key/value on backspace 2023-10-29 10:26:38 -07:00
Gregory Schier
f1260911ea Move workspace menu, better env mgmt, QoL 2023-10-29 09:45:16 -07:00
Gregory Schier
076ff63dbe Bump version 2023-10-28 23:41:58 -07:00
Gregory Schier
899092b4d2 Better listening for path changes 2023-10-28 23:41:24 -07:00
Gregory Schier
c2c3a28aab Bump version 2023-10-28 22:14:51 -07:00
Gregory Schier
25c0db502e Fixed auto-focus in prompt and env dropdown 2023-10-28 22:14:12 -07:00
Gregory Schier
6dcbe45a53 Clear selected sidebar index on drag-drop end 2023-10-28 21:47:00 -07:00
Gregory Schier
e2b46f25ff Revert debug name 2023-10-28 21:43:09 -07:00
Gregory Schier
981182be46 Fix drag-n-drop things 2023-10-28 21:42:35 -07:00
Gregory Schier
ad164ebd5e Persist window paths 2023-10-28 21:23:46 -07:00
Gregory Schier
cacdad8826 Bump version to 2023.1.0 2023-10-28 19:15:33 -07:00
Gregory Schier
77e5142a7c Update placeholders when env changes 2023-10-28 19:14:51 -07:00
Gregory Schier
613081728d Placeholder error and fix env nav 2023-10-28 19:08:31 -07:00
Gregory Schier
23e77dfec1 Recent requests/workspaces. Closes #1 2023-10-28 18:46:54 -07:00
Gregory Schier
6e273ae2a3 Fix recent requests loading on startup 2023-10-28 18:27:18 -07:00
Gregory Schier
4061094988 Add tauri window save state plugin 2023-10-28 13:14:27 -07:00
Gregory Schier
82b185e27f Fix rustfmt 2023-10-28 12:45:25 -07:00
Gregory Schier
27dc261639 Handle enabled/disabled variables and render multi 2023-10-28 11:36:40 -07:00
Gregory Schier
7e45fecf19 Remove unused Variable type 2023-10-28 11:31:45 -07:00
Gregory Schier
1a5053380b Variables under Environment, and render all props 2023-10-28 11:29:29 -07:00
Gregory Schier
408665c62d Native Codemirror cursor 2023-10-27 13:14:41 -07:00
Gregory Schier
65efee2048 Only wrap URLBar on focus and hotkey to open recent requests 2023-10-27 12:40:43 -07:00
Gregory Schier
3faa66a1fc Resizing window no longer changes sidebar visibility
Fixes #4
2023-10-27 11:21:59 -07:00
Gregory Schier
9dafe4f704 Auto-expand URL bar height 2023-10-27 10:57:07 -07:00
Gregory Schier
356eaf1713 Environment deletion and better actions menu 2023-10-26 16:18:47 -07:00
Gregory Schier
f8584f1537 Stop autocomplete from jumping around 2023-10-26 15:27:48 -07:00
Gregory Schier
6ad6cb34b0 Fix request creation from menu 2023-10-26 10:41:14 -07:00
Gregory Schier
32b27cd780 Send requests with active environment 2023-10-26 10:32:06 -07:00
Gregory Schier
0344a1e8c9 Move create request and fix slow HTML highlighting 2023-10-26 09:42:19 -07:00
Gregory Schier
0515271c12 Better project selector, Fixes #2, and a bunch more 2023-10-26 09:11:44 -07:00
Gregory Schier
5ae8d54ce0 Fixed some routing and introspection requests 2023-10-25 21:53:18 -07:00
Gregory Schier
33c406ce49 Environments in URL and better rendering 2023-10-25 11:13:00 -07:00
Gregory Schier
3b660ddbd0 Move responses dropdown to separate component 2023-10-25 07:59:10 -07:00
Gregory Schier
3132728a27 Fix dialog height 2023-10-25 00:02:51 -07:00
Gregory Schier
7063128342 Better style when no active environment 2023-10-24 23:58:12 -07:00
Gregory Schier
2187775462 Environment dropdown and actions 2023-10-24 09:17:29 -07:00
Gregory Schier
18adcd1004 Started on environment edit dialog 2023-10-23 21:00:36 -07:00
Gregory Schier
b0656d1e38 Hacky implementation of variable autocomplete 2023-10-23 10:31:21 -07:00
Gregory Schier
38e66047e0 Rendered first variable! 2023-10-22 22:30:29 -07:00
Gregory Schier
c24f049dac Updating environments! 2023-10-22 22:06:51 -07:00
Gregory Schier
53d13c8172 Update .gitignore 2023-10-22 20:40:00 -07:00
Gregory Schier
0727c6e437 Prettier and start of env editor 2023-10-22 20:38:57 -07:00
Gregory Schier
8328d20150 Environments data model 2023-10-22 18:28:56 -07:00
Gregory Schier
afe6a3bf57 Environment data model backend 2023-10-22 16:05:09 -07:00
Gregory Schier
d920632cbd Fix some eslint warnings 2023-10-22 11:02:39 -07:00
Gregory Schier
5c456fd4d5 Add APPLE_TEAM_ID 2023-10-18 14:12:08 -07:00
Gregory Schier
38c247e350 Revert artifacts things 2023-10-18 13:25:35 -07:00
Gregory Schier
0c8f72124a Bump cargo deps 2023-10-18 13:25:20 -07:00
Gregory Schier
80ed6b1525 Bump version 2023-10-18 12:14:38 -07:00
Gregory Schier
4424b3f208 Fix sidebar drag-n-drop 2023-10-18 11:58:58 -07:00
Gregory Schier
2c75abce09 Retry button on introspection errors 2023-06-12 13:20:42 -07:00
Gregory Schier
4e15eb197f Fix autocomplete doc font size 2023-05-31 21:32:48 -07:00
Gregory Schier
a7544b4f8c Persist introspection queries and also improve 2023-05-31 21:29:41 -07:00
Gregory Schier
d126aad172 Update tauri NPM 2023-05-29 12:49:50 -07:00
Gregory Schier
acc5c0de50 Fix graphql instrospection 2023-05-29 12:31:34 -07:00
Gregory Schier
3391da111d Change version 2023-04-27 16:53:39 -07:00
Gregory Schier
e37ce96956 Version 1.0.0 2023-04-27 16:47:49 -07:00
Gregory Schier
c51831c975 Fix window methods on Linux/Windows 2023-04-27 16:27:02 -07:00
Gregory Schier
180aa39de4 Cross platform window controls 2023-04-27 10:19:49 -07:00
Gregory Schier
3bd780782e Fix ubuntu build 2023-04-26 17:00:30 -07:00
Gregory Schier
f9ba2f79c2 Windows and Linux 2023-04-26 16:54:51 -07:00
Gregory Schier
d9493de2be Bump version 2023-04-26 16:47:50 -07:00
Gregory Schier
bc9a623742 Very basic CSV viewer 2023-04-22 21:53:04 +08:00
Gregory Schier
532edbf274 Truncate response files 2023-04-14 14:15:33 -07:00
Gregory Schier
1585692328 Randomly offset new windows 2023-04-14 14:05:23 -07:00
Gregory Schier
083f565b12 Fix text encoding and delete responses 2023-04-14 13:50:41 -07:00
Gregory Schier
f7f7438c9e Delete response files 2023-04-14 12:17:11 -07:00
Gregory Schier
19934a93bb Readonly editor disable tabindex 2023-04-13 22:36:11 -07:00
Gregory Schier
577cfe5bdc Fix imageview padding 2023-04-13 22:33:47 -07:00
Gregory Schier
43ac6afae1 Duration and size tags 2023-04-13 20:50:17 -07:00
Gregory Schier
8cc11703d3 Comment 2023-04-13 18:55:32 -07:00
Gregory Schier
4f7a116378 Always store response on filesystem 2023-04-13 18:52:56 -07:00
Gregory Schier
513793d9ce Support binary responses! 2023-04-13 18:48:40 -07:00
Gregory Schier
67f32b6734 Blur backdrop 2023-04-11 16:12:26 -07:00
Gregory Schier
66813d67fe Autofocus buttons 2023-04-11 14:04:23 -07:00
Gregory Schier
a38691ed53 Better opening workspaces and redirect workspace to recent request 2023-04-11 11:11:36 -07:00
Gregory Schier
deeefdcfbf Button disabled style opacity 2023-04-10 16:03:45 -07:00
Gregory Schier
db292511b1 Dropdown keys and pointer events 2023-04-10 16:02:29 -07:00
Gregory Schier
1a5334c1ce Upgrade deno core 2023-04-10 11:16:25 -07:00
Gregory Schier
11002abe39 Tweak response history 2023-04-09 23:15:51 -07:00
Gregory Schier
d922dcb062 Fixed multi-window model sync 2023-04-09 22:32:47 -07:00
Gregory Schier
6fcaa18e86 Tweak recent requests 2023-04-09 22:25:00 -07:00
Gregory Schier
7664c941dd Toggle settings 2023-04-09 22:12:16 -07:00
Gregory Schier
6f5cb528c6 Fix sidebar request focus 2023-04-09 22:03:41 -07:00
Gregory Schier
ebb78922f0 More stuff on sidebar 2023-04-09 21:52:04 -07:00
Gregory Schier
2285fe9f1c Small tweaks 2023-04-09 15:32:13 -07:00
Gregory Schier
38ba8625d8 Request history navigator 2023-04-09 15:26:54 -07:00
Gregory Schier
ab5681c7ad Enter name on create workspace 2023-04-09 12:27:02 -07:00
Gregory Schier
f66dcb9267 Rename workspace 2023-04-09 12:23:41 -07:00
Gregory Schier
1b6cfbac77 Sidebar hover transitions 2023-04-06 16:30:46 -07:00
Gregory Schier
4c27e788ea Remove some more key value usage 2023-04-06 16:26:56 -07:00
Gregory Schier
769da0b052 A bunch of tweaks 2023-04-06 16:05:25 -07:00
Gregory Schier
6b60c86300 macOS 12 2023-04-06 08:39:30 -07:00
Gregory Schier
30c1b5e8c7 Remove system tray icon 2023-04-06 08:15:40 -07:00
Gregory Schier
10af9b6f99 Minor tweaks 2023-04-04 17:21:02 -07:00
Gregory Schier
aa8c066f2d Fix some things 2023-04-04 16:56:45 -07:00
Gregory Schier
b913b74449 Editor line wrapping support (not used yet) 2023-04-04 16:40:37 -07:00
Gregory Schier
b71adce50b remove janky last location tracking 2023-04-04 16:23:08 -07:00
Gregory Schier
0fbb44c701 Fix resize cursor 2023-04-04 16:12:45 -07:00
Gregory Schier
de335e8637 Better button styles 2023-04-04 15:40:25 -07:00
Gregory Schier
2999f63a4c Bump version 2023-04-04 13:56:24 -07:00
Gregory Schier
2abc5e6f0b Some small fixes 2023-04-04 13:56:14 -07:00
Gregory Schier
639de4321e A few fixes 2023-04-04 13:31:48 -07:00
Gregory Schier
b3c461afdd Better status tags and delete request on key 2023-04-04 12:36:30 -07:00
Gregory Schier
7d154800a0 Remove expects from request sending 2023-04-04 08:14:32 -07:00
Gregory Schier
b48ed0399e Fix web view height 2023-04-04 07:51:41 -07:00
Gregory Schier
c5d6e7d74a Fix autocomplete spacing 2023-04-04 07:51:19 -07:00
Gregory Schier
e82f915363 Fix input focus border 2023-04-03 12:19:37 -07:00
Gregory Schier
3128e9ce76 Hot keys and cleanup 2023-04-03 07:59:49 -07:00
Gregory Schier
bc0e86757c Add entitlemet for v8 2023-04-02 20:23:21 -07:00
Gregory Schier
fec99916c2 Debug codesigned build 2023-04-02 19:09:14 -07:00
Gregory Schier
3b5d059b11 Disable code signing 2023-04-02 18:27:14 -07:00
Gregory Schier
c3fe2acc8a Fix tauri script command 2023-04-02 17:25:24 -07:00
Gregory Schier
4d002c412b Fix universal binary 2023-04-02 17:12:20 -07:00
Gregory Schier
46d152b5f1 Bump version 2023-04-02 15:44:41 -07:00
Gregory Schier
25fa81ebbc Fix toolchain 2023-04-02 15:44:21 -07:00
Gregory Schier
7c2de3c360 Add proper target 2023-04-02 15:42:19 -07:00
Gregory Schier
3a3b187cd0 Try universal binary 2023-04-02 15:33:13 -07:00
Gregory Schier
3226bbe083 Fix version 2023-04-02 15:25:24 -07:00
Gregory Schier
a1e4e0e6c9 Bump version 2023-04-02 14:54:41 -07:00
Gregory Schier
b3aa8b893b Notorization (hopefully) 2023-04-02 14:53:49 -07:00
Gregory Schier
f057139634 Change tabs again 2023-04-02 11:11:53 -07:00
Gregory Schier
71a2b11ab4 Better response headers 2023-04-02 10:45:41 -07:00
Gregory Schier
587254a0e7 Show response headers 2023-04-01 23:43:22 -07:00
Gregory Schier
9f4de66f3c Some more refactoring 2023-04-01 21:48:30 -07:00
Gregory Schier
b0d8908724 Refactor debounce and tauri event listeners 2023-04-01 21:39:46 -07:00
Gregory Schier
15c22d98c6 Fix dropdown and dialog key handling 2023-04-01 21:04:39 -07:00
Gregory Schier
3105ae0edc Refactor sidebar display 2023-04-01 20:58:53 -07:00
Gregory Schier
11a89f06c1 Better GraphQL schema fetching 2023-04-01 17:53:36 -07:00
Gregory Schier
9cbe24e740 More eslint fixes 2023-04-01 15:48:37 -07:00
Gregory Schier
bfbed13b8f Add React hooks eslint 2023-04-01 15:26:57 -07:00
Gregory Schier
2268de6321 Fix Tauri listeners 2023-04-01 00:02:17 -07:00
Gregory Schier
dd99aa7fcd Memo editor 2023-03-31 23:19:15 -07:00
Gregory Schier
be436bb706 Fix request duplication 2023-03-31 22:54:32 -07:00
Gregory Schier
bd48726f44 Fix tauri listeners causing too many updates 2023-03-31 22:48:34 -07:00
Gregory Schier
10bea83f98 Remove import 2023-03-31 22:42:41 -07:00
Gregory Schier
8122b4fb84 Fix 2023-03-31 22:42:26 -07:00
Gregory Schier
3ae57fb2d8 Upgrade Deno 2023-03-31 22:42:08 -07:00
Gregory Schier
6dc3eecca4 Tweak 2023-03-31 16:14:25 -07:00
Gregory Schier
9d1d732154 Fix send hotkey 2023-03-31 16:13:34 -07:00
Gregory Schier
8a117415b7 Better schema fetching 2023-03-31 16:02:09 -07:00
Gregory Schier
d36623ebc9 Finally fix the editor! 2023-03-31 15:56:35 -07:00
Gregory Schier
94a3ae3696 Fix editor blurring bug! 2023-03-31 13:53:28 -07:00
Gregory Schier
2836a28988 Better model updates 2023-03-31 13:21:02 -07:00
Gregory Schier
946d7dc89e Fix text obscuring 2023-03-30 17:22:52 -07:00
Gregory Schier
af6300f18b Button ring colors 2023-03-30 17:17:07 -07:00
Gregory Schier
905cb4b18e Remove dummy button 2023-03-30 17:12:38 -07:00
Gregory Schier
305ed09547 Confirm deletions 2023-03-30 17:09:11 -07:00
Gregory Schier
643356bad3 Dedicated event for model creation 2023-03-30 16:49:49 -07:00
Gregory Schier
e458675627 Unify text selection color 2023-03-30 16:36:24 -07:00
Gregory Schier
91e3853692 Some icon tweaks 2023-03-30 16:29:14 -07:00
Gregory Schier
5f0876a136 Fix strict mode editor blur bug 2023-03-30 10:38:33 -07:00
Gregory Schier
3a38127fb4 Better tauri listeners and stuff 2023-03-30 09:05:54 -07:00
Gregory Schier
f3b6070235 Remove updated_by, remember last location 2023-03-30 08:11:51 -07:00
Gregory Schier
5e6e78eb9e Remove hardcoded window config 2023-03-29 22:16:21 -07:00
Gregory Schier
9b66a1d1a8 Fix build 2023-03-29 22:15:55 -07:00
Gregory Schier
e954d0d7bc Remove unused import 2023-03-29 21:53:49 -07:00
Gregory Schier
dab2df7e79 Better multi-window updates 2023-03-29 21:53:20 -07:00
Gregory Schier
bc40e22008 Fixed key/value stuff 2023-03-29 14:46:36 -07:00
Gregory Schier
eef262c398 Fix bundle parts 2023-03-29 14:00:34 -07:00
Gregory Schier
8eab6e14db Fix(ish) multiwindow updates 2023-03-29 11:15:37 -07:00
Gregory Schier
ded33a110a Obscure text 2023-03-29 10:16:51 -07:00
Gregory Schier
e448a7602a Simple auth schemes 2023-03-29 09:03:38 -07:00
Gregory Schier
4c22215ca5 Good start to multi-window 2023-03-28 18:29:40 -07:00
Gregory Schier
4f501abb72 Focus traps for dialog and dropdown 2023-03-26 23:07:09 -07:00
Gregory Schier
b2dcc38982 Confirmation Dialogs 2023-03-26 12:02:20 -07:00
Gregory Schier
11b719955b Floating sidebar 2023-03-26 10:09:28 -07:00
Gregory Schier
d563ac63db Panel icons 2023-03-25 23:29:04 -07:00
Gregory Schier
6d826064c6 Update dialog 2023-03-25 21:59:18 -07:00
Gregory Schier
d30b9d6518 Optimistically-update key values 2023-03-25 21:54:00 -07:00
Gregory Schier
8da3364d0f More tweaks 2023-03-25 21:40:14 -07:00
Gregory Schier
07c372b7f5 Animate dropdown 2023-03-25 21:36:17 -07:00
Gregory Schier
7e01f38253 Animate sidebar transition 2023-03-25 21:31:52 -07:00
Gregory Schier
ba637009a7 Refactor and improve layout resizing 2023-03-25 21:16:10 -07:00
Gregory Schier
da7388e510 Even better layouts 2023-03-25 18:33:01 -07:00
Gregory Schier
3ec88fc896 Better grid layouts 2023-03-25 18:12:09 -07:00
Gregory Schier
1c9381b2bd Global layout component 2023-03-25 13:26:31 -07:00
Gregory Schier
06349b8d5b Better dropdown separator 2023-03-25 11:06:05 -07:00
Gregory Schier
6dc7dc6ad2 Fix sidebar drag 2023-03-24 08:37:52 -07:00
Gregory Schier
f981a15ec3 Upgrade TYpescript 2023-03-23 15:37:36 -07:00
Gregory Schier
8b648c0301 Fix resize 2023-03-23 07:47:58 -07:00
Gregory Schier
83ce09075b Style tweak 2023-03-21 23:59:09 -07:00
Gregory Schier
168dfb9f6b GraphQL autocomplete and duplicate request 2023-03-21 23:54:45 -07:00
Gregory Schier
9b8961c23d Tweak sidebar drag resizer 2023-03-21 19:36:32 -07:00
Gregory Schier
89bca42ee6 Minor style tweaks 2023-03-21 18:31:05 -07:00
Gregory Schier
07d2a43a17 Pull out resize bar 2023-03-21 16:53:49 -07:00
Gregory Schier
c84f2afd09 Resize titlebar and tweak things 2023-03-21 16:42:52 -07:00
Gregory Schier
df4dbaecc8 Remove icon generation from build script 2023-03-21 14:21:07 -07:00
Gregory Schier
d9bf03cefe query client cache and better body types 2023-03-21 11:38:37 -07:00
Gregory Schier
39223e8d89 Fix workspace deletion 2023-03-21 09:32:15 -07:00
Gregory Schier
67925e18b2 Use proper gray for syntax 2023-03-20 17:15:12 -07:00
Gregory Schier
89ad65513d fix import 2023-03-20 17:13:14 -07:00
Gregory Schier
90166ddfa3 Minor tweaks 2023-03-20 17:12:19 -07:00
Gregory Schier
0981b23faf Fix URL bar spacing 2023-03-20 17:01:29 -07:00
Gregory Schier
664f3b4d87 Better radio dropdown type 2023-03-20 16:54:26 -07:00
Gregory Schier
dc97b91a4e Typesafe routing and CM line height issue 2023-03-20 16:47:36 -07:00
Gregory Schier
d310272d19 Better tab dropdown handling 2023-03-20 14:14:30 -07:00
Gregory Schier
f1be3f01e1 Fix request creation priority 2023-03-20 13:56:03 -07:00
Gregory Schier
c57b6e1d73 Remove log 2023-03-20 13:49:35 -07:00
Gregory Schier
a938dc45f0 Handle "no body" case 2023-03-20 13:49:21 -07:00
Gregory Schier
bb139744a1 Small fix 2023-03-20 13:37:14 -07:00
Gregory Schier
3aa3e09552 Fix pointer window drag 2023-03-20 13:34:49 -07:00
Gregory Schier
74abfd21b8 Fix extra dropdown element 2023-03-20 13:19:23 -07:00
Gregory Schier
e703817ba2 Remove most of Radix UI 2023-03-20 13:16:58 -07:00
Gregory Schier
80dd1e457b Better Header validation 2023-03-20 01:38:05 -07:00
Gregory Schier
ea9f8d3ab2 Tweak sidebar 2023-03-20 01:30:45 -07:00
Gregory Schier
fa222bdf12 Fix pair editor container 2023-03-20 01:18:44 -07:00
Gregory Schier
45b360dabd Fix input thingy 2023-03-20 01:14:13 -07:00
Gregory Schier
5923399359 Container queries! 2023-03-20 01:08:41 -07:00
Gregory Schier
f4600f3e90 Better pair editor delete button 2023-03-20 00:30:42 -07:00
Gregory Schier
f883837685 Pair validation 2023-03-20 00:17:29 -07:00
Gregory Schier
b58bc409f0 Don't send disabled headers 2023-03-20 00:05:19 -07:00
Gregory Schier
e893e539bb Small tweak 2023-03-20 00:04:40 -07:00
Gregory Schier
90294fbb5d Pair checkboxes and fix twig indent 2023-03-20 00:03:33 -07:00
Gregory Schier
ae65f222bc Rewrote twig grammar 2023-03-19 22:12:11 -07:00
Gregory Schier
1b9813fb4c Re-order of pair editor 2023-03-19 13:28:57 -07:00
Gregory Schier
b708b5ae41 Better header editor and added completion data 2023-03-19 11:09:21 -07:00
Gregory Schier
df136fa915 A couple tweaks 2023-03-19 01:01:13 -07:00
Gregory Schier
f8329f5b8d Persist sort priority! 2023-03-19 00:48:09 -07:00
Gregory Schier
21141090de Create new workspace, and more optimizations 2023-03-18 19:36:31 -07:00
Gregory Schier
c0d9740a7d Optimized a few components 2023-03-18 18:49:01 -07:00
Gregory Schier
afcf630443 Fix sidebar drag-n-drop 2023-03-18 18:09:36 -07:00
Gregory Schier
1fe2c9826a Got drag opacity working 2023-03-18 15:06:38 -07:00
Gregory Schier
7272b80a3f Good start to drag-n-drop sidebar! 2023-03-18 14:41:07 -07:00
Gregory Schier
92114b7368 Fix mixed parser 2023-03-17 17:57:43 -07:00
Gregory Schier
f39d3e7eed Dropdown highlight 2023-03-17 17:32:24 -07:00
Gregory Schier
cbe0d27a5e Beginnings of autocomplete for headers 2023-03-17 16:51:20 -07:00
Gregory Schier
cd39699467 Flatten migrations, kvs lib, fix tabs 2023-03-17 08:36:21 -07:00
Gregory Schier
b3ea67aacf Sidebar item dropdown 2023-03-16 15:37:53 -07:00
Gregory Schier
db4ed9797c Sidebar dragging 2023-03-16 14:34:49 -07:00
Gregory Schier
1ea7d7d685 Add devtools toggle hotkey 2023-03-16 11:25:38 -07:00
Gregory Schier
2df725b57a Adjust window sizes 2023-03-16 11:16:23 -07:00
Gregory Schier
74e6648249 Store appearance in k/v 2023-03-16 11:01:30 -07:00
Gregory Schier
1026350d9c Hotkeys and view mode kvs 2023-03-16 09:24:28 -07:00
Gregory Schier
98fb87874d Some fixes 2023-03-15 23:33:46 -07:00
Gregory Schier
41fc3afdc1 Got key values working 2023-03-15 23:24:41 -07:00
Gregory Schier
83dbf46ba4 Fix editor padding 2023-03-15 17:29:35 -07:00
Gregory Schier
0b2e35bdde Minor style updates 2023-03-15 17:25:04 -07:00
Gregory Schier
d90a7331c9 Add stuff to app header 2023-03-15 16:35:19 -07:00
Gregory Schier
264e64a996 Better request delete and formatting 2023-03-15 09:41:38 -07:00
Gregory Schier
8915915c47 Fix graphql and other things 2023-03-15 09:06:56 -07:00
Gregory Schier
951ed787fa Header editor to pair editor 2023-03-15 08:09:45 -07:00
Gregory Schier
64ef6b0c22 Better header editor 2023-03-15 07:54:04 -07:00
Gregory Schier
ef18377b3c Strict mode and tweak layout padding 2023-03-14 20:19:45 -07:00
Gregory Schier
5904b6fded Add GraphQL variables editor 2023-03-14 19:56:02 -07:00
Gregory Schier
f4401e77bb GraphQL query editor transformer works! 2023-03-14 19:08:18 -07:00
Gregory Schier
efa5455a7b Add body type to request and tab dropdown 2023-03-14 11:18:56 -07:00
Gregory Schier
619c8d9e72 Improved header editor 2023-03-14 00:54:41 -07:00
Gregory Schier
bdf89ac288 Fix platform check 2023-03-14 00:15:01 -07:00
Gregory Schier
debd3c8185 Some small changes 2023-03-14 00:08:03 -07:00
Gregory Schier
f81a3ae8e7 Move stuff around 2023-03-13 23:30:14 -07:00
Gregory Schier
7d4e9894c3 Refactor hooks to be easier to use 2023-03-13 23:25:41 -07:00
Gregory Schier
4bf22d8a60 Fix header editor and scroll in general 2023-03-13 19:37:36 -07:00
Gregory Schier
8be4971a23 Lazy load routes 2023-03-13 13:56:13 -07:00
Gregory Schier
359e916b73 Back to React 2023-03-13 09:50:49 -07:00
Gregory Schier
68058f3e41 Move some stuff around 2023-03-13 09:24:38 -07:00
Gregory Schier
0c6fa3e634 Fix URL bar 2023-03-13 00:13:25 -07:00
Gregory Schier
0fa25c6335 Fix ButtonLink and edit request names 2023-03-13 00:11:23 -07:00
Gregory Schier
5684479f1d Remove old rust cache action 2023-03-12 22:48:43 -07:00
Gregory Schier
2d1603601c Better rust cache 2023-03-12 22:47:43 -07:00
Gregory Schier
f5394b2210 Start GraphQL support 2023-03-12 22:43:25 -07:00
Gregory Schier
833db5df06 Fix artifact tag 2023-03-12 21:41:15 -07:00
Gregory Schier
525ac7e980 Remove wasm stuff 2023-03-12 21:25:31 -07:00
Gregory Schier
44a747c80a Use tauri action 2023-03-12 21:13:08 -07:00
Gregory Schier
2056e7f40a Fix traffic lights thingy 2023-03-12 20:47:52 -07:00
Gregory Schier
9b6c1ad364 Cache cargo bin for "install" 2023-03-12 19:10:39 -07:00
Gregory Schier
34987bcacb Refformat 2023-03-12 19:03:27 -07:00
Gregory Schier
b62c11222a Fix artifact upload 2023-03-12 19:01:48 -07:00
Gregory Schier
b3cee3ace3 Fix dev 2023-03-12 18:39:02 -07:00
Gregory Schier
222c054c95 Split out macos deps 2023-03-12 18:36:25 -07:00
Gregory Schier
46f18a2491 Cache workflow 2023-03-12 18:28:14 -07:00
Gregory Schier
f2ca8e2753 Add wasm-pack 2023-03-12 18:19:20 -07:00
Gregory Schier
b0d243c378 Install rsw 2023-03-12 18:14:38 -07:00
Gregory Schier
6161fb86c8 Fix artifact names 2023-03-12 18:13:00 -07:00
Gregory Schier
b09cc91fe5 Fix build command 2023-03-12 18:11:24 -07:00
Gregory Schier
ef1638cbb3 Update secrets context 2023-03-12 18:07:57 -07:00
Gregory Schier
00ef8743f2 Update workflow name 2023-03-12 18:05:45 -07:00
Gregory Schier
68222659e3 Fix workflow 2023-03-12 18:05:13 -07:00
Gregory Schier
69420a4bba Start of auto updates 2023-03-12 18:04:11 -07:00
Gregory Schier
0161bbaeb1 Fix tabbing to tabs 2023-03-11 23:32:39 -08:00
Gregory Schier
948dbfe3cc Fix eslint errors 2023-03-11 23:29:25 -08:00
Gregory Schier
338ba8b189 Got tab content scrolling working 2023-03-11 22:36:13 -08:00
Gregory Schier
ca4655b441 Removed some debug stuff 2023-03-10 10:43:15 -08:00
Gregory Schier
bf37499428 Refactor editor to update better 2023-03-10 10:39:23 -08:00
Gregory Schier
0b94b57e2a Fix headers persistence and better sending 2023-03-09 13:38:17 -08:00
Gregory Schier
fc40aead98 Hook up header editor! 2023-03-09 13:07:13 -08:00
Gregory Schier
7d7f934e6a Fix 2023-03-09 10:58:27 -08:00
Gregory Schier
d5fbf4d622 Fix blur de-select speed 2023-03-09 10:57:34 -08:00
Gregory Schier
e4f6c919dc Fix Codemirror performance!! 2023-03-09 10:50:55 -08:00
Gregory Schier
4d806ff2b1 Switch to Preact!!! 2023-03-09 00:47:25 -08:00
Gregory Schier
bf8f12274f Move some things around 2023-03-08 23:20:15 -08:00
Gregory Schier
f4f438d9fe Better scrollbar color 2023-03-08 19:23:24 -08:00
Gregory Schier
2434f373be Zoom, better sizes, color picker, sidebar footer 2023-03-08 19:22:04 -08:00
Gregory Schier
2bb2061f97 Read-only editor 2023-03-08 16:53:13 -08:00
Gregory Schier
2c011a5c2a More theme tweaks 2023-03-08 16:37:20 -08:00
Gregory Schier
f66b0ccea1 Debounce autocomplete 2023-03-08 11:25:20 -08:00
Gregory Schier
665dd8447d Minor theme updates again 2023-03-08 09:43:35 -08:00
Gregory Schier
1b61ce31e6 Editor tweaks 2023-03-07 23:05:33 -08:00
Gregory Schier
ef4d960698 Remove unneeded space 2023-03-07 22:58:13 -08:00
Gregory Schier
b6d557b632 Fix small view 2023-03-07 22:55:51 -08:00
Gregory Schier
b700bd356c Minor style tweaks 2023-03-07 22:21:58 -08:00
Gregory Schier
620dd7d3ef Lots more theme stuff 2023-03-07 21:52:21 -08:00
Gregory Schier
6575121902 Start of themes 2023-03-07 11:24:38 -08:00
Gregory Schier
7c1755a0dc More subtle layout tweaks 2023-03-06 08:57:57 -08:00
Gregory Schier
8ad301a666 More layout fiddling and error page 2023-03-04 22:26:00 -08:00
Gregory Schier
ae24cd4939 More work on the layout 2023-03-04 21:51:17 -08:00
Gregory Schier
7152e1845e Try new layout and a bunch of editor fixes 2023-03-04 19:06:12 -08:00
Gregory Schier
96c1dd4081 Fix autocomplete inside dialog 2023-03-03 17:03:20 -08:00
Gregory Schier
87c7b3a663 Beginnings of Header Editor 2023-03-03 13:18:57 -08:00
Gregory Schier
c1be46a539 Fix tailwind dark selector 2023-03-03 07:54:19 -08:00
Gregory Schier
4655e0018b Fix content type in URL 2023-03-02 23:17:09 -08:00
Gregory Schier
da5ba2e3be Add Dialog component 2023-03-02 18:46:10 -08:00
Gregory Schier
aaf95f565f More colors 2023-03-02 17:56:53 -08:00
Gregory Schier
f32b984e77 Minor style tweaks 2023-03-02 16:16:41 -08:00
Gregory Schier
548aa4c7cd Improved autocompletion! 2023-03-02 11:14:51 -08:00
Gregory Schier
0ccceaac77 Rename, fix autocomplete and singleline, etc... 2023-03-02 10:42:43 -08:00
Gregory Schier
70f534f1d8 Editor placeholder 2023-03-01 14:22:10 -08:00
Gregory Schier
61fe95b300 Some minor bugs 2023-03-01 14:16:02 -08:00
Gregory Schier
915e0e8613 Fix migrations for build and iframe rendering 2023-03-01 10:31:50 -08:00
Gregory Schier
aace2580da Tweaks 2023-03-01 10:19:21 -08:00
Gregory Schier
3d36905664 Response streaming 2023-03-01 09:05:00 -08:00
Gregory Schier
0d671423da Autocomplete, and more CM stuff! 2023-02-28 22:54:54 -08:00
Gregory Schier
aebfcb9437 Some small tweaks 2023-02-28 17:25:59 -08:00
Gregory Schier
be7ef7beb1 Better editor updating 2023-02-28 12:41:03 -08:00
Gregory Schier
d77ed0c5cc URL highlighting with inline CM 2023-02-28 11:26:26 -08:00
Gregory Schier
e57e7bcec5 Implement request deletion 2023-02-27 15:42:06 -08:00
Gregory Schier
a637842ce4 Tauri events for request model updates 2023-02-27 13:28:50 -08:00
Gregory Schier
fc54ec49af Split request upsert command 2023-02-27 10:00:57 -08:00
Gregory Schier
5c43d8510a Add toggle for pretty view 2023-02-27 09:08:48 -08:00
Gregory Schier
83f84ded8d Small tweaks 2023-02-26 15:25:55 -08:00
Gregory Schier
5658da34a2 Add variable highlighting widgets 2023-02-26 15:06:14 -08:00
Gregory Schier
38e8ef6535 Dropdown scrolling 2023-02-25 23:33:07 -08:00
Gregory Schier
8c89b06238 Show response body size 2023-02-25 23:08:19 -08:00
Gregory Schier
d85c021305 A bunch more small things 2023-02-25 23:04:31 -08:00
Gregory Schier
83bb18df03 Added react-router 2023-02-25 18:04:14 -08:00
Gregory Schier
93105a3e89 Migrations and initial data stuff 2023-02-25 16:39:18 -08:00
Gregory Schier
ba3b899115 Minor tweaks 2023-02-24 17:01:48 -08:00
Gregory Schier
fcfbc1d1da Dummy requests in sidebar 2023-02-24 16:46:56 -08:00
Gregory Schier
72486b448c Codemirror initial value support 2023-02-24 16:43:47 -08:00
Gregory Schier
7dea1b7870 Send request body 2023-02-24 16:09:19 -08:00
Gregory Schier
4de2c496c9 Vendor basicSetup 2023-02-24 14:51:56 -08:00
Gregory Schier
9e1393a392 Additional methods and tweaks 2023-02-24 14:10:25 -08:00
Gregory Schier
0901690ed6 Focus states 2023-02-24 12:35:13 -08:00
Gregory Schier
95303648cc Hook up theme and clear responses 2023-02-24 12:13:30 -08:00
Gregory Schier
1dbb08c045 SQLite store in proper dir 2023-02-22 20:18:14 -08:00
Gregory Schier
00a7d9a180 Started on grid layout 2023-02-22 19:44:44 -08:00
Gregory Schier
6c549dc086 Save responses in DB 2023-02-22 18:53:44 -08:00
Gregory Schier
dc368e326a Better URL bar 2023-02-22 16:15:25 -08:00
Gregory Schier
e42188a627 Cleaner URL bar and some improvements 2023-02-22 15:58:04 -08:00
Gregory Schier
7a6a337eff Refactor classname usage 2023-02-21 18:03:57 -08:00
Gregory Schier
3907344884 Some minor tweaks 2023-02-21 17:56:48 -08:00
261 changed files with 20010 additions and 9520 deletions

View File

@@ -1,25 +1,37 @@
module.exports = {
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:import/recommended',
'plugin:jsx-a11y/recommended',
'plugin:@typescript-eslint/recommended',
'eslint-config-prettier',
],
ignorePatterns: ['src-tauri/**/*'],
settings: {
react: {
version: 'detect',
},
'import/resolver': {
node: {
paths: ['src-web'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
rules: {
"react/react-in-jsx-scope": "off",
extends: [
"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",
parserOptions: {
project: ["./tsconfig.json"]
},
ignorePatterns: ["src-tauri/**/*", "plugins/**/*"],
settings: {
react: {
version: "detect"
},
"import/resolver": {
node: {
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"
}]
}
};

71
.github/workflows/artifacts.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
name: Generate Artifacts
on:
push:
tags: [ v* ]
permissions: write-all
jobs:
build-artifacts:
strategy:
fail-fast: false
matrix:
include:
- os: macos-12
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
- os: windows-2022
target: x86_64-pc-windows-msvc
- os: ubuntu-20.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Cache Rust
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
./src-tauri/target
key: ${{ runner.os }}-cargo-${{ hashFiles('src-tauri/Cargo.lock') }}
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- name: install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-20.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
# Pin dev version to get non-default targets
# https://github.com/tauri-apps/tauri-action/issues/356
- uses: tauri-apps/tauri-action@dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
with:
tagName: 'v__VERSION__'
releaseName: 'Release __VERSION__'
releaseBody: '<!-- Release Notes -->'
releaseDraft: true
prerelease: false
args: '--target ${{ matrix.target }}'

3
.gitignore vendored
View File

@@ -22,5 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.eslintcache
.rsw
*.sqlite

1
.husky/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

2
.nvmrc
View File

@@ -1 +1 @@
18
20

12
.run/Dev Desktop.run.xml Normal file
View File

@@ -0,0 +1,12 @@
<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="tauri-dev" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>

5
.sqllsrc.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "yaak-dev",
"adapter": "sqlite3",
"filename": "src-tauri/db.sqlite"
}

13
Makefile Normal file
View File

@@ -0,0 +1,13 @@
.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

View File

@@ -1,3 +1,16 @@
# Tauri REST Client
# Yaak Network Toolkit
It's a REST client, yo.
The most fun you'll ever have working with APIs.
## Common Commands
```sh
# Start dev app
npm run tauri-dev
# 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'
```

BIN
design/Icons.afdesign Normal file
View File

Binary file not shown.

BIN
design/logo.afdesign Normal file
View File

Binary file not shown.

View File

@@ -1,15 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + React + TS</title>
</head>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Yaak App</title>
<!-- <script src="http://localhost:8097"></script>-->
<style>
body {
background-color: white;
}
<body>
<div id="root"></div>
<div id="radix-portal"></div>
<script type="module" src="/src-web/main.tsx"></script>
</body>
@media (prefers-color-scheme: dark) {
body {
background-color: black;
}
}
</style>
</head>
<body>
<div id="root"></div>
<div id="cm-portal" class="cm-portal"></div>
<div id="react-portal"></div>
<div id="radix-portal" class="cm-portal"></div>
<script type="module" src="/src-web/main.tsx"></script>
</body>
</html>

10732
package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,91 @@
{
"name": "tauri-app",
"name": "yaak-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "rsw build && tsc && vite build",
"dev": "vite",
"lint": "eslint . --ext .ts,.tsx",
"preview": "vite preview",
"tauri-dev": "concurrently -n app,rsw \"tauri dev\" \"rsw watch\""
"tauri-dev": "YAAK_ENV=development tauri dev",
"tauri-build": "tauri build",
"tauri": "tauri",
"build": "npm run build:frontend",
"dev": "vite dev",
"lint": "tsc && eslint . --ext .ts,.tsx",
"build:icon": "tauri icon src-tauri/icons/icon.png",
"build:frontend": "vite build",
"test": "vitest",
"coverage": "vitest run --coverage",
"prepare": "husky install"
},
"dependencies": {
"@codemirror/lang-html": "^6.4.2",
"@codemirror/commands": "^6.2.1",
"@codemirror/lang-javascript": "^6.1.4",
"@codemirror/lang-json": "^6.0.1",
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@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",
"@radix-ui/react-icons": "^1.2.0",
"@radix-ui/react-popover": "1.0.3",
"@tauri-apps/api": "^1.2.0",
"@typescript-eslint/eslint-plugin": "^5.52.0",
"@typescript-eslint/parser": "^5.52.0",
"@react-hook/resize-observer": "^1.2.6",
"@tailwindcss/container-queries": "^0.1.0",
"@tanstack/query-sync-storage-persister": "^4.27.1",
"@tanstack/react-query": "^4.28.0",
"@tanstack/react-query-devtools": "^4.28.0",
"@tanstack/react-query-persist-client": "^4.28.0",
"@tauri-apps/api": "^1.5.1",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"cm6-graphql": "^0.0.9",
"codemirror": "^6.0.1",
"focus-trap-react": "^10.1.1",
"format-graphql": "^1.4.0",
"framer-motion": "^9.0.4",
"papaparse": "^5.4.1",
"parse-color": "^1.0.0",
"react": "^18.2.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0",
"react-router-dom": "^6.8.1",
"react-use": "^17.4.0",
"uuid": "^9.0.0"
},
"devDependencies": {
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tauri-apps/cli": "^1.5.4",
"@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": "^5.57.0",
"@typescript-eslint/parser": "^5.57.0",
"@vitejs/plugin-react": "^3.1.0",
"autoprefixer": "^10.4.13",
"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",
"framer-motion": "^9.0.4",
"prettier": "^2.8.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0"
},
"devDependencies": {
"@tauri-apps/cli": "^1.2.2",
"@types/node": "^18.7.10",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^3.0.0",
"autoprefixer": "^10.4.13",
"concurrently": "^7.6.0",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^8.0.3",
"lint-staged": "^15.0.2",
"postcss": "^8.4.21",
"postcss-nesting": "^11.2.1",
"prettier": "^2.8.4",
"tailwindcss": "^3.2.7",
"typescript": "^4.6.4",
"typescript": "^5.0.2",
"vite": "^4.0.0",
"vite-plugin-rsw": "^2.0.11",
"vite-plugin-top-level-await": "^1.2.4"
"vite-plugin-svgr": "^2.4.0",
"vite-plugin-top-level-await": "^1.2.4",
"vitest": "^0.29.2"
},
"lint-staged": {
"*.{ts,tsx}": "eslint --cache --fix",
"*.{js,css,md}": "prettier --write"
}
}

View File

@@ -1,6 +1,7 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
plugins: [
require('@tailwindcss/nesting')(require('postcss-nesting')),
require('tailwindcss'),
require('autoprefixer'),
]
}

View File

@@ -1,39 +0,0 @@
name = "rsw"
version = "0.1.0"
#! time interval for file changes to trigger wasm-pack build, default `50` milliseconds
interval = 50
#! link
#! npm link @see https://docs.npmjs.com/cli/v8/commands/npm-link
#! yarn link @see https://classic.yarnpkg.com/en/docs/cli/link
#! pnpm link @see https://pnpm.io/cli/link
#! The link command will only be executed if `[[crates]] link = true`
#! cli: `npm` | `yarn` | `pnpm`, default is `npm`
cli = "npm"
#! ---------------------------
#! rsw new <name>
[new]
#! @see https://rustwasm.github.io/docs/wasm-pack/commands/new.html
#! using: `wasm-pack` | `rsw` | `user`, default is `wasm-pack`
#! 1. wasm-pack: `rsw new <name> --template <template> --mode <normal|noinstall|force>`
#! 2. rsw: `rsw new <name>`, built-in templates
#! 3. user: `rsw new <name>`, if `dir` is not configured, use `wasm-pack new <name>` to initialize the project
using = "wasm-pack"
#! this field needs to be configured when `using = "user"`
#! `using = "wasm-pack"` or `using = "rsw"`, this field will be ignored
#! copy all files in this directory
dir = "my-template"
#! ################# NPM Package #################
#! When there is only `name`, other fields will use the default configuration
#! 📦 -------- package: @rsw --------
[[crates]]
#! npm package name (path: $ROOT/@rsw)
name = "src-wasm/hello"
#! run `npm link`: `true` | `false`, default is `false`
link = true

1
rustfmt.toml Normal file
View File

@@ -0,0 +1 @@
edition = "2018"

3762
src-tauri/Cargo.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,50 @@
[package]
name = "tauri-app"
name = "yaak-app"
version = "0.0.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
description = "A network protocol testing utility app"
authors = ["Gregory Schier"]
license = "MIT"
repository = "https://github.com/gschier/yaak-app"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[profile.release]
strip = true # Automatically strip symbols from the binary.
[build-dependencies]
tauri-build = { version = "1.2", features = [] }
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7"
cocoa = "0.25.0"
[dependencies]
serde_json = { version = "1.0" }
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2", features = ["config-toml", "devtools", "shell-open", "system-tray", "window-start-dragging"] }
http = { version = "0.2.8" }
base64 = "0.21.0"
boa_engine = "0.17.3"
boa_runtime = "0.17.3"
chrono = { version = "0.4.23", features = ["serde"] }
futures = "0.3.26"
http = "0.2.8"
rand = "0.8.5"
reqwest = { version = "0.11.14", features = ["json"] }
tokio = { version = "1.25.0", features = ["full"] }
futures = { version = "0.3.26" }
deno_core = { version = "0.171.0" }
deno_ast = { version = "0.24.0", features = ["transpiling"] }
objc = { version = "0.2.7" }
cocoa = { version = "0.24.1" }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time", "offline"] }
tauri = { version = "1.3", features = [
"cli",
"config-toml",
"devtools",
"fs-read-file",
"os-all",
"protocol-asset",
"shell-open",
"system-tray",
"updater",
"window-start-dragging",
"dialog-open",
] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
tokio = { version = "1.25.0", features = ["sync"] }
uuid = "1.3.0"
[features]
# by default Tauri runs in production mode

View File

@@ -1,3 +1,3 @@
fn main() {
tauri_build::build()
tauri_build::build()
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Binary file not shown.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,65 @@
CREATE TABLE key_values
(
model TEXT DEFAULT 'key_value' NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
namespace TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (namespace, key)
);
CREATE TABLE workspaces
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'workspace' NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
name TEXT NOT NULL,
description TEXT NOT NULL
);
CREATE TABLE http_requests
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'http_request' NOT NULL,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
name TEXT NOT NULL,
url TEXT NOT NULL,
method TEXT NOT NULL,
headers TEXT NOT NULL,
body TEXT,
body_type TEXT
);
CREATE TABLE http_responses
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'http_response' NOT NULL,
request_id TEXT NOT NULL
REFERENCES http_requests
ON DELETE CASCADE,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
elapsed INTEGER NOT NULL,
status INTEGER NOT NULL,
status_reason TEXT,
url TEXT NOT NULL,
body TEXT NOT NULL,
headers TEXT NOT NULL,
error TEXT
);

View File

@@ -0,0 +1 @@
ALTER TABLE main.http_requests ADD COLUMN sort_priority REAL NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,2 @@
ALTER TABLE http_requests ADD COLUMN authentication TEXT NOT NULL DEFAULT '{}';
ALTER TABLE http_requests ADD COLUMN authentication_type TEXT;

View File

@@ -0,0 +1,5 @@
DELETE FROM main.http_responses;
ALTER TABLE http_responses DROP COLUMN body;
ALTER TABLE http_responses ADD COLUMN body BLOB;
ALTER TABLE http_responses ADD COLUMN body_path TEXT;
ALTER TABLE http_responses ADD COLUMN content_length INTEGER;

View File

@@ -0,0 +1,15 @@
CREATE TABLE environments
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'workspace' NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
name TEXT NOT NULL,
data TEXT NOT NULL
DEFAULT '{}'
);

View File

@@ -0,0 +1,2 @@
ALTER TABLE environments DROP COLUMN data;
ALTER TABLE environments ADD COLUMN variables DEFAULT '[]' NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE workspaces ADD COLUMN variables DEFAULT '[]' NOT NULL;

View File

@@ -0,0 +1,19 @@
CREATE TABLE folders
(
id TEXT NOT NULL
PRIMARY KEY,
model TEXT DEFAULT 'folder' NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
workspace_id TEXT NOT NULL
REFERENCES workspaces
ON DELETE CASCADE,
folder_id TEXT NULL
REFERENCES folders
ON DELETE CASCADE,
name TEXT NOT NULL,
sort_priority REAL DEFAULT 0 NOT NULL
);
ALTER TABLE http_requests ADD COLUMN folder_id TEXT REFERENCES folders(id) ON DELETE CASCADE;

View File

@@ -0,0 +1,4 @@
export function greet() {
// Call Rust-provided fn!
sayHello('Plugin');
}

View File

@@ -0,0 +1,7 @@
import { greet } from './greet.js';
export function hello() {
greet();
console.log('Try JSON parse', JSON.parse(`{ "hello": 123 }`).hello);
console.log('Try RegExp', '123'.match(/[\d]+/));
}

View File

@@ -0,0 +1,156 @@
function O(e, t) {
return (
console.log('IMPORTING Environment', e._id, e.name, JSON.stringify(e, null, 2)),
{
id: e._id,
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
workspaceId: t,
model: 'environment',
name: e.name,
variables: Object.entries(e.data).map(([i, s]) => ({
enabled: !0,
name: i,
value: `${s}`,
})),
}
);
}
function g(e) {
return d(e) && e._type === 'workspace';
}
function y(e) {
return d(e) && e._type === 'request_group';
}
function _(e) {
return d(e) && e._type === 'request';
}
function I(e) {
return d(e) && e._type === 'environment';
}
function d(e) {
return Object.prototype.toString.call(e) === '[object Object]';
}
function h(e) {
return Object.prototype.toString.call(e) === '[object String]';
}
function N(e) {
return Object.entries(e).map(([t, i]) => ({
enabled: !0,
name: t,
value: `${i}`,
}));
}
function c(e) {
return h(e) ? e.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') : e;
}
function D(e, t, i = 0) {
var a, u;
console.log('IMPORTING REQUEST', e._id, e.name, JSON.stringify(e, null, 2));
let s = null,
n = null;
((a = e.body) == null ? void 0 : a.mimeType) === 'application/graphql'
? ((s = 'graphql'), (n = c(e.body.text)))
: ((u = e.body) == null ? void 0 : u.mimeType) === 'application/json' &&
((s = 'application/json'), (n = c(e.body.text)));
let p = null,
o = {};
return (
e.authentication.type === 'bearer'
? ((p = 'bearer'),
(o = {
token: c(e.authentication.token),
}))
: e.authentication.type === 'basic' &&
((p = 'basic'),
(o = {
username: c(e.authentication.username),
password: c(e.authentication.password),
})),
{
id: e._id,
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
workspaceId: t,
folderId: e.parentId === t ? null : e.parentId,
model: 'http_request',
sortPriority: i,
name: e.name,
url: c(e.url),
body: n,
bodyType: s,
authentication: o,
authenticationType: p,
method: e.method,
headers: (e.headers ?? []).map(({ name: m, value: r, disabled: f }) => ({
enabled: !f,
name: m,
value: r,
})),
}
);
}
function w(e, t) {
return (
console.log('IMPORTING Workspace', e._id, e.name, JSON.stringify(e, null, 2)),
{
id: e._id,
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
model: 'workspace',
name: e.name,
variables: t,
}
);
}
function b(e, t) {
return (
console.log('IMPORTING FOLDER', e._id, e.name, JSON.stringify(e, null, 2)),
{
id: e._id,
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
folderId: e.parentId === t ? null : e.parentId,
workspaceId: t,
model: 'folder',
name: e.name,
}
);
}
function T(e) {
const t = JSON.parse(e);
if (!d(t)) return;
const { _type: i, __export_format: s } = t;
if (i !== 'export' || s !== 4 || !Array.isArray(t.resources)) return;
const n = {
workspaces: [],
requests: [],
environments: [],
folders: [],
},
p = t.resources.filter(g);
for (const o of p) {
console.log('IMPORTING WORKSPACE', o.name);
const a = t.resources.find((r) => I(r) && r.parentId === o._id);
console.log('FOUND BASE ENV', a.name),
n.workspaces.push(w(o, a ? N(a.data) : [])),
console.log('IMPORTING ENVIRONMENTS', a.name);
const u = t.resources.filter((r) => I(r) && r.parentId === (a == null ? void 0 : a._id));
console.log('FOUND', u.length, 'ENVIRONMENTS'),
n.environments.push(...u.map((r) => O(r, o._id)));
const m = (r) => {
const f = t.resources.filter((l) => l.parentId === r);
let S = 0;
for (const l of f)
y(l) ? (n.folders.push(b(l, o._id)), m(l._id)) : _(l) && n.requests.push(D(l, o._id, S++));
};
m(o._id);
}
return (
(n.requests = n.requests.filter(Boolean)),
(n.environments = n.environments.filter(Boolean)),
(n.workspaces = n.workspaces.filter(Boolean)),
n
);
}
export { T as pluginHookImport };

View File

@@ -0,0 +1,23 @@
export function isWorkspace(obj) {
return isJSObject(obj) && obj._type === 'workspace';
}
export function isRequestGroup(obj) {
return isJSObject(obj) && obj._type === 'request_group';
}
export function isRequest(obj) {
return isJSObject(obj) && obj._type === 'request';
}
export function isEnvironment(obj) {
return isJSObject(obj) && obj._type === 'environment';
}
export function isJSObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
}
export function isJSString(obj) {
return Object.prototype.toString.call(obj) === '[object String]';
}

View File

@@ -0,0 +1,18 @@
import { isJSString } from './types.js';
export function parseVariables(data) {
return Object.entries(data).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`,
}));
}
/**
* Convert Insomnia syntax to Yaak syntax
* @param {string} variable - Text to convert
*/
export function convertSyntax(variable) {
if (!isJSString(variable)) return variable;
return variable.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}');
}

View File

@@ -0,0 +1,21 @@
/**
* Import an Insomnia environment object.
* @param {Object} e - The environment object to import.
* @param workspaceId - Workspace to import into.
*/
export function importEnvironment(e, workspaceId) {
console.log('IMPORTING Environment', e._id, e.name, JSON.stringify(e, null, 2));
return {
id: e._id,
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
workspaceId,
model: 'environment',
name: e.name,
variables: Object.entries(e.data).map(([name, value]) => ({
enabled: true,
name,
value: `${value}`,
})),
};
}

View File

@@ -0,0 +1,17 @@
/**
* Import an Insomnia folder object.
* @param {Object} f - The environment object to import.
* @param workspaceId - Workspace to import into.
*/
export function importFolder(f, workspaceId) {
console.log('IMPORTING FOLDER', f._id, f.name, JSON.stringify(f, null, 2));
return {
id: f._id,
createdAt: new Date(f.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(f.updated ?? Date.now()).toISOString().replace('Z', ''),
folderId: f.parentId === workspaceId ? null : f.parentId,
workspaceId,
model: 'folder',
name: f.name,
};
}

View File

@@ -0,0 +1,58 @@
import { convertSyntax } from '../helpers/variables.js';
/**
* Import an Insomnia request object.
* @param {Object} r - The request object to import.
* @param workspaceId - The workspace ID to use for the request.
* @param {number} sortPriority - The sort priority to use for the request.
*/
export function importRequest(r, workspaceId, sortPriority = 0) {
console.log('IMPORTING REQUEST', r._id, r.name, JSON.stringify(r, null, 2));
let bodyType = null;
let body = null;
if (r.body?.mimeType === 'application/graphql') {
bodyType = 'graphql';
body = convertSyntax(r.body.text);
} else if (r.body?.mimeType === 'application/json') {
bodyType = 'application/json';
body = convertSyntax(r.body.text);
}
let authenticationType = null;
let authentication = {};
if (r.authentication.type === 'bearer') {
authenticationType = 'bearer';
authentication = {
token: convertSyntax(r.authentication.token),
};
} else if (r.authentication.type === 'basic') {
authenticationType = 'basic';
authentication = {
username: convertSyntax(r.authentication.username),
password: convertSyntax(r.authentication.password),
};
}
return {
id: r._id,
createdAt: new Date(r.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(r.updated ?? Date.now()).toISOString().replace('Z', ''),
workspaceId,
folderId: r.parentId === workspaceId ? null : r.parentId,
model: 'http_request',
sortPriority,
name: r.name,
url: convertSyntax(r.url),
body,
bodyType,
authentication,
authenticationType,
method: r.method,
headers: (r.headers ?? []).map(({ name, value, disabled }) => ({
enabled: !disabled,
name,
value,
})),
};
}

View File

@@ -0,0 +1,15 @@
/**
* Import an Insomnia workspace object.
* @param {Object} w - The workspace object to import.
*/
export function importWorkspace(w, variables) {
console.log('IMPORTING Workspace', w._id, w.name, JSON.stringify(w, null, 2));
return {
id: w._id,
createdAt: new Date(w.created ?? Date.now()).toISOString().replace('Z', ''),
updatedAt: new Date(w.updated ?? Date.now()).toISOString().replace('Z', ''),
model: 'workspace',
name: w.name,
variables,
};
}

View File

@@ -0,0 +1,78 @@
import { importEnvironment } from './importers/environment.js';
import { importRequest } from './importers/request.js';
import { importWorkspace } from './importers/workspace.js';
import {
isEnvironment,
isJSObject,
isRequest,
isRequestGroup,
isWorkspace,
} from './helpers/types.js';
import { parseVariables } from './helpers/variables.js';
import { importFolder } from './importers/folder.js';
export function pluginHookImport(contents) {
const parsed = JSON.parse(contents);
if (!isJSObject(parsed)) {
return;
}
const { _type, __export_format } = parsed;
if (_type !== 'export' || __export_format !== 4 || !Array.isArray(parsed.resources)) {
return;
}
const resources = {
workspaces: [],
requests: [],
environments: [],
folders: [],
};
// Import workspaces
const workspacesToImport = parsed.resources.filter(isWorkspace);
for (const workspaceToImport of workspacesToImport) {
console.log('IMPORTING WORKSPACE', workspaceToImport.name);
const baseEnvironment = parsed.resources.find(
(r) => isEnvironment(r) && r.parentId === workspaceToImport._id,
);
console.log('FOUND BASE ENV', baseEnvironment.name);
resources.workspaces.push(
importWorkspace(
workspaceToImport,
baseEnvironment ? parseVariables(baseEnvironment.data) : [],
),
);
console.log('IMPORTING ENVIRONMENTS', baseEnvironment.name);
const environmentsToImport = parsed.resources.filter(
(r) => isEnvironment(r) && r.parentId === baseEnvironment?._id,
);
console.log('FOUND', environmentsToImport.length, 'ENVIRONMENTS');
resources.environments.push(
...environmentsToImport.map((r) => importEnvironment(r, workspaceToImport._id)),
);
const nextFolder = (parentId) => {
const children = parsed.resources.filter((r) => r.parentId === parentId);
let sortPriority = 0;
for (const child of children) {
if (isRequestGroup(child)) {
resources.folders.push(importFolder(child, workspaceToImport._id));
nextFolder(child._id);
} else if (isRequest(child)) {
resources.requests.push(importRequest(child, workspaceToImport._id, sortPriority++));
}
}
};
// Import folders
nextFolder(workspaceToImport._id);
}
// Filter out any `null` values
resources.requests = resources.requests.filter(Boolean);
resources.environments = resources.environments.filter(Boolean);
resources.workspaces = resources.workspaces.filter(Boolean);
return resources;
}

View File

@@ -0,0 +1,13 @@
import { resolve } from 'path';
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.js'),
fileName: 'index',
formats: ['es'],
},
outDir: resolve(__dirname, 'out'),
},
});

View File

@@ -1,5 +0,0 @@
console.log('---------------------------');
console.log('- 👋 Hello from plugin.ts -');
console.log('---------------------------');
Deno.core.ops.op_hello('World');

1027
src-tauri/sqlx-data.json Normal file
View File

File diff suppressed because it is too large Load Diff

118
src-tauri/src/analytics.rs Normal file
View File

@@ -0,0 +1,118 @@
use sqlx::types::JsonValue;
use tauri::{async_runtime, AppHandle, Manager};
use crate::is_dev;
pub enum AnalyticsResource {
App,
// Workspace,
// Environment,
// Folder,
// HttpRequest,
// HttpResponse,
}
pub enum AnalyticsAction {
Launch,
// Create,
// Update,
// Upsert,
// Delete,
// Send,
// Duplicate,
}
fn resource_name(resource: AnalyticsResource) -> &'static str {
match resource {
AnalyticsResource::App => "app",
// AnalyticsResource::Workspace => "workspace",
// AnalyticsResource::Environment => "environment",
// AnalyticsResource::Folder => "folder",
// AnalyticsResource::HttpRequest => "http_request",
// AnalyticsResource::HttpResponse => "http_response",
}
}
fn action_name(action: AnalyticsAction) -> &'static str {
match action {
AnalyticsAction::Launch => "launch",
// AnalyticsAction::Create => "create",
// AnalyticsAction::Update => "update",
// AnalyticsAction::Upsert => "upsert",
// AnalyticsAction::Delete => "delete",
// AnalyticsAction::Send => "send",
// AnalyticsAction::Duplicate => "duplicate",
}
}
pub fn track_event(
app_handle: &AppHandle,
resource: AnalyticsResource,
action: AnalyticsAction,
attributes: Option<JsonValue>,
) {
async_runtime::block_on(async move {
let event = format!("{}.{}", resource_name(resource), action_name(action));
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
let info = app_handle.package_info();
let params = vec![
("e", event.clone()),
("a", attributes_json.clone()),
("id", "site_zOK0d7jeBy2TLxFCnZ".to_string()),
("v", info.version.clone().to_string()),
("os", get_os().to_string()),
("xy", get_window_size(app_handle)),
];
let url = format!("https://t.yaak.app/t/e");
let req = reqwest::Client::builder()
.build()
.unwrap()
.get(&url)
.query(&params);
if is_dev() {
println!("Ignore dev analytics event: {} {:?}", event, params);
} else {
if let Err(e) = req.send().await {
println!("Error sending analytics event: {}", e);
} else {
println!("Sent analytics event: {}", event);
}
}
});
}
fn get_os() -> &'static str {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
"unknown"
}
}
fn get_window_size(app_handle: &AppHandle) -> String {
let window = match app_handle.windows().into_values().next() {
Some(w) => w,
None => return "unknown".to_string(),
};
let current_monitor = match window.current_monitor() {
Ok(Some(m)) => m,
_ => return "unknown".to_string(),
};
let scale_factor = current_monitor.scale_factor();
let size = current_monitor.size();
let width: f64 = size.width as f64 / scale_factor;
let height: f64 = size.height as f64 / scale_factor;
format!(
"{}x{}",
(width / 100.0).round() * 100.0,
(height / 100.0).round() * 100.0
)
}

View File

@@ -1,102 +0,0 @@
use http::header::{HeaderName, USER_AGENT};
use http::{HeaderMap, HeaderValue, Method};
use reqwest::redirect::Policy;
use std::collections::HashMap;
use tauri::{AppHandle, Wry};
#[derive(serde::Serialize)]
pub struct CustomResponse {
status: String,
body: String,
url: String,
method: String,
elapsed: u128,
elapsed2: u128,
headers: HashMap<String, String>,
}
#[tauri::command]
pub async fn send_request(
app_handle: AppHandle<Wry>,
url: &str,
method: &str,
) -> Result<CustomResponse, String> {
let start = std::time::Instant::now();
let mut abs_url = url.to_string();
if !abs_url.starts_with("http://") && !abs_url.starts_with("https://") {
abs_url = format!("http://{}", url);
}
let client = reqwest::Client::builder()
.redirect(Policy::none())
.build()
.unwrap();
let mut headers = HeaderMap::new();
// headers.insert(CONTENT_TYPE, HeaderValue::from_static("image/png"));
headers.insert(USER_AGENT, HeaderValue::from_static("reqwest"));
headers.insert("x-foo-bar", HeaderValue::from_static("hi mom"));
headers.insert(
HeaderName::from_static("x-api-key"),
HeaderValue::from_static("123-123-123"),
);
let m = Method::from_bytes(method.to_uppercase().as_bytes()).unwrap();
let req = client
.request(m, abs_url.to_string())
.headers(headers)
.build();
let req = match req {
Ok(v) => v,
Err(e) => {
println!("Error: {}", e);
return Err(e.to_string());
}
};
let resp = client.execute(req).await;
let elapsed = start.elapsed().as_millis();
let p = app_handle
.path_resolver()
.resolve_resource("plugins/plugin.ts")
.expect("failed to resolve resource");
crate::runtime::run_plugin_sync(p.to_str().unwrap()).unwrap();
match resp {
Ok(v) => {
let url = v.url().to_string();
let status = v.status().to_string();
let method = method.to_string();
let headers = v
.headers()
.iter()
.map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap().to_string()))
.collect::<HashMap<String, String>>();
let body = v.text().await.unwrap();
let elapsed2 = start.elapsed().as_millis();
Ok(CustomResponse {
status,
body,
elapsed,
elapsed2,
method,
url,
headers,
})
}
Err(e) => {
println!("Error: {}", e);
Err(e.to_string())
}
}
}
#[tauri::command]
pub fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}

View File

@@ -7,58 +7,943 @@
#[macro_use]
extern crate objc;
mod commands;
mod runtime;
mod window_ext;
use std::collections::HashMap;
use std::env::current_dir;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::process::exit;
use tauri::{
CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
WindowEvent,
};
use window_ext::WindowExt;
use base64::Engine;
use http::header::{HeaderName, ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderValue, Method};
use rand::random;
use reqwest::redirect::Policy;
use serde::Serialize;
use sqlx::migrate::Migrator;
use sqlx::sqlite::SqlitePoolOptions;
use sqlx::types::Json;
use sqlx::{Pool, Sqlite};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::{AppHandle, Menu, RunEvent, State, Submenu, Window, WindowUrl, Wry};
use tauri::{CustomMenuItem, Manager, WindowEvent};
use tauri_plugin_window_state::{StateFlags, WindowExt};
use tokio::sync::Mutex;
use window_ext::TrafficLightWindowExt;
use crate::analytics::{track_event, AnalyticsAction, AnalyticsResource};
mod analytics;
mod models;
mod plugin;
mod render;
mod window_ext;
mod window_menu;
#[derive(serde::Serialize)]
pub struct CustomResponse {
status: u16,
body: String,
url: String,
method: String,
elapsed: u128,
elapsed2: u128,
headers: HashMap<String, String>,
pub status_reason: Option<&'static str>,
}
async fn migrate_db(
app_handle: AppHandle<Wry>,
db_instance: &Mutex<Pool<Sqlite>>,
) -> Result<(), String> {
let pool = &*db_instance.lock().await;
let p = app_handle
.path_resolver()
.resolve_resource("migrations")
.expect("failed to resolve resource");
println!("Running migrations at {}", p.to_string_lossy());
let m = Migrator::new(p).await.expect("Failed to load migrations");
m.run(pool).await.expect("Failed to run migrations");
println!("Migrations complete");
Ok(())
}
#[tauri::command]
async fn send_ephemeral_request(
mut request: models::HttpRequest,
environment_id: Option<&str>,
app_handle: AppHandle<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::HttpResponse, String> {
let pool = &*db_instance.lock().await;
let response = models::HttpResponse::new();
let environment_id2 = environment_id.unwrap_or("n/a").to_string();
request.id = "".to_string();
return actually_send_request(request, &response, &environment_id2, &app_handle, pool).await;
}
async fn actually_send_request(
request: models::HttpRequest,
response: &models::HttpResponse,
environment_id: &str,
app_handle: &AppHandle<Wry>,
pool: &Pool<Sqlite>,
) -> Result<models::HttpResponse, String> {
let start = std::time::Instant::now();
let environment = models::get_environment(environment_id, pool).await.ok();
let environment_ref = environment.as_ref();
let workspace = models::get_workspace(&request.workspace_id, pool)
.await
.expect("Failed to get Workspace");
let mut url_string = render::render(&request.url, &workspace, environment.as_ref());
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
url_string = format!("http://{}", url_string);
}
let client = reqwest::Client::builder()
.redirect(Policy::none())
// .danger_accept_invalid_certs(true)
.build()
.expect("Failed to build client");
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static("yaak"));
headers.insert(ACCEPT, HeaderValue::from_static("*/*"));
for h in request.headers.0 {
if h.name.is_empty() && h.value.is_empty() {
continue;
}
if !h.enabled {
continue;
}
let name = render::render(&h.name, &workspace, environment_ref);
let value = render::render(&h.value, &workspace, environment_ref);
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
Ok(n) => n,
Err(e) => {
eprintln!("Failed to create header name: {}", e);
continue;
}
};
let header_value = match HeaderValue::from_str(value.as_str()) {
Ok(n) => n,
Err(e) => {
eprintln!("Failed to create header value: {}", e);
continue;
}
};
headers.insert(header_name, header_value);
}
if let Some(b) = &request.authentication_type {
let empty_value = &serde_json::to_value("").unwrap();
let a = request.authentication.0;
if b == "basic" {
let raw_username = a
.get("username")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let raw_password = a
.get("password")
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let username = render::render(raw_username, &workspace, environment_ref);
let password = render::render(raw_password, &workspace, environment_ref);
let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.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, &workspace, environment_ref);
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
);
}
}
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
.expect("Failed to create method");
let builder = client.request(m, url_string.to_string()).headers(headers);
let sendable_req_result = match (request.body, request.body_type) {
(Some(raw_body), Some(_)) => {
let body = render::render(&raw_body, &workspace, environment_ref);
builder.body(body).build()
}
_ => builder.build(),
};
let sendable_req = match sendable_req_result {
Ok(r) => r,
Err(e) => {
return response_err(response, e.to_string(), &app_handle, pool).await;
}
};
let raw_response = client.execute(sendable_req).await;
match raw_response {
Ok(v) => {
let mut response = response.clone();
response.status = v.status().as_u16() as i64;
response.status_reason = v.status().canonical_reason().map(|s| s.to_string());
response.headers = Json(
v.headers()
.iter()
.map(|(k, v)| models::HttpResponseHeader {
name: k.as_str().to_string(),
value: v.to_str().unwrap().to_string(),
})
.collect(),
);
response.url = v.url().to_string();
let body_bytes = v.bytes().await.expect("Failed to get body").to_vec();
response.content_length = Some(body_bytes.len() as i64);
{
// Write body to FS
let dir = app_handle.path_resolver().app_data_dir().unwrap();
let base_dir = dir.join("responses");
create_dir_all(base_dir.clone()).expect("Failed to create responses dir");
let body_path = match response.id == "" {
false => base_dir.join(response.id.clone()),
true => base_dir.join(uuid::Uuid::new_v4().to_string()),
};
let mut f = File::options()
.create(true)
.truncate(true)
.write(true)
.open(&body_path)
.expect("Failed to open file");
f.write_all(body_bytes.as_slice())
.expect("Failed to write to file");
response.body_path = Some(
body_path
.to_str()
.expect("Failed to get body path")
.to_string(),
);
}
// Also store body directly on the model, if small enough
if body_bytes.len() < 100_000 {
response.body = Some(body_bytes);
}
response.elapsed = start.elapsed().as_millis() as i64;
response = models::update_response_if_id(&response, pool)
.await
.expect("Failed to update response");
if request.id != "" {
emit_side_effect(app_handle, "updated_model", &response);
}
Ok(response)
}
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
}
}
#[tauri::command]
async fn import_data(
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
file_paths: Vec<&str>,
) -> Result<plugin::ImportedResources, String> {
let pool = &*db_instance.lock().await;
let imported = plugin::run_plugin_import(
&window.app_handle(),
pool,
"insomnia-importer",
file_paths.first().unwrap(),
)
.await;
Ok(imported)
}
#[tauri::command]
async fn send_request(
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
request_id: &str,
environment_id: Option<&str>,
) -> Result<models::HttpResponse, String> {
let pool = &*db_instance.lock().await;
let req = models::get_request(request_id, pool)
.await
.expect("Failed to get request");
let response = models::create_response(&req.id, 0, "", 0, None, None, None, None, vec![], pool)
.await
.expect("Failed to create response");
let response2 = response.clone();
let environment_id2 = environment_id.unwrap_or("n/a").to_string();
let app_handle2 = window.app_handle().clone();
let pool2 = pool.clone();
tokio::spawn(async move {
actually_send_request(req, &response2, &environment_id2, &app_handle2, &pool2)
.await
.expect("Failed to send request");
});
emit_and_return(&window, "created_model", response)
}
async fn response_err(
response: &models::HttpResponse,
error: String,
app_handle: &AppHandle<Wry>,
pool: &Pool<Sqlite>,
) -> Result<models::HttpResponse, String> {
let mut response = response.clone();
response.elapsed = -1;
response.error = Some(error.clone());
response = models::update_response_if_id(&response, pool)
.await
.expect("Failed to update response");
emit_side_effect(app_handle, "updated_model", &response);
Ok(response)
}
#[tauri::command]
async fn get_key_value(
namespace: &str,
key: &str,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<Option<models::KeyValue>, ()> {
let pool = &*db_instance.lock().await;
let result = models::get_key_value(namespace, key, pool).await;
Ok(result)
}
#[tauri::command]
async fn set_key_value(
namespace: &str,
key: &str,
value: &str,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::KeyValue, String> {
let pool = &*db_instance.lock().await;
let (key_value, created) = models::set_key_value(namespace, key, value, pool).await;
if created {
emit_and_return(&window, "created_model", key_value)
} else {
emit_and_return(&window, "updated_model", key_value)
}
}
#[tauri::command]
async fn create_workspace(
name: &str,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::Workspace, String> {
let pool = &*db_instance.lock().await;
let created_workspace = models::upsert_workspace(
pool,
models::Workspace {
name: name.to_string(),
..Default::default()
},
)
.await
.expect("Failed to create Workspace");
emit_and_return(&window, "created_model", created_workspace)
}
#[tauri::command]
async fn create_environment(
workspace_id: &str,
name: &str,
variables: Vec<models::EnvironmentVariable>,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::Environment, String> {
let pool = &*db_instance.lock().await;
let created_environment = models::upsert_environment(
pool,
models::Environment {
workspace_id: workspace_id.to_string(),
name: name.to_string(),
variables: Json(variables),
..Default::default()
},
)
.await
.expect("Failed to create environment");
emit_and_return(&window, "created_model", created_environment)
}
#[tauri::command]
async fn create_request(
workspace_id: &str,
name: &str,
sort_priority: f64,
folder_id: Option<&str>,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::HttpRequest, String> {
let pool = &*db_instance.lock().await;
let created_request = models::upsert_request(
pool,
models::HttpRequest {
workspace_id: workspace_id.to_string(),
name: name.to_string(),
method: "GET".to_string(),
folder_id: folder_id.map(|s| s.to_string()),
sort_priority,
..Default::default()
},
)
.await
.expect("Failed to create request");
emit_and_return(&window, "created_model", created_request)
}
#[tauri::command]
async fn duplicate_request(
id: &str,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::HttpRequest, String> {
let pool = &*db_instance.lock().await;
let request = models::duplicate_request(id, pool)
.await
.expect("Failed to duplicate request");
emit_and_return(&window, "updated_model", request)
}
#[tauri::command]
async fn update_workspace(
workspace: models::Workspace,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::Workspace, String> {
let pool = &*db_instance.lock().await;
let updated_workspace = models::upsert_workspace(pool, workspace)
.await
.expect("Failed to update request");
emit_and_return(&window, "updated_model", updated_workspace)
}
#[tauri::command]
async fn update_environment(
environment: models::Environment,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::Environment, String> {
let pool = &*db_instance.lock().await;
let updated_environment = models::upsert_environment(pool, environment)
.await
.expect("Failed to update environment");
emit_and_return(&window, "updated_model", updated_environment)
}
#[tauri::command]
async fn update_request(
request: models::HttpRequest,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::HttpRequest, String> {
let pool = &*db_instance.lock().await;
let updated_request = models::upsert_request(pool, request)
.await
.expect("Failed to update request");
emit_and_return(&window, "updated_model", updated_request)
}
#[tauri::command]
async fn delete_request(
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
request_id: &str,
) -> Result<models::HttpRequest, String> {
let pool = &*db_instance.lock().await;
let req = models::delete_request(request_id, pool)
.await
.expect("Failed to delete request");
emit_and_return(&window, "deleted_model", req)
}
#[tauri::command]
async fn list_folders(
workspace_id: &str,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<Vec<models::Folder>, String> {
let pool = &*db_instance.lock().await;
models::find_folders(workspace_id, pool)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn create_folder(
workspace_id: &str,
name: &str,
sort_priority: f64,
folder_id: Option<&str>,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::Folder, String> {
let pool = &*db_instance.lock().await;
let created_request = models::upsert_folder(
pool,
models::Folder {
workspace_id: workspace_id.to_string(),
name: name.to_string(),
folder_id: folder_id.map(|s| s.to_string()),
sort_priority,
..Default::default()
},
)
.await
.expect("Failed to create folder");
emit_and_return(&window, "created_model", created_request)
}
#[tauri::command]
async fn update_folder(
folder: models::Folder,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::Folder, String> {
let pool = &*db_instance.lock().await;
let updated_folder = models::upsert_folder(pool, folder)
.await
.expect("Failed to update request");
emit_and_return(&window, "updated_model", updated_folder)
}
#[tauri::command]
async fn delete_folder(
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
folder_id: &str,
) -> Result<models::Folder, String> {
let pool = &*db_instance.lock().await;
let req = models::delete_folder(folder_id, pool)
.await
.expect("Failed to delete folder");
emit_and_return(&window, "deleted_model", req)
}
#[tauri::command]
async fn delete_environment(
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
environment_id: &str,
) -> Result<models::Environment, String> {
let pool = &*db_instance.lock().await;
let req = models::delete_environment(environment_id, pool)
.await
.expect("Failed to delete environment");
emit_and_return(&window, "deleted_model", req)
}
#[tauri::command]
async fn list_requests(
workspace_id: &str,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<Vec<models::HttpRequest>, String> {
let pool = &*db_instance.lock().await;
models::find_requests(workspace_id, pool)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn list_environments(
workspace_id: &str,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<Vec<models::Environment>, String> {
let pool = &*db_instance.lock().await;
let environments = models::find_environments(workspace_id, pool)
.await
.expect("Failed to find environments");
Ok(environments)
}
#[tauri::command]
async fn get_folder(
id: &str,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::Folder, String> {
let pool = &*db_instance.lock().await;
models::get_folder(id, pool)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn get_request(
id: &str,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::HttpRequest, String> {
let pool = &*db_instance.lock().await;
models::get_request(id, pool)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn get_environment(
id: &str,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::Environment, String> {
let pool = &*db_instance.lock().await;
models::get_environment(id, pool)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn get_workspace(
id: &str,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::Workspace, String> {
let pool = &*db_instance.lock().await;
models::get_workspace(id, pool)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn list_responses(
request_id: &str,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<Vec<models::HttpResponse>, String> {
let pool = &*db_instance.lock().await;
models::find_responses(request_id, pool)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn delete_response(
id: &str,
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<models::HttpResponse, String> {
let pool = &*db_instance.lock().await;
let response = models::delete_response(id, pool)
.await
.expect("Failed to delete response");
emit_and_return(&window, "deleted_model", response)
}
#[tauri::command]
async fn delete_all_responses(
request_id: &str,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<(), String> {
let pool = &*db_instance.lock().await;
models::delete_all_responses(request_id, pool)
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn list_workspaces(
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<Vec<models::Workspace>, String> {
let pool = &*db_instance.lock().await;
let workspaces = models::find_workspaces(pool)
.await
.expect("Failed to find workspaces");
if workspaces.is_empty() {
let workspace = models::upsert_workspace(
pool,
models::Workspace {
name: "My Project".to_string(),
..Default::default()
},
)
.await
.expect("Failed to create Workspace");
Ok(vec![workspace])
} else {
Ok(workspaces)
}
}
#[tauri::command]
async fn new_window(window: Window<Wry>, url: &str) -> Result<(), String> {
create_window(&window.app_handle(), Some(url));
Ok(())
}
#[tauri::command]
async fn delete_workspace(
window: Window<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
workspace_id: &str,
) -> Result<models::Workspace, String> {
let pool = &*db_instance.lock().await;
let workspace = models::delete_workspace(workspace_id, pool)
.await
.expect("Failed to delete Workspace");
emit_and_return(&window, "deleted_model", workspace)
}
fn main() {
// here `"quit".to_string()` defines the menu item id, and the second parameter is the menu item label.
let quit = CustomMenuItem::new("quit".to_string(), "Quit");
let tray_menu = SystemTrayMenu::new().add_item(quit);
let system_tray = SystemTray::new().with_menu(tray_menu);
tauri::Builder::default()
.system_tray(system_tray)
.plugin(tauri_plugin_window_state::Builder::default().build())
.setup(|app| {
let win = app.get_window("main").unwrap();
win.position_traffic_lights();
Ok(())
})
.on_system_tray_event(|app, event| match event {
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"quit" => {
std::process::exit(0);
}
"hide" => {
let window = app.get_window("main").unwrap();
window.hide().unwrap();
}
_ => {}
},
_ => {}
})
.on_window_event(|e| {
let apply_offset = || {
let win = e.window();
win.position_traffic_lights();
let dir = match is_dev() {
true => current_dir().unwrap(),
false => app.path_resolver().app_data_dir().unwrap(),
};
match e.event() {
WindowEvent::Resized(..) => apply_offset(),
WindowEvent::ThemeChanged(..) => apply_offset(),
_ => {}
}
create_dir_all(dir.clone()).expect("Problem creating App directory!");
let p = dir.join("db.sqlite");
let p_string = p.to_string_lossy().replace(' ', "%20");
let url = format!("sqlite://{}?mode=rwc", p_string);
println!("Connecting to database at {}", url);
tauri::async_runtime::block_on(async move {
let pool = SqlitePoolOptions::new()
.connect(url.as_str())
.await
.expect("Failed to connect to database");
// Setup the DB handle
let m = Mutex::new(pool.clone());
migrate_db(app.handle(), &m)
.await
.expect("Failed to migrate database");
app.manage(m);
let _ = models::cancel_pending_responses(&pool).await;
// TODO: Move this somewhere better
match app.get_cli_matches() {
Ok(matches) => {
let cmd = matches.subcommand.unwrap_or_default();
if cmd.name == "import" {
let arg_file = cmd
.matches
.args
.get("file")
.unwrap()
.value
.as_str()
.unwrap();
plugin::run_plugin_import(
&app.handle(),
&pool,
"insomnia-importer",
arg_file,
)
.await;
exit(0);
} else if cmd.name == "hello" {
plugin::run_plugin_hello(&app.handle(), "hello-world");
exit(0);
}
}
Err(e) => {
println!("Nothing found: {}", e);
}
}
Ok(())
})
})
.invoke_handler(tauri::generate_handler![
commands::send_request,
commands::greet
create_environment,
create_folder,
create_request,
create_workspace,
delete_all_responses,
delete_environment,
delete_folder,
delete_request,
delete_response,
delete_workspace,
duplicate_request,
get_key_value,
get_environment,
get_folder,
get_request,
get_workspace,
import_data,
list_environments,
list_folders,
list_requests,
list_responses,
list_workspaces,
new_window,
send_ephemeral_request,
send_request,
set_key_value,
update_environment,
update_folder,
update_request,
update_workspace,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
.build(tauri::generate_context!())
.expect("error while running tauri application")
.run(|app_handle, event| match event {
RunEvent::Ready => {
let w = create_window(app_handle, None);
w.restore_state(StateFlags::all())
.expect("Failed to restore window state");
track_event(
app_handle,
AnalyticsResource::App,
AnalyticsAction::Launch,
None,
);
}
// ExitRequested { api, .. } => {
// }
_ => {}
});
}
fn is_dev() -> bool {
let env = option_env!("YAAK_ENV");
env.unwrap_or("production") != "production"
}
fn create_window(handle: &AppHandle<Wry>, url: Option<&str>) -> Window<Wry> {
let mut app_menu = window_menu::os_default("Yaak".to_string().as_str());
if is_dev() {
let submenu = Submenu::new(
"Developer",
Menu::new()
.add_item(
CustomMenuItem::new("refresh".to_string(), "Refresh")
.accelerator("CmdOrCtrl + Shift + r"),
)
.add_item(
CustomMenuItem::new("toggle_devtools".to_string(), "Open Devtools")
.accelerator("CmdOrCtrl + Option + i"),
),
);
app_menu = app_menu.add_submenu(submenu);
}
let window_num = handle.windows().len();
let window_id = format!("wnd_{}", window_num);
let mut win_builder = tauri::WindowBuilder::new(
handle,
window_id,
WindowUrl::App(url.unwrap_or_default().into()),
)
.menu(app_menu)
.fullscreen(false)
.resizable(true)
.inner_size(1100.0, 600.0)
.position(
// Randomly offset so windows don't stack exactly
100.0 + random::<f64>() * 30.0,
100.0 + random::<f64>() * 30.0,
)
.title(match is_dev() {
true => "Yaak Dev",
false => "Yaak",
});
// Add macOS-only things
#[cfg(target_os = "macos")]
{
win_builder = win_builder
.hidden_title(true)
.title_bar_style(TitleBarStyle::Overlay);
}
let win = win_builder.build().expect("failed to build window");
let win2 = win.clone();
let handle2 = handle.clone();
win.on_menu_event(move |event| match event.menu_item_id() {
"quit" => exit(0),
"close" => win2.close().unwrap(),
"zoom_reset" => win2.emit("zoom", 0).unwrap(),
"zoom_in" => win2.emit("zoom", 1).unwrap(),
"zoom_out" => win2.emit("zoom", -1).unwrap(),
"toggle_sidebar" => win2.emit("toggle_sidebar", true).unwrap(),
"focus_url" => win2.emit("focus_url", true).unwrap(),
"focus_sidebar" => win2.emit("focus_sidebar", true).unwrap(),
"send_request" => win2.emit("send_request", true).unwrap(),
"new_request" => _ = win2.emit("new_request", true).unwrap(),
"toggle_settings" => _ = win2.emit("toggle_settings", true).unwrap(),
"duplicate_request" => _ = win2.emit("duplicate_request", true).unwrap(),
"refresh" => win2.eval("location.reload()").unwrap(),
"new_window" => _ = create_window(&handle2, None),
"toggle_devtools" => {
if win2.is_devtools_open() {
win2.close_devtools();
} else {
win2.open_devtools();
}
}
_ => {}
});
let win3 = win.clone();
win.on_window_event(move |e| {
let apply_offset = || {
win3.position_traffic_lights();
};
match e {
WindowEvent::Resized(..) => apply_offset(),
WindowEvent::ThemeChanged(..) => apply_offset(),
WindowEvent::CloseRequested { .. } => {
println!("CLOSE REQUESTED");
// api.prevent_close();
}
_ => {}
}
});
win.position_traffic_lights();
win
}
/// Emit an event to all windows, with a source window
fn emit_and_return<S: Serialize + Clone, E>(
current_window: &Window<Wry>,
event: &str,
payload: S,
) -> Result<S, E> {
current_window.emit_all(event, &payload).unwrap();
Ok(payload)
}
/// Emit an event to all windows, used for side-effects where there is no source window to attribute. This
fn emit_side_effect<S: Serialize + Clone>(app_handle: &AppHandle<Wry>, event: &str, payload: S) {
app_handle.emit_all(event, &payload).unwrap();
}

796
src-tauri/src/models.rs Normal file
View File

@@ -0,0 +1,796 @@
use std::collections::HashMap;
use std::fs;
use rand::distributions::{Alphanumeric, DistString};
use serde::{Deserialize, Serialize};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::types::{Json, JsonValue};
use sqlx::{Pool, Sqlite};
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct Workspace {
pub id: String,
pub model: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub name: String,
pub description: String,
pub variables: Json<Vec<EnvironmentVariable>>,
}
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct Environment {
pub id: String,
pub workspace_id: String,
pub model: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub name: String,
pub variables: Json<Vec<EnvironmentVariable>>,
}
fn default_enabled() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct EnvironmentVariable {
#[serde(default = "default_enabled")]
pub enabled: bool,
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct HttpRequestHeader {
#[serde(default = "default_enabled")]
pub enabled: bool,
pub name: String,
pub value: String,
}
fn default_http_request_method() -> String {
"GET".to_string()
}
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct HttpRequest {
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub id: String,
pub workspace_id: String,
pub folder_id: Option<String>,
pub model: String,
pub sort_priority: f64,
pub name: String,
pub url: String,
#[serde(default = "default_http_request_method")]
pub method: String,
pub body: Option<String>,
pub body_type: Option<String>,
pub authentication: Json<HashMap<String, JsonValue>>,
pub authentication_type: Option<String>,
pub headers: Json<Vec<HttpRequestHeader>>,
}
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct Folder {
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub id: String,
pub workspace_id: String,
pub folder_id: Option<String>,
pub model: String,
pub name: String,
pub sort_priority: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct HttpResponseHeader {
pub name: String,
pub value: String,
}
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct HttpResponse {
pub id: String,
pub model: String,
pub workspace_id: String,
pub request_id: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub error: Option<String>,
pub url: String,
pub content_length: Option<i64>,
pub elapsed: i64,
pub status: i64,
pub status_reason: Option<String>,
pub body: Option<Vec<u8>>,
pub body_path: Option<String>,
pub headers: Json<Vec<HttpResponseHeader>>,
}
impl HttpResponse {
pub(crate) fn new() -> Self {
Self {
model: "http_response".to_string(),
..Default::default()
}
}
}
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct KeyValue {
pub model: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub namespace: String,
pub key: String,
pub value: String,
}
pub async fn set_key_value(
namespace: &str,
key: &str,
value: &str,
pool: &Pool<Sqlite>,
) -> (KeyValue, bool) {
let existing = get_key_value(namespace, key, pool).await;
sqlx::query!(
r#"
INSERT INTO key_values (namespace, key, value)
VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET
updated_at = CURRENT_TIMESTAMP,
value = excluded.value
"#,
namespace,
key,
value,
)
.execute(pool)
.await
.expect("Failed to insert key value");
let kv = get_key_value(namespace, key, pool)
.await
.expect("Failed to get key value");
return (kv, existing.is_none());
}
pub async fn get_key_value(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> Option<KeyValue> {
sqlx::query_as!(
KeyValue,
r#"
SELECT model, created_at, updated_at, namespace, key, value
FROM key_values
WHERE namespace = ? AND key = ?
"#,
namespace,
key,
)
.fetch_one(pool)
.await
.ok()
}
pub async fn find_workspaces(pool: &Pool<Sqlite>) -> Result<Vec<Workspace>, sqlx::Error> {
sqlx::query_as!(
Workspace,
r#"
SELECT id, model, created_at, updated_at, name, description,
variables AS "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>"
FROM workspaces
"#,
)
.fetch_all(pool)
.await
}
pub async fn get_workspace(id: &str, pool: &Pool<Sqlite>) -> Result<Workspace, sqlx::Error> {
sqlx::query_as!(
Workspace,
r#"
SELECT id, model, created_at, updated_at, name, description,
variables AS "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>"
FROM workspaces WHERE id = ?
"#,
id,
)
.fetch_one(pool)
.await
}
pub async fn delete_workspace(id: &str, pool: &Pool<Sqlite>) -> Result<Workspace, sqlx::Error> {
let workspace = get_workspace(id, pool).await?;
let _ = sqlx::query!(
r#"
DELETE FROM workspaces
WHERE id = ?
"#,
id,
)
.execute(pool)
.await;
for r in find_responses_by_workspace_id(id, pool).await? {
delete_response(&r.id, pool).await?;
}
Ok(workspace)
}
pub async fn find_environments(
workspace_id: &str,
pool: &Pool<Sqlite>,
) -> Result<Vec<Environment>, sqlx::Error> {
sqlx::query_as!(
Environment,
r#"
SELECT id, workspace_id, model, created_at, updated_at, name,
variables AS "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>"
FROM environments
WHERE workspace_id = ?
"#,
workspace_id,
)
.fetch_all(pool)
.await
}
pub async fn delete_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environment, sqlx::Error> {
let env = get_environment(id, pool).await?;
let _ = sqlx::query!(
r#"
DELETE FROM environments
WHERE id = ?
"#,
id,
)
.execute(pool)
.await;
Ok(env)
}
pub async fn upsert_environment(
pool: &Pool<Sqlite>,
environment: Environment,
) -> Result<Environment, sqlx::Error> {
let id = match environment.id.as_str() {
"" => generate_id(Some("ev")),
_ => environment.id.to_string(),
};
let trimmed_name = environment.name.trim();
sqlx::query!(
r#"
INSERT INTO environments (
id,
workspace_id,
name,
variables
)
VALUES (?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
updated_at = CURRENT_TIMESTAMP,
name = excluded.name,
variables = excluded.variables
"#,
id,
environment.workspace_id,
trimmed_name,
environment.variables,
)
.execute(pool)
.await?;
get_environment(&id, pool).await
}
pub async fn get_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environment, sqlx::Error> {
sqlx::query_as!(
Environment,
r#"
SELECT
id,
model,
workspace_id,
created_at,
updated_at,
name,
variables AS "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>"
FROM environments
WHERE id = ?
"#,
id,
)
.fetch_one(pool)
.await
}
pub async fn get_folder(id: &str, pool: &Pool<Sqlite>) -> Result<Folder, sqlx::Error> {
sqlx::query_as!(
Folder,
r#"
SELECT
id,
model,
workspace_id,
created_at,
updated_at,
folder_id,
name,
sort_priority
FROM folders
WHERE id = ?
"#,
id,
)
.fetch_one(pool)
.await
}
pub async fn find_folders(
workspace_id: &str,
pool: &Pool<Sqlite>,
) -> Result<Vec<Folder>, sqlx::Error> {
sqlx::query_as!(
Folder,
r#"
SELECT
id,
model,
workspace_id,
created_at,
updated_at,
folder_id,
name,
sort_priority
FROM folders
WHERE workspace_id = ?
"#,
workspace_id,
)
.fetch_all(pool)
.await
}
pub async fn delete_folder(id: &str, pool: &Pool<Sqlite>) -> Result<Folder, sqlx::Error> {
let env = get_folder(id, pool).await?;
let _ = sqlx::query!(
r#"
DELETE FROM folders
WHERE id = ?
"#,
id,
)
.execute(pool)
.await;
Ok(env)
}
pub async fn upsert_folder(pool: &Pool<Sqlite>, r: Folder) -> Result<Folder, sqlx::Error> {
let id = match r.id.as_str() {
"" => generate_id(Some("fl")),
_ => r.id.to_string(),
};
let trimmed_name = r.name.trim();
sqlx::query!(
r#"
INSERT INTO folders (
id,
workspace_id,
folder_id,
name,
sort_priority
)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
updated_at = CURRENT_TIMESTAMP,
name = excluded.name,
folder_id = excluded.folder_id,
sort_priority = excluded.sort_priority
"#,
id,
r.workspace_id,
r.folder_id,
trimmed_name,
r.sort_priority,
)
.execute(pool)
.await?;
get_folder(&id, pool).await
}
pub async fn duplicate_request(id: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest, sqlx::Error> {
let mut request = get_request(id, pool).await?.clone();
request.id = "".to_string();
upsert_request(pool, request).await
}
pub async fn upsert_request(
pool: &Pool<Sqlite>,
r: HttpRequest,
) -> Result<HttpRequest, sqlx::Error> {
let id = match r.id.as_str() {
"" => generate_id(Some("rq")),
_ => r.id.to_string(),
};
let headers_json = Json(r.headers);
let auth_json = Json(r.authentication);
let trimmed_name = r.name.trim();
sqlx::query!(
r#"
INSERT INTO http_requests (
id,
workspace_id,
folder_id,
name,
url,
method,
body,
body_type,
authentication,
authentication_type,
headers,
sort_priority
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
updated_at = CURRENT_TIMESTAMP,
name = excluded.name,
folder_id = excluded.folder_id,
method = excluded.method,
headers = excluded.headers,
body = excluded.body,
body_type = excluded.body_type,
authentication = excluded.authentication,
authentication_type = excluded.authentication_type,
url = excluded.url,
sort_priority = excluded.sort_priority
"#,
id,
r.workspace_id,
r.folder_id,
trimmed_name,
r.url,
r.method,
r.body,
r.body_type,
auth_json,
r.authentication_type,
headers_json,
r.sort_priority,
)
.execute(pool)
.await?;
get_request(&id, pool).await
}
pub async fn find_requests(
workspace_id: &str,
pool: &Pool<Sqlite>,
) -> Result<Vec<HttpRequest>, sqlx::Error> {
sqlx::query_as!(
HttpRequest,
r#"
SELECT
id,
model,
workspace_id,
folder_id,
created_at,
updated_at,
name,
url,
method,
body,
body_type,
authentication AS "authentication!: Json<HashMap<String, JsonValue>>",
authentication_type,
sort_priority,
headers AS "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>"
FROM http_requests
WHERE workspace_id = ?
"#,
workspace_id,
)
.fetch_all(pool)
.await
}
pub async fn get_request(id: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest, sqlx::Error> {
sqlx::query_as!(
HttpRequest,
r#"
SELECT
id,
model,
workspace_id,
folder_id,
created_at,
updated_at,
name,
url,
method,
body,
body_type,
authentication AS "authentication!: Json<HashMap<String, JsonValue>>",
authentication_type,
sort_priority,
headers AS "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>"
FROM http_requests
WHERE id = ?
"#,
id,
)
.fetch_one(pool)
.await
}
pub async fn delete_request(id: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest, sqlx::Error> {
let req = get_request(id, pool).await?;
// DB deletes will cascade but this will delete the files
delete_all_responses(id, pool).await?;
let _ = sqlx::query!(
r#"
DELETE FROM http_requests
WHERE id = ?
"#,
id,
)
.execute(pool)
.await;
Ok(req)
}
pub async fn create_response(
request_id: &str,
elapsed: i64,
url: &str,
status: i64,
status_reason: Option<&str>,
content_length: Option<i64>,
body: Option<Vec<u8>>,
body_path: Option<&str>,
headers: Vec<HttpResponseHeader>,
pool: &Pool<Sqlite>,
) -> Result<HttpResponse, sqlx::Error> {
let req = get_request(request_id, pool).await?;
let id = generate_id(Some("rp"));
let headers_json = Json(headers);
sqlx::query!(
r#"
INSERT INTO http_responses (
id,
request_id,
workspace_id,
elapsed,
url,
status,
status_reason,
content_length,
body,
body_path,
headers
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"#,
id,
request_id,
req.workspace_id,
elapsed,
url,
status,
status_reason,
content_length,
body,
body_path,
headers_json,
)
.execute(pool)
.await?;
get_response(&id, pool).await
}
pub async fn cancel_pending_responses(pool: &Pool<Sqlite>) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
UPDATE http_responses
SET (elapsed, status_reason) = (-1, 'Cancelled')
WHERE elapsed = 0;
"#,
)
.execute(pool)
.await?;
Ok(())
}
pub async fn update_response_if_id(
response: &HttpResponse,
pool: &Pool<Sqlite>,
) -> Result<HttpResponse, sqlx::Error> {
if response.id == "" {
return Ok(response.clone());
}
return update_response(response, pool).await;
}
pub async fn upsert_workspace(
pool: &Pool<Sqlite>,
workspace: Workspace,
) -> Result<Workspace, sqlx::Error> {
let id = match workspace.id.as_str() {
"" => generate_id(Some("wk")),
_ => workspace.id.to_string(),
};
let trimmed_name = workspace.name.trim();
sqlx::query!(
r#"
INSERT INTO workspaces (id, name, description, variables)
VALUES (?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
updated_at = CURRENT_TIMESTAMP,
name = excluded.name,
description = excluded.description,
variables = excluded.variables
"#,
id,
trimmed_name,
workspace.description,
workspace.variables,
)
.execute(pool)
.await?;
get_workspace(&id, pool).await
}
pub async fn update_response(
response: &HttpResponse,
pool: &Pool<Sqlite>,
) -> Result<HttpResponse, sqlx::Error> {
let headers_json = Json(&response.headers);
sqlx::query!(
r#"
UPDATE http_responses SET (
elapsed,
url,
status,
status_reason,
content_length,
body,
body_path,
error,
headers,
updated_at
) = (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;
"#,
response.elapsed,
response.url,
response.status,
response.status_reason,
response.content_length,
response.body,
response.body_path,
response.error,
headers_json,
response.id,
)
.execute(pool)
.await?;
get_response(&response.id, pool).await
}
pub async fn get_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse, sqlx::Error> {
sqlx::query_as!(
HttpResponse,
r#"
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
status, status_reason, content_length, body, body_path, elapsed, error,
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
FROM http_responses
WHERE id = ?
"#,
id,
)
.fetch_one(pool)
.await
}
pub async fn find_responses(
request_id: &str,
pool: &Pool<Sqlite>,
) -> Result<Vec<HttpResponse>, sqlx::Error> {
sqlx::query_as!(
HttpResponse,
r#"
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
status, status_reason, content_length, body, body_path, elapsed, error,
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
FROM http_responses
WHERE request_id = ?
ORDER BY created_at DESC
"#,
request_id,
)
.fetch_all(pool)
.await
}
pub async fn find_responses_by_workspace_id(
workspace_id: &str,
pool: &Pool<Sqlite>,
) -> Result<Vec<HttpResponse>, sqlx::Error> {
sqlx::query_as!(
HttpResponse,
r#"
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
status, status_reason, content_length, body, body_path, elapsed, error,
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
FROM http_responses
WHERE workspace_id = ?
ORDER BY created_at DESC
"#,
workspace_id,
)
.fetch_all(pool)
.await
}
pub async fn delete_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse, sqlx::Error> {
let resp = get_response(id, pool).await?;
// Delete the body file if it exists
if let Some(p) = resp.body_path.clone() {
if let Err(e) = fs::remove_file(p) {
println!("Failed to delete body file: {}", e);
};
}
let _ = sqlx::query!(
r#"
DELETE FROM http_responses
WHERE id = ?
"#,
id,
)
.execute(pool)
.await;
Ok(resp)
}
pub async fn delete_all_responses(
request_id: &str,
pool: &Pool<Sqlite>,
) -> Result<(), sqlx::Error> {
for r in find_responses(request_id, pool).await? {
delete_response(&r.id, pool).await?;
}
Ok(())
}
pub fn generate_id(prefix: Option<&str>) -> String {
let id = Alphanumeric.sample_string(&mut rand::thread_rng(), 10);
return match prefix {
None => id,
Some(p) => format!("{p}_{id}"),
};
}

187
src-tauri/src/plugin.rs Normal file
View File

@@ -0,0 +1,187 @@
use std::fs;
use boa_engine::builtins::promise::PromiseState;
use boa_engine::{
js_string,
module::{ModuleLoader, SimpleModuleLoader},
property::Attribute,
Context, JsArgs, JsNativeError, JsValue, Module, NativeFunction, Source,
};
use boa_runtime::Console;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::{Pool, Sqlite};
use tauri::AppHandle;
use crate::models::{self, Environment, Folder, HttpRequest, Workspace};
pub fn run_plugin_hello(app_handle: &AppHandle, plugin_name: &str) {
run_plugin(app_handle, plugin_name, "hello", &[]);
}
#[derive(Default, Debug, Deserialize, Serialize)]
pub struct ImportedResources {
workspaces: Vec<Workspace>,
environments: Vec<Environment>,
folders: Vec<Folder>,
requests: Vec<HttpRequest>,
}
pub async fn run_plugin_import(
app_handle: &AppHandle,
pool: &Pool<Sqlite>,
plugin_name: &str,
file_path: &str,
) -> ImportedResources {
let file = fs::read_to_string(file_path)
.expect(format!("Unable to read file {}", file_path.to_string()).as_str());
let file_contents = file.as_str();
let result_json = run_plugin(
app_handle,
plugin_name,
"pluginHookImport",
&[js_string!(file_contents).into()],
);
let resources: ImportedResources =
serde_json::from_value(result_json).expect("failed to parse result json");
let mut imported_resources = ImportedResources::default();
println!("Importing resources");
for w in resources.workspaces {
println!("Importing workspace: {:?}", w);
let x = models::upsert_workspace(&pool, w)
.await
.expect("Failed to create workspace");
imported_resources.workspaces.push(x.clone());
println!("Imported workspace: {}", x.name);
}
for e in resources.environments {
println!("Importing environment: {:?}", e);
let x = models::upsert_environment(&pool, e)
.await
.expect("Failed to create environment");
imported_resources.environments.push(x.clone());
println!("Imported environment: {}", x.name);
}
for f in resources.folders {
println!("Importing folder: {:?}", f);
let x = models::upsert_folder(&pool, f)
.await
.expect("Failed to create folder");
imported_resources.folders.push(x.clone());
println!("Imported folder: {}", x.name);
}
for r in resources.requests {
println!("Importing request: {:?}", r);
let x = models::upsert_request(&pool, r)
.await
.expect("Failed to create request");
imported_resources.requests.push(x.clone());
println!("Imported request: {}", x.name);
}
imported_resources
}
fn run_plugin(
app_handle: &AppHandle,
plugin_name: &str,
entrypoint: &str,
js_args: &[JsValue],
) -> serde_json::Value {
let plugin_dir = app_handle
.path_resolver()
.resolve_resource("plugins")
.expect("failed to resolve plugin directory resource")
.join(plugin_name);
let plugin_index_file = plugin_dir.join("out/index.js");
println!("Plugin dir={:?} file={:?}", plugin_dir, plugin_index_file);
// Module loader for the specific plugin
let loader = &SimpleModuleLoader::new(plugin_dir).expect("failed to create module loader");
let dyn_loader: &dyn ModuleLoader = loader;
let context = &mut Context::builder()
.module_loader(dyn_loader)
.build()
.expect("failed to create context");
add_runtime(context);
add_globals(context);
let source = Source::from_filepath(&plugin_index_file).expect("Error opening file");
// Can also pass a `Some(realm)` if you need to execute the module in another realm.
let module = Module::parse(source, None, context).expect("failed to parse module");
// Insert parsed entrypoint into the module loader
// TODO: Is this needed if loaded from file already?
loader.insert(plugin_index_file, module.clone());
let promise_result = module
.load_link_evaluate(context)
.expect("failed to evaluate module");
// Very important to push forward the job queue after queueing promises.
context.run_jobs();
// Checking if the final promise didn't return an error.
match promise_result.state().expect("failed to get promise state") {
PromiseState::Pending => {
panic!("Promise was pending");
}
PromiseState::Fulfilled(v) => {
assert_eq!(v, JsValue::undefined())
}
PromiseState::Rejected(err) => {
panic!("Failed to link: {}", err.display());
}
}
let namespace = module.namespace(context);
let result = namespace
.get(js_string!(entrypoint), context)
.expect("failed to get entrypoint")
.as_callable()
.cloned()
.ok_or_else(|| JsNativeError::typ().with_message("export wasn't a function!"))
.expect("Failed to get entrypoint")
.call(&JsValue::undefined(), js_args, context)
.expect("Failed to call entrypoint");
match result.is_undefined() {
true => json!(null), // to_json doesn't work with undefined (yet)
false => result
.to_json(context)
.expect("failed to convert result to json"),
}
}
fn add_runtime(context: &mut Context<'_>) {
let console = Console::init(context);
context
.register_global_property(js_string!(Console::NAME), console, Attribute::all())
.expect("the console builtin shouldn't exist");
}
fn add_globals(context: &mut Context<'_>) {
context
.register_global_builtin_callable(
"sayHello",
1,
NativeFunction::from_fn_ptr(|_, args, context| {
let value: String = args
.get_or_undefined(0)
.try_js_into(context)
.expect("failed to convert arg");
println!("Hello {}!", value);
Ok(value.into())
}),
)
.expect("failed to register global");
}

32
src-tauri/src/render.rs Normal file
View File

@@ -0,0 +1,32 @@
use crate::models::{Environment, Workspace};
use std::collections::HashMap;
use tauri::regex::Regex;
pub fn render(template: &str, workspace: &Workspace, environment: Option<&Environment>) -> String {
let mut map = HashMap::new();
let workspace_variables = &workspace.variables.0;
for variable in workspace_variables {
if !variable.enabled || variable.value.is_empty() {
continue;
}
map.insert(variable.name.as_str(), variable.value.as_str());
}
if let Some(e) = environment {
let environment_variables = &e.variables.0;
for variable in environment_variables {
if !variable.enabled || variable.value.is_empty() {
continue;
}
map.insert(variable.name.as_str(), variable.value.as_str());
}
}
Regex::new(r"\$\{\[\s*([^]\s]+)\s*]}")
.expect("Failed to create regex")
.replace_all(template, |caps: &tauri::regex::Captures| {
let key = caps.get(1).unwrap().as_str();
map.get(key).unwrap_or(&"")
})
.to_string()
}

View File

@@ -1,16 +0,0 @@
(function (globalThis) {
Deno.core.initializeAsyncOps();
function argsToMessage(...args) {
return args.map((arg) => JSON.stringify(arg)).join(' ');
}
globalThis.console = {
log: (...args) => {
Deno.core.print(`[log]: ${argsToMessage(...args)}\n`, false);
},
error: (...args) => {
Deno.core.print(`[err]: ${argsToMessage(...args)}\n`, true);
},
};
})(globalThis);

View File

@@ -1,111 +0,0 @@
use deno_ast::{MediaType, ParseParams, SourceTextInfo};
use deno_core::error::AnyError;
use deno_core::{op, Extension, JsRuntime, ModuleSource, ModuleType, RuntimeOptions};
use std::rc::Rc;
use deno_core::futures::FutureExt;
use futures::executor;
pub fn run_plugin_sync(file_path: &str) -> Result<(), AnyError> {
executor::block_on(run_plugin(file_path))
}
pub async fn run_plugin(file_path: &str) -> Result<(), AnyError> {
let extension = Extension::builder("runtime")
.ops(vec![op_hello::decl()])
.build();
// Initialize a runtime instance
let mut runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(Rc::new(TsModuleLoader)),
extensions: vec![extension],
..Default::default()
});
runtime
.execute_script("<runtime>", include_str!("runtime.js"))
.unwrap();
let main_module = deno_core::resolve_path(file_path)?;
let mod_id = runtime.load_main_module(&main_module, None).await?;
let result = runtime.mod_evaluate(mod_id);
runtime.run_event_loop(false).await?;
result.await?
}
#[op]
async fn op_hello(name: String) -> Result<String, AnyError> {
let contents = format!("Hello {} from Rust!", name);
println!("{}", contents);
Ok(contents)
}
struct TsModuleLoader;
impl deno_core::ModuleLoader for TsModuleLoader {
fn resolve(
&self,
specifier: &str,
referrer: &str,
_kind: deno_core::ResolutionKind,
) -> Result<deno_core::ModuleSpecifier, AnyError> {
deno_core::resolve_import(specifier, referrer).map_err(|e| e.into())
}
fn load(
&self,
module_specifier: &deno_core::ModuleSpecifier,
_maybe_referrer: Option<deno_core::ModuleSpecifier>,
_is_dyn_import: bool,
) -> std::pin::Pin<Box<deno_core::ModuleSourceFuture>> {
let module_specifier = module_specifier.clone();
async move {
let path = module_specifier.to_file_path().unwrap();
// Determine what the MediaType is (this is done based on the file
// extension) and whether transpiling is required.
let media_type = MediaType::from(&path);
let (module_type, should_transpile) = match MediaType::from(&path) {
MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => {
(ModuleType::JavaScript, false)
}
MediaType::Jsx => (ModuleType::JavaScript, true),
MediaType::TypeScript
| MediaType::Mts
| MediaType::Cts
| MediaType::Dts
| MediaType::Dmts
| MediaType::Dcts
| MediaType::Tsx => (ModuleType::JavaScript, true),
MediaType::Json => (ModuleType::Json, false),
_ => panic!("Unknown extension {:?}", path.extension()),
};
// Read the file, transpile if necessary.
let code = std::fs::read_to_string(&path)?;
let code = if should_transpile {
let parsed = deno_ast::parse_module(ParseParams {
specifier: module_specifier.to_string(),
text_info: SourceTextInfo::from_string(code),
media_type,
capture_tokens: false,
scope_analysis: false,
maybe_syntax: None,
})?;
parsed.transpile(&Default::default())?.text
} else {
code
};
// Load and return module.
let module = ModuleSource {
code: code.into_bytes().into_boxed_slice(),
module_type,
module_url_specified: module_specifier.to_string(),
module_url_found: module_specifier.to_string(),
};
Ok(module)
}
.boxed_local()
}
}

View File

@@ -1,14 +1,18 @@
use tauri::{Runtime, Window};
const TRAFFIC_LIGHT_OFFSET_X: f64 = 15.0;
const TRAFFIC_LIGHT_OFFSET_Y: f64 = 20.0;
const TRAFFIC_LIGHT_OFFSET_X: f64 = 13.0;
const TRAFFIC_LIGHT_OFFSET_Y: f64 = 18.0;
pub trait WindowExt {
#[cfg(target_os = "macos")]
pub trait TrafficLightWindowExt {
fn position_traffic_lights(&self);
}
impl<R: Runtime> WindowExt for Window<R> {
impl<R: Runtime> TrafficLightWindowExt for Window<R> {
#[cfg(not(target_os = "macos"))]
fn position_traffic_lights(&self) {
// No-op
}
#[cfg(target_os = "macos")]
fn position_traffic_lights(&self) {
use cocoa::appkit::{NSView, NSWindow, NSWindowButton};

View File

@@ -0,0 +1,119 @@
use tauri::{AboutMetadata, CustomMenuItem, Menu, MenuItem, Submenu};
pub fn os_default(#[allow(unused)] app_name: &str) -> Menu {
let mut menu = Menu::new();
#[cfg(target_os = "macos")]
{
menu = menu.add_submenu(Submenu::new(
app_name,
Menu::new()
.add_native_item(MenuItem::About(
app_name.to_string(),
AboutMetadata::default(),
))
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Services)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Hide)
.add_native_item(MenuItem::HideOthers)
.add_native_item(MenuItem::ShowAll)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::Quit),
));
}
let mut file_menu = Menu::new();
file_menu = file_menu.add_native_item(MenuItem::CloseWindow);
#[cfg(not(target_os = "macos"))]
{
file_menu = file_menu.add_native_item(MenuItem::Quit);
}
menu = menu.add_submenu(Submenu::new("File", file_menu));
#[cfg(not(target_os = "linux"))]
let mut edit_menu = Menu::new();
#[cfg(target_os = "macos")]
{
edit_menu = edit_menu.add_native_item(MenuItem::Undo);
edit_menu = edit_menu.add_native_item(MenuItem::Redo);
edit_menu = edit_menu.add_native_item(MenuItem::Separator);
}
#[cfg(not(target_os = "linux"))]
{
edit_menu = edit_menu.add_native_item(MenuItem::Cut);
edit_menu = edit_menu.add_native_item(MenuItem::Copy);
edit_menu = edit_menu.add_native_item(MenuItem::Paste);
}
#[cfg(target_os = "macos")]
{
edit_menu = edit_menu.add_native_item(MenuItem::SelectAll);
}
#[cfg(not(target_os = "linux"))]
{
menu = menu.add_submenu(Submenu::new("Edit", edit_menu));
}
let mut view_menu = Menu::new();
#[cfg(target_os = "macos")]
{
view_menu = view_menu
.add_native_item(MenuItem::EnterFullScreen)
.add_native_item(MenuItem::Separator);
}
view_menu = view_menu
.add_item(
CustomMenuItem::new("zoom_reset".to_string(), "Zoom to Actual Size")
.accelerator("CmdOrCtrl+0"),
)
.add_item(
CustomMenuItem::new("zoom_in".to_string(), "Zoom In").accelerator("CmdOrCtrl+Plus"),
)
.add_item(
CustomMenuItem::new("zoom_out".to_string(), "Zoom Out").accelerator("CmdOrCtrl+-"),
)
.add_native_item(MenuItem::Separator)
.add_item(
CustomMenuItem::new("toggle_sidebar".to_string(), "Toggle Sidebar")
.accelerator("CmdOrCtrl+b"),
)
.add_item(
CustomMenuItem::new("focus_sidebar".to_string(), "Focus Sidebar")
.accelerator("CmdOrCtrl+1"),
)
.add_item(
CustomMenuItem::new("toggle_settings".to_string(), "Toggle Settings")
.accelerator("CmdOrCtrl+,"),
)
.add_item(
CustomMenuItem::new("focus_url".to_string(), "Focus URL").accelerator("CmdOrCtrl+l"),
);
menu = menu.add_submenu(Submenu::new("View", view_menu));
let mut window_menu = Menu::new();
window_menu = window_menu.add_native_item(MenuItem::Minimize);
#[cfg(target_os = "macos")]
{
window_menu = window_menu.add_native_item(MenuItem::Zoom);
window_menu = window_menu.add_native_item(MenuItem::Separator);
}
window_menu = window_menu.add_native_item(MenuItem::CloseWindow);
menu = menu.add_submenu(Submenu::new("Window", window_menu));
menu = menu.add_submenu(Submenu::new(
"Workspace",
Menu::new()
.add_item(
CustomMenuItem::new("send_request".to_string(), "Send Request")
.accelerator("CmdOrCtrl+r"),
)
.add_item(
CustomMenuItem::new("new_request".to_string(), "New Request")
.accelerator("CmdOrCtrl+n"),
)
.add_item(
CustomMenuItem::new("duplicate_request".to_string(), "Duplicate Request")
.accelerator("CmdOrCtrl+d"),
),
));
menu
}

116
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,116 @@
{
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm run dev",
"devPath": "http://localhost:1420",
"distDir": "../dist",
"withGlobalTauri": false
},
"package": {
"productName": "Yaak",
"version": "2023.2.0"
},
"tauri": {
"windows": [],
"cli": {
"description": "Yaak CLI",
"longDescription": "This is the Yaak CLI, yo",
"beforeHelp": "u can use it to build, develop and manage your Yaak application.",
"afterHelp": "Have fun!",
"args": [],
"subcommands": {
"import": {
"args": [{
"name": "file",
"short": "f",
"takesValue": true
}]
},
"hello": {}
}
},
"allowlist": {
"all": false,
"os": {
"all": true
},
"protocol": {
"assetScope": [
"$APPDATA/responses/*"
],
"asset": true
},
"fs": {
"readFile": true,
"scope": [
"$RESOURCE/*",
"$APPDATA/responses/*"
]
},
"shell": {
"all": false,
"open": true
},
"window": {
"startDragging": true
},
"dialog": {
"all": false,
"open": true
}
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "co.schier.yaak",
"longDescription": "The best cross-platform visual API client",
"resources": [
"migrations/*",
"plugins/*"
],
"shortDescription": "The best API client",
"targets": [
"deb",
"appimage",
"nsis",
"app",
"dmg",
"updater"
],
"deb": {
"depends": []
},
"macOS": {
"exceptionDomain": "",
"entitlements": "macos/entitlements.plist",
"frameworks": []
},
"windows": {
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {},
"systemTray": {
"iconAsTemplate": true,
"iconPath": "icons/icon.png"
},
"updater": {
"active": true,
"dialog": true,
"endpoints": [
"https://update.yaak.app/check/{{target}}/{{arch}}/{{current_version}}"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEMxRDJFREQ1MjExQjdGN0IKUldSN2Z4c2gxZTNTd1FHNCtmYnFXMHVVQzhuNkJOM1cwOFBodmdLall3ckhKenpKUytHSTR1MlkK"
}
}
}

View File

@@ -1,73 +0,0 @@
[build]
beforeDevCommand = "npm run dev"
beforeBuildCommand = "npm run build"
devPath = "http://localhost:1420"
distDir = "../dist"
withGlobalTauri = false
[package]
productName = "Twosomnia"
version = "0.0.1"
[tauri.allowlist]
all = false
[tauri.allowlist.shell]
all = false
open = true
[tauri.allowlist.window]
startDragging = true
[tauri.allowlist.fs]
scope = [ "$RESOURCE/*" ]
[tauri.bundle]
active = true
category = "DeveloperTool"
copyright = ""
externalBin = [ ]
icon = [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
identifier = "co.schier.twosomnia"
longDescription = ""
resources = [ "plugins/*" ]
shortDescription = ""
targets = "all"
[tauri.bundle.deb]
depends = [ ]
[tauri.bundle.macOS]
exceptionDomain = ""
frameworks = [ ]
[tauri.bundle.windows]
digestAlgorithm = "sha256"
timestampUrl = ""
[tauri.security]
[tauri.updater]
active = false
endpoints = [ ]
pubkey = ""
dialog = true
[[tauri.windows]]
fullscreen = false
height = 800
resizable = true
title = "Twosomnia"
width = 1_400
titleBarStyle = "Overlay"
hiddenTitle = true
[tauri.systemTray]
iconPath = "icons/icon.png"
iconAsTemplate = true

View File

@@ -1,6 +0,0 @@
/target
**/*.rs.bk
Cargo.lock
bin/
pkg/
wasm-pack.log

View File

@@ -1,33 +0,0 @@
[package]
name = "hello"
version = "0.1.0"
authors = ["Gregory Schier <gschier1990@gmail.com>"]
edition = "2018"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2.63"
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.6", optional = true }
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. It is slower than the default
# allocator, however.
wee_alloc = { version = "0.4.5", optional = true }
wasm-bindgen-futures = "0.4.34"
[dev-dependencies]
wasm-bindgen-test = "0.3.13"
[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"

View File

@@ -1,21 +0,0 @@
use wasm_bindgen::prelude::*;
mod utils;
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
log("Hello from Rust WASM!");
}

View File

@@ -1,10 +0,0 @@
pub fn set_panic_hook() {
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function at least once during initialization, and then
// we will get better error messages if our code ever panics.
//
// For more details see
// https://github.com/rustwasm/console_error_panic_hook#readme
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}

View File

@@ -1,137 +0,0 @@
import { FormEvent, useState } from 'react';
import { invoke } from '@tauri-apps/api/tauri';
import Editor from './components/Editor/Editor';
import { Input } from './components/Input';
import { HStack, VStack } from './components/Stacks';
import { Button } from './components/Button';
import { DropdownMenuRadio } from './components/Dropdown';
import { WindowDragRegion } from './components/WindowDragRegion';
import { IconButton } from './components/IconButton';
interface Response {
url: string;
method: string;
body: string;
status: string;
elapsed: number;
elapsed2: number;
headers: Record<string, string>;
}
function App() {
const [error, setError] = useState<string | null>(null);
const [response, setResponse] = useState<Response | null>(null);
const [url, setUrl] = useState('https://go-server.schier.dev/debug');
const [loading, setLoading] = useState(false);
const [method, setMethod] = useState<string>('get');
async function sendRequest(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setLoading(true);
setError(null);
try {
const resp = (await invoke('send_request', { method, url })) as Response;
if (resp.body.includes('<head>')) {
resp.body = resp.body.replace(/<head>/gi, `<head><base href="${resp.url}"/>`);
}
setLoading(false);
setResponse(resp);
} catch (err) {
setLoading(false);
setError(`${err}`);
}
}
const contentType = response?.headers['content-type']?.split(';')[0] ?? 'text/plain';
return (
<>
<div className="grid grid-cols-[auto_1fr] h-full">
<nav className="w-52 bg-gray-50 h-full border-r border-gray-500/10">
<HStack as={WindowDragRegion} className="pl-24 px-1" items="center" justify="end">
<IconButton icon="archive" size="sm" />
<DropdownMenuRadio
onValueChange={null}
value={'get'}
items={[
{ label: 'This is a cool one', value: 'get' },
{ label: 'But this one is better', value: 'put' },
{ label: 'This one is just alright', value: 'post' },
]}
>
<IconButton icon="camera" size="sm" />
</DropdownMenuRadio>
</HStack>
</nav>
<VStack className="w-full">
<HStack as={WindowDragRegion} items="center" className="pl-4 pr-1">
<h5>Hello, Friend!</h5>
<IconButton icon="gear" className="ml-auto" size="sm" />
</HStack>
<VStack className="p-4 max-w-[35rem] mx-auto" space={3}>
<HStack as="form" className="items-end" onSubmit={sendRequest} space={2}>
<DropdownMenuRadio
onValueChange={setMethod}
value={method}
items={[
{ label: 'GET', value: 'get' },
{ label: 'PUT', value: 'put' },
{ label: 'POST', value: 'post' },
]}
>
<Button disabled={loading} color="secondary" forDropdown>
{method.toUpperCase()}
</Button>
</DropdownMenuRadio>
<HStack>
<Input
hideLabel
name="url"
label="Enter URL"
className="rounded-r-none font-mono"
onChange={(e) => setUrl(e.currentTarget.value)}
value={url}
placeholder="Enter a URL..."
/>
<Button
className="mr-1 rounded-l-none -ml-3"
color="primary"
type="submit"
disabled={loading}
>
{loading ? 'Sending...' : 'Send'}
</Button>
</HStack>
</HStack>
{error && <div className="text-white bg-red-500 px-4 py-1 rounded">{error}</div>}
{response !== null && (
<>
<div className="my-1 italic text-gray-500 text-sm">
{response?.method.toUpperCase()}
&nbsp;&bull;&nbsp;
{response?.status}
&nbsp;&bull;&nbsp;
{response?.elapsed}ms &nbsp;&bull;&nbsp;
{response?.elapsed2}ms
</div>
{contentType.includes('html') ? (
<iframe
title="Response preview"
srcDoc={response.body}
sandbox="allow-scripts allow-same-origin"
className="h-[70vh] w-full rounded-lg"
/>
) : response?.body ? (
<Editor value={response?.body} contentType={contentType} />
) : null}
</>
)}
</VStack>
</VStack>
</div>
</>
);
}
export default App;

View File

@@ -0,0 +1,5 @@
<svg width="100%" height="100%" viewBox="0 0 15 15" version="1.1" xmlns="http://www.w3.org/2000/svg"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path fill="currentColor"
d="M2.5,1C1.672,1 1,1.672 1,2.5L1,12.5C1,13.328 1.672,14 2.5,14L12.5,14C13.328,14 14,13.328 14,12.5L14,2.5C14,1.672 13.328,1 12.5,1L2.5,1ZM12.5,13C12.776,13 13,12.776 13,12.5L13,2.5C13,2.224 12.776,2 12.5,2L6,2L6,13L12.5,13ZM2.5,2L5,2L5,13L2.5,13C2.224,13 2,12.776 2,12.5L2,2.5C2,2.224 2.224,2 2.5,2Z"/>
</svg>

After

Width:  |  Height:  |  Size: 553 B

View File

@@ -0,0 +1,6 @@
<svg width="100%" height="100%" viewBox="0 0 15 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<rect x="0" y="0" width="15" height="15" style="fill:none;"/>
<g transform="matrix(1,0,0,1,-16,-8.88178e-16)">
<path fill="currentColor" d="M18.5,1C17.672,1 17,1.672 17,2.5L17,12.5C17,13.328 17.672,14 18.5,14L28.5,14C29.328,14 30,13.328 30,12.5L30,2.5C30,1.672 29.328,1 28.5,1L18.5,1ZM28.5,13C28.776,13 29,12.776 29,12.5L29,2.5C29,2.224 28.776,2 28.5,2L22,2L22,13L28.5,13ZM18,11.535L21,12.285L21,13L18.5,13C18.224,13 18,12.776 18,12.5L18,11.535ZM18,10.504L21,11.254L21,9.81L18,9.06L18,10.504ZM18,8.029L21,8.779L21,7.327L18,6.577L18,8.029ZM18,5.546L21,6.296L21,4.833L18,4.083L18,5.546ZM21,3.802L18,3.052L18,2.5C18,2.224 18.224,2 18.5,2L21,2L21,3.802Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1006 B

View File

@@ -0,0 +1,35 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MotionConfig } from 'framer-motion';
import { Suspense } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { HelmetProvider } from 'react-helmet-async';
import { AppRouter } from './AppRouter';
const queryClient = new QueryClient({
logger: undefined,
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: true,
networkMode: 'offlineFirst',
},
},
});
export function App() {
return (
<QueryClientProvider client={queryClient}>
<MotionConfig transition={{ duration: 0.1 }}>
<HelmetProvider>
<DndProvider backend={HTML5Backend}>
<Suspense>
<AppRouter />
{/*<ReactQueryDevtools initialIsOpen={false} />*/}
</Suspense>
</DndProvider>
</HelmetProvider>
</MotionConfig>
</QueryClientProvider>
);
}

View File

@@ -0,0 +1,81 @@
import { createBrowserRouter, Navigate, Outlet, RouterProvider } from 'react-router-dom';
import { routePaths, useAppRoutes } from '../hooks/useAppRoutes';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRequests } from '../hooks/useRequests';
import { GlobalHooks } from './GlobalHooks';
import Workspace from './Workspace';
import Workspaces from './Workspaces';
import { DialogProvider } from './DialogContext';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import RouteError from './RouteError';
const router = createBrowserRouter([
{
path: '/',
errorElement: <RouteError />,
element: <Layout />,
children: [
{
path: '/',
element: <Navigate to={routePaths.workspaces()} replace={true} />,
},
{
path: routePaths.workspaces(),
element: <Workspaces />,
},
{
path: routePaths.workspace({
workspaceId: ':workspaceId',
environmentId: ':environmentId',
}),
element: <WorkspaceOrRedirect />,
},
{
path: routePaths.request({
workspaceId: ':workspaceId',
environmentId: ':environmentId',
requestId: ':requestId',
}),
element: <Workspace />,
},
],
},
]);
export function AppRouter() {
return <RouterProvider router={router} />;
}
function WorkspaceOrRedirect() {
const recentRequests = useRecentRequests();
const activeEnvironmentId = useActiveEnvironmentId();
const requests = useRequests();
const request = requests.find((r) => r.id === recentRequests[0]);
const routes = useAppRoutes();
if (request === undefined) {
return <Workspace />;
}
const { id: requestId, workspaceId } = request;
const environmentId = activeEnvironmentId ?? undefined;
return (
<Navigate
to={routes.paths.request({
workspaceId,
environmentId,
requestId,
})}
/>
);
}
function Layout() {
return (
<DialogProvider>
<Outlet />
<GlobalHooks />
</DialogProvider>
);
}

View File

@@ -0,0 +1,45 @@
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import type { HttpRequest } from '../lib/models';
import { Input } from './core/Input';
import { VStack } from './core/Stacks';
interface Props {
requestId: string;
authentication: HttpRequest['authentication'];
}
export function BasicAuth({ requestId, authentication }: Props) {
const updateRequest = useUpdateRequest(requestId);
return (
<VStack className="my-2" space={2}>
<Input
useTemplating
label="Username"
name="username"
size="sm"
defaultValue={`${authentication.username}`}
onChange={(username: string) => {
updateRequest.mutate((r) => ({
...r,
authentication: { password: r.authentication.password, username },
}));
}}
/>
<Input
useTemplating
label="Password"
name="password"
size="sm"
type="password"
defaultValue={`${authentication.password}`}
onChange={(password: string) => {
updateRequest.mutate((r) => ({
...r,
authentication: { username: r.authentication.username, password },
}));
}}
/>
</VStack>
);
}

View File

@@ -0,0 +1,33 @@
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import type { HttpRequest } from '../lib/models';
import { Input } from './core/Input';
import { VStack } from './core/Stacks';
interface Props {
requestId: string;
authentication: HttpRequest['authentication'];
}
export function BearerAuth({ requestId, authentication }: Props) {
const updateRequest = useUpdateRequest(requestId);
return (
<VStack className="my-2" space={2}>
<Input
useTemplating
autocompleteVariables
type="password"
label="Token"
name="token"
size="sm"
defaultValue={`${authentication.token}`}
onChange={(token: string) => {
updateRequest.mutate((r) => ({
...r,
authentication: { token },
}));
}}
/>
</VStack>
);
}

View File

@@ -1,33 +0,0 @@
import classnames from 'classnames';
import { ButtonHTMLAttributes, forwardRef } from 'react';
import { Icon } from './Icon';
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
color?: 'primary' | 'secondary';
size?: 'sm' | 'md';
forDropdown?: boolean;
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{ className, children, size = 'md', forDropdown, color, ...props }: ButtonProps,
ref,
) {
return (
<button
ref={ref}
className={classnames(
className,
'rounded-md text-white flex items-center',
{ 'h-10 px-4': size === 'md' },
{ 'h-8 px-3': size === 'sm' },
{ 'hover:bg-gray-500/[0.1] active:bg-gray-500/[0.15]': color === undefined },
{ 'bg-blue-500 hover:bg-blue-500/90 active:bg-blue-500/80': color === 'primary' },
{ 'bg-violet-500 hover:bg-violet-500/90 active:bg-violet-500/80': color === 'secondary' },
)}
{...props}
>
{children}
{forDropdown && <Icon icon="triangle-down" className="ml-1 -mr-1" />}
</button>
);
});

View File

@@ -0,0 +1,65 @@
import React, { createContext, useContext, useMemo, useState } from 'react';
import type { DialogProps } from './core/Dialog';
import { Dialog } from './core/Dialog';
type DialogEntry = {
id: string;
render: ({ hide }: { hide: () => void }) => React.ReactNode;
} & Pick<DialogProps, 'title' | 'description' | 'hideX' | 'className' | 'size'>;
type DialogEntryOptionalId = Omit<DialogEntry, 'id'> & { id?: string };
interface State {
dialogs: DialogEntry[];
actions: Actions;
}
interface Actions {
show: (d: DialogEntryOptionalId) => void;
hide: (id: string) => void;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const DialogContext = createContext<State>({} as any);
export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
const [dialogs, setDialogs] = useState<State['dialogs']>([]);
const actions = useMemo<Actions>(
() => ({
show: ({ id: oid, ...props }: DialogEntryOptionalId) => {
const id = oid ?? Math.random().toString(36).slice(2);
setDialogs((a) => [...a.filter((d) => d.id !== id), { id, ...props }]);
},
hide: (id: string) => {
setDialogs((a) => a.filter((d) => d.id !== id));
},
}),
[],
);
const state: State = {
dialogs,
actions,
};
return (
<DialogContext.Provider value={state}>
{children}
{dialogs.map((props: DialogEntry) => (
<DialogInstance key={props.id} {...props} />
))}
</DialogContext.Provider>
);
};
function DialogInstance({ id, render, ...props }: DialogEntry) {
const { actions } = useContext(DialogContext);
const children = render({ hide: () => actions.hide(id) });
return (
<Dialog open onClose={() => actions.hide(id)} {...props}>
{children}
</Dialog>
);
}
export const useDialog = () => useContext(DialogContext).actions;

View File

@@ -0,0 +1,22 @@
import classNames from 'classnames';
import React, { memo } from 'react';
interface Props {
className?: string;
}
export const DropMarker = memo(
function DropMarker({ className }: Props) {
return (
<div
className={classNames(
className,
'relative w-full h-0 overflow-visible pointer-events-none',
)}
>
<div className="absolute z-50 left-2 right-2 -bottom-[0.1rem] h-[0.2rem] bg-blue-500/50 rounded-full" />
</div>
);
},
() => true,
);

View File

@@ -1,311 +0,0 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { DropdownMenuRadioGroup } from '@radix-ui/react-dropdown-menu';
import { motion } from 'framer-motion';
import {
CheckIcon,
ChevronRightIcon,
DotFilledIcon,
HamburgerMenuIcon,
} from '@radix-ui/react-icons';
import { forwardRef, HTMLAttributes, ReactNode, useState } from 'react';
import { Button } from './Button';
import classnames from 'classnames';
import { HotKey } from './HotKey';
interface DropdownMenuRadioProps {
children: ReactNode;
onValueChange: ((value: string) => void) | null;
value: string;
items: {
label: string;
value: string;
}[];
}
export function DropdownMenuRadio({
children,
items,
onValueChange,
value,
}: DropdownMenuRadioProps) {
return (
<DropdownMenu.Root>
<DropdownMenuTrigger>{children}</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent>
<DropdownMenuRadioGroup onValueChange={onValueChange ?? undefined} value={value}>
{items.map((item) => (
<DropdownMenuRadioItem key={item.value} value={item.value}>
{item.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenu.Root>
);
}
export function Dropdown() {
const [bookmarksChecked, setBookmarksChecked] = useState(true);
const [urlsChecked, setUrlsChecked] = useState(false);
const [person, setPerson] = useState('pedro');
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<Button aria-label="Customise options">
<HamburgerMenuIcon />
</Button>
</DropdownMenu.Trigger>
<DropdownMenuPortal>
<DropdownMenuContent>
<DropdownMenuItem rightSlot={<HotKey>T</HotKey>}>New Tab</DropdownMenuItem>
<DropdownMenuItem rightSlot={<HotKey>N</HotKey>}>New Window</DropdownMenuItem>
<DropdownMenuItem disabled rightSlot={<HotKey>N</HotKey>}>
New Private Window
</DropdownMenuItem>
<DropdownMenu.Sub>
<DropdownMenuSubTrigger rightSlot={<ChevronRightIcon />}>
More Tools
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem rightSlot={<HotKey>S</HotKey>}>Save Page As</DropdownMenuItem>
<DropdownMenuItem>Create Shortcut</DropdownMenuItem>
<DropdownMenuItem>Name Window</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Developer Tools</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenu.Sub>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
checked={bookmarksChecked}
onCheckedChange={(v) => setBookmarksChecked(!!v)}
rightSlot={<HotKey>B</HotKey>}
leftSlot={
<DropdownMenu.ItemIndicator className="DropdownMenuItemIndicator">
<CheckIcon />
</DropdownMenu.ItemIndicator>
}
>
Show Bookmarks
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={urlsChecked}
onCheckedChange={(v) => setUrlsChecked(!!v)}
leftSlot={
<DropdownMenu.ItemIndicator className="DropdownMenuItemIndicator">
<CheckIcon />
</DropdownMenu.ItemIndicator>
}
>
Show Full URLs
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuLabel>People</DropdownMenuLabel>
<DropdownMenu.RadioGroup value={person} onValueChange={setPerson}>
<DropdownMenuRadioItem value="pedro">Pedro Duarte</DropdownMenuRadioItem>
<DropdownMenuRadioItem className="DropdownMenuRadioItem" value="colm">
Colm Tuite
</DropdownMenuRadioItem>
</DropdownMenu.RadioGroup>
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenu.Root>
);
}
const dropdownMenuClasses = 'bg-background rounded-md shadow-lg p-1.5 border border-gray-100';
interface DropdownMenuPortalProps {
children: ReactNode;
}
function DropdownMenuPortal({ children }: DropdownMenuPortalProps) {
return (
<DropdownMenu.Portal container={document.querySelector<HTMLElement>('#radix-portal')}>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
{children}
</motion.div>
</DropdownMenu.Portal>
);
}
const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenu.DropdownMenuContentProps>(
function DropdownMenuContent(
{ className, children, ...props }: DropdownMenu.DropdownMenuContentProps,
ref,
) {
return (
<DropdownMenu.Content
ref={ref}
align="start"
className={classnames(className, dropdownMenuClasses, 'mt-1')}
{...props}
>
{children}
</DropdownMenu.Content>
);
},
);
type DropdownMenuItemProps = DropdownMenu.DropdownMenuItemProps & ItemInnerProps;
function DropdownMenuItem({
leftSlot,
rightSlot,
className,
children,
...props
}: DropdownMenuItemProps) {
return (
<DropdownMenu.Item
asChild
className={classnames(className, { 'opacity-30': props.disabled })}
{...props}
>
<ItemInner leftSlot={leftSlot} rightSlot={rightSlot}>
{children}
</ItemInner>
</DropdownMenu.Item>
);
}
type DropdownMenuCheckboxItemProps = DropdownMenu.DropdownMenuCheckboxItemProps & ItemInnerProps;
function DropdownMenuCheckboxItem({
leftSlot,
rightSlot,
children,
...props
}: DropdownMenuCheckboxItemProps) {
return (
<DropdownMenu.CheckboxItem asChild {...props}>
<ItemInner leftSlot={leftSlot} rightSlot={rightSlot}>
{children}
</ItemInner>
</DropdownMenu.CheckboxItem>
);
}
type DropdownMenuSubTriggerProps = DropdownMenu.DropdownMenuSubTriggerProps & ItemInnerProps;
function DropdownMenuSubTrigger({
leftSlot,
rightSlot,
children,
...props
}: DropdownMenuSubTriggerProps) {
return (
<DropdownMenu.SubTrigger asChild {...props}>
<ItemInner leftSlot={leftSlot} rightSlot={rightSlot}>
{children}
</ItemInner>
</DropdownMenu.SubTrigger>
);
}
type DropdownMenuRadioItemProps = Omit<
DropdownMenu.DropdownMenuRadioItemProps & ItemInnerProps,
'leftSlot'
>;
function DropdownMenuRadioItem({ rightSlot, children, ...props }: DropdownMenuRadioItemProps) {
return (
<DropdownMenu.RadioItem asChild {...props}>
<ItemInner
leftSlot={
<DropdownMenu.ItemIndicator>
<DotFilledIcon />
</DropdownMenu.ItemIndicator>
}
rightSlot={rightSlot}
>
{children}
</ItemInner>
</DropdownMenu.RadioItem>
);
}
const DropdownMenuSubContent = forwardRef<HTMLDivElement, DropdownMenu.DropdownMenuSubContentProps>(
function DropdownMenuSubContent(
{ className, ...props }: DropdownMenu.DropdownMenuSubContentProps,
ref,
) {
return (
<DropdownMenu.SubContent
ref={ref}
alignOffset={0}
sideOffset={4}
className={classnames(className, dropdownMenuClasses)}
{...props}
/>
);
},
);
function DropdownMenuLabel({ className, children, ...props }: DropdownMenu.DropdownMenuLabelProps) {
return (
<DropdownMenu.Label asChild {...props}>
<ItemInner noHover className={classnames(className, 'opacity-50 uppercase text-sm')}>
{children}
</ItemInner>
</DropdownMenu.Label>
);
}
function DropdownMenuSeparator({ className, ...props }: DropdownMenu.DropdownMenuSeparatorProps) {
return (
<DropdownMenu.Separator
className={classnames(className, 'h-[1px] bg-gray-400 bg-opacity-30 my-1')}
{...props}
/>
);
}
function DropdownMenuTrigger({ className, ...props }: DropdownMenu.DropdownMenuTriggerProps) {
return (
<DropdownMenu.Trigger
asChild
className={classnames(className, 'focus:outline-none')}
{...props}
/>
);
}
interface ItemInnerProps extends HTMLAttributes<HTMLDivElement> {
leftSlot?: ReactNode;
rightSlot?: ReactNode;
children: ReactNode;
noHover?: boolean;
}
const ItemInner = forwardRef<HTMLDivElement, ItemInnerProps>(function ItemInner(
{ leftSlot, rightSlot, children, className, noHover, ...props }: ItemInnerProps,
ref,
) {
return (
<div
ref={ref}
className={classnames(
className,
'outline-none px-2 py-1.5 flex items-center text-sm text-gray-700',
{
'focus:bg-gray-50 focus:text-gray-900 rounded': !noHover,
},
)}
{...props}
>
<div className="w-7">{leftSlot}</div>
<div>{children}</div>
{rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>}
</div>
);
});

View File

@@ -1,54 +0,0 @@
.cm-editor {
width: 100%;
overflow: hidden;
border-radius: var(--border-radius-lg);
}
.cm-editor .cm-scroller {
border-radius: var(--border-radius-lg);
background-color: hsl(var(--color-gray-50) / 0.5);
}
.cm-editor .cm-line {
padding-left: 1em;
padding-right: 1.5em;
color: hsl(var(--color-gray-900));
}
.cm-editor .cm-gutters {
background-color: transparent;
border-right: 0;
color: hsl(var(--color-gray-300));
}
.cm-editor .cm-foldPlaceholder {
background-color: hsl(var(--color-gray-100));
border: 1px solid hsl(var(--color-gray-200));
padding: 0 0.3em;
}
.cm-editor .cm-activeLineGutter,
.cm-editor .cm-activeLine {
background-color: hsl(var(--color-gray-50));
}
.cm-editor * {
cursor: text;
}
.cm-editor.cm-focused {
outline: 0;
box-shadow: 0 0 0 2pt rgba(180, 180, 180, 0.1);
}
.cm-editor .cm-cursor {
border-left: 2px solid red;
}
.cm-editor .cm-selectionBackground {
background-color: hsl(var(--color-gray-100));
}
.cm-editor.cm-focused .cm-selectionBackground {
background-color: hsl(var(--color-gray-100));
}

View File

@@ -1,12 +0,0 @@
import useCodeMirror from '../../hooks/useCodemirror';
import './Editor.css';
interface Props {
contentType: string;
value: string;
}
export default function Editor(props: Props) {
const { ref } = useCodeMirror({ value: props.value, contentType: props.contentType });
return <div ref={ref} className="m-0 text-sm overflow-hidden" />;
}

View File

@@ -0,0 +1,13 @@
import type { ReactNode } from 'react';
interface Props {
children: ReactNode;
}
export function EmptyStateText({ children }: Props) {
return (
<div className="rounded-lg border border-dashed border-highlight h-full text-gray-400 flex items-center justify-center">
{children}
</div>
);
}

View File

@@ -0,0 +1,79 @@
import classNames from 'classnames';
import { memo, useCallback, useMemo } from 'react';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useEnvironments } from '../hooks/useEnvironments';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { useDialog } from './DialogContext';
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
type Props = {
className?: string;
} & Pick<ButtonProps, 'forDropdown' | 'leftSlot'>;
export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdown({
className,
...buttonProps
}: Props) {
const environments = useEnvironments();
const activeEnvironment = useActiveEnvironment();
const dialog = useDialog();
const routes = useAppRoutes();
const showEnvironmentDialog = useCallback(() => {
dialog.show({
title: 'Manage Environments',
render: () => <EnvironmentEditDialog initialEnvironment={activeEnvironment} />,
});
}, [dialog, activeEnvironment]);
const items: DropdownItem[] = useMemo(
() => [
...environments.map(
(e) => ({
key: e.id,
label: e.name,
rightSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : undefined,
onSelect: async () => {
if (e.id !== activeEnvironment?.id) {
routes.setEnvironment(e);
} else {
routes.setEnvironment(null);
}
},
}),
[activeEnvironment?.id],
),
...((environments.length > 0
? [{ type: 'separator', label: 'Environments' }]
: []) as DropdownItem[]),
{
key: 'edit',
label: 'Manage Environments',
leftSlot: <Icon icon="gear" />,
onSelect: showEnvironmentDialog,
},
],
[activeEnvironment, environments, routes, showEnvironmentDialog],
);
return (
<Dropdown items={items}>
<Button
size="sm"
className={classNames(
className,
'text-gray-800 !px-2 truncate',
activeEnvironment == null && 'text-opacity-disabled italic',
)}
{...buttonProps}
>
{activeEnvironment?.name ?? 'No Environment'}
</Button>
</Dropdown>
);
});

View File

@@ -0,0 +1,238 @@
import classNames from 'classnames';
import { useCallback, useMemo, useState } from 'react';
import { useWindowSize } from 'react-use';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment';
import { useEnvironments } from '../hooks/useEnvironments';
import { usePrompt } from '../hooks/usePrompt';
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
import type { Environment, Workspace } from '../lib/models';
import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import type {
GenericCompletionConfig,
GenericCompletionOption,
} from './core/Editor/genericCompletion';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import type { PairEditorProps } from './core/PairEditor';
import { PairEditor } from './core/PairEditor';
import { HStack, VStack } from './core/Stacks';
interface Props {
initialEnvironment: Environment | null;
}
export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string | null>(
initialEnvironment?.id ?? null,
);
const environments = useEnvironments();
const createEnvironment = useCreateEnvironment();
const activeWorkspace = useActiveWorkspace();
const windowSize = useWindowSize();
const showSidebar = windowSize.width > 500;
const selectedEnvironment = useMemo(
() => environments.find((e) => e.id === selectedEnvironmentId) ?? null,
[environments, selectedEnvironmentId],
);
return (
<div
className={classNames(
'h-full grid gap-x-8 grid-rows-[minmax(0,1fr)]',
showSidebar ? 'grid-cols-[auto_minmax(0,1fr)]' : 'grid-cols-[minmax(0,1fr)]',
)}
>
{showSidebar && (
<aside className="grid grid-rows-[minmax(0,1fr)_auto] gap-y-0.5 h-full max-w-[250px] pr-3 border-r border-gray-100 -ml-2">
<div className="min-w-0 h-full w-full overflow-y-scroll">
<SidebarButton
active={selectedEnvironment == null}
onClick={() => setSelectedEnvironmentId(null)}
>
Base Environment
</SidebarButton>
<div className="ml-3 pl-2 border-l border-highlight">
{environments.map((e) => (
<SidebarButton
key={e.id}
active={selectedEnvironment?.id === e.id}
onClick={() => setSelectedEnvironmentId(e.id)}
>
{e.name}
</SidebarButton>
))}
</div>
</div>
<Button
size="sm"
className="w-full text-center"
color="gray"
justify="center"
onClick={() => createEnvironment.mutate()}
>
New Environment
</Button>
</aside>
)}
{activeWorkspace != null ? (
<EnvironmentEditor environment={selectedEnvironment} workspace={activeWorkspace} />
) : (
<div className="flex w-full h-full items-center justify-center text-gray-400 italic">
select an environment
</div>
)}
</div>
);
};
const EnvironmentEditor = function ({
environment,
workspace,
}: {
environment: Environment | null;
workspace: Workspace;
}) {
const environments = useEnvironments();
const updateEnvironment = useUpdateEnvironment(environment?.id ?? 'n/a');
const updateWorkspace = useUpdateWorkspace(workspace.id);
const deleteEnvironment = useDeleteEnvironment(environment);
const variables = environment == null ? workspace.variables : environment.variables;
const handleChange = useCallback<PairEditorProps['onChange']>(
(variables) => {
if (environment != null) {
updateEnvironment.mutate({ variables });
} else {
updateWorkspace.mutate({ variables });
}
},
[updateWorkspace, updateEnvironment, environment],
);
// Gather a list of env names from other environments, to help the user get them aligned
const nameAutocomplete = useMemo<GenericCompletionConfig>(() => {
const otherEnvironments = environments.filter((e) => e.id !== environment?.id);
const allVariableNames =
environment == null
? [
// Nothing to autocomplete if we're in the base environment
]
: [
...workspace.variables.map((v) => v.name),
...otherEnvironments.flatMap((e) => e.variables.map((v) => v.name)),
];
// Filter out empty strings and variables that already exist
const variableNames = allVariableNames.filter(
(name) => name != '' && !variables.find((v) => v.name === name),
);
const uniqueVariableNames = [...new Set(variableNames)];
const options = uniqueVariableNames.map(
(name): GenericCompletionOption => ({
label: name,
type: 'constant',
}),
);
return { options };
}, [environments, variables, workspace, environment]);
const prompt = usePrompt();
const items = useMemo<DropdownItem[] | null>(
() =>
environment == null
? null
: [
{
key: 'rename',
label: 'Rename',
leftSlot: <Icon icon="pencil" size="sm" />,
onSelect: async () => {
const name = await prompt({
title: 'Rename Environment',
description: (
<>
Enter a new name for <InlineCode>{environment.name}</InlineCode>
</>
),
name: 'name',
label: 'Name',
defaultValue: environment.name,
});
updateEnvironment.mutate({ name });
},
},
{
key: 'delete',
variant: 'danger',
label: 'Delete',
leftSlot: <Icon icon="trash" size="sm" />,
onSelect: () => deleteEnvironment.mutate(),
},
],
[deleteEnvironment, updateEnvironment, prompt, environment],
);
const validateName = useCallback((name: string) => {
// Empty just means the variable doesn't have a name yet, and is unusable
if (name === '') return true;
return name.match(/^[a-z_][a-z0-9_]*$/i) != null;
}, []);
return (
<VStack space={2}>
<HStack space={2} className="justify-between">
<h1 className="text-xl">{environment?.name ?? 'Base Environment'}</h1>
{items != null && (
<Dropdown items={items}>
<IconButton icon="gear" title="Environment Actions" size="sm" className="!h-auto w-8" />
</Dropdown>
)}
</HStack>
<PairEditor
nameAutocomplete={nameAutocomplete}
nameAutocompleteVariables={false}
namePlaceholder="VAR_NAME"
valuePlaceholder="variable value"
nameValidate={validateName}
valueAutocompleteVariables={false}
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
pairs={variables}
onChange={handleChange}
/>
</VStack>
);
};
function SidebarButton({
children,
className,
active,
onClick,
}: {
className?: string;
children: string;
active: boolean;
onClick: () => void;
}) {
return (
<button
tabIndex={active ? 0 : -1}
onClick={onClick}
className={classNames(
className,
'flex items-center text-sm text-left w-full mb-1 h-xs rounded px-2',
'text-gray-600 hocus:text-gray-800 focus:bg-highlightSecondary outline-none',
active && '!text-gray-900',
)}
>
{children}
</button>
);
}

View File

@@ -0,0 +1,143 @@
import { useQueryClient } from '@tanstack/react-query';
import { appWindow } from '@tauri-apps/api/window';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useEffectOnce } from 'react-use';
import { keyValueQueryKey } from '../hooks/useKeyValue';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { requestsQueryKey } from '../hooks/useRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { responsesQueryKey } from '../hooks/useResponses';
import { workspacesQueryKey } from '../hooks/useWorkspaces';
import { trackPage } from '../lib/analytics';
import { DEFAULT_FONT_SIZE } from '../lib/constants';
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models';
import { modelsEq } from '../lib/models';
import { setPathname } from '../lib/persistPathname';
export function GlobalHooks() {
// Include here so they always update, even
// if no component references them
useRecentWorkspaces();
useRecentEnvironments();
useRecentRequests();
const queryClient = useQueryClient();
const { wasUpdatedExternally } = useRequestUpdateKey(null);
// Listen for location changes and update the pathname
const location = useLocation();
useEffect(() => {
setPathname(location.pathname).catch(console.error);
}, [location.pathname]);
useEffectOnce(() => {
trackPage('/');
});
useListenToTauriEvent<Model>('created_model', ({ payload, windowLabel }) => {
if (shouldIgnoreEvent(payload, windowLabel)) return;
const queryKey =
payload.model === 'http_request'
? requestsQueryKey(payload)
: payload.model === 'http_response'
? responsesQueryKey(payload)
: payload.model === 'workspace'
? workspacesQueryKey(payload)
: payload.model === 'key_value'
? keyValueQueryKey(payload)
: null;
if (queryKey === null) {
console.log('Unrecognized created model:', payload);
return;
}
if (!shouldIgnoreModel(payload)) {
// Order newest first
queryClient.setQueryData<Model[]>(queryKey, (values) => [payload, ...(values ?? [])]);
}
});
useListenToTauriEvent<Model>('updated_model', ({ payload, windowLabel }) => {
if (shouldIgnoreEvent(payload, windowLabel)) return;
const queryKey =
payload.model === 'http_request'
? requestsQueryKey(payload)
: payload.model === 'http_response'
? responsesQueryKey(payload)
: payload.model === 'workspace'
? workspacesQueryKey(payload)
: payload.model === 'key_value'
? keyValueQueryKey(payload)
: null;
if (queryKey === null) {
console.log('Unrecognized updated model:', payload);
return;
}
if (payload.model === 'http_request') {
wasUpdatedExternally(payload.id);
}
if (!shouldIgnoreModel(payload)) {
queryClient.setQueryData<Model[]>(queryKey, (values) =>
values?.map((v) => (modelsEq(v, payload) ? payload : v)),
);
}
});
useListenToTauriEvent<Model>('deleted_model', ({ payload, windowLabel }) => {
if (shouldIgnoreEvent(payload, windowLabel)) return;
if (shouldIgnoreModel(payload)) return;
if (payload.model === 'workspace') {
queryClient.setQueryData<Workspace[]>(workspacesQueryKey(), removeById(payload));
} else if (payload.model === 'http_request') {
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey(payload), removeById(payload));
} else if (payload.model === 'http_response') {
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey(payload), removeById(payload));
} else if (payload.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(payload), undefined);
}
});
useListenToTauriEvent<number>('zoom', ({ payload: zoomDelta, windowLabel }) => {
if (windowLabel !== appWindow.label) return;
const fontSize = parseFloat(window.getComputedStyle(document.documentElement).fontSize);
let newFontSize;
if (zoomDelta === 0) {
newFontSize = DEFAULT_FONT_SIZE;
} else if (zoomDelta > 0) {
newFontSize = Math.min(fontSize * 1.1, DEFAULT_FONT_SIZE * 5);
} else if (zoomDelta < 0) {
newFontSize = Math.max(fontSize * 0.9, DEFAULT_FONT_SIZE * 0.4);
}
document.documentElement.style.fontSize = `${newFontSize}px`;
});
return null;
}
function removeById<T extends { id: string }>(model: T) {
return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id);
}
const shouldIgnoreEvent = (payload: Model, windowLabel: string) =>
windowLabel === appWindow.label && payload.model !== 'http_response';
const shouldIgnoreModel = (payload: Model) => {
if (payload.model === 'key_value') {
return payload.namespace === NAMESPACE_NO_SYNC;
}
return false;
};

View File

@@ -0,0 +1,135 @@
import { updateSchema } from 'cm6-graphql';
import type { EditorView } from 'codemirror';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useIntrospectGraphQL } from '../hooks/useIntrospectGraphQL';
import type { HttpRequest } from '../lib/models';
import { Button } from './core/Button';
import type { EditorProps } from './core/Editor';
import { Editor, formatGraphQL } from './core/Editor';
import { FormattedError } from './core/FormattedError';
import { Separator } from './core/Separator';
import { useDialog } from './DialogContext';
type Props = Pick<
EditorProps,
'heightMode' | 'onChange' | 'defaultValue' | 'className' | 'forceUpdateKey'
> & {
baseRequest: HttpRequest;
};
interface GraphQLBody {
query: string;
variables?: Record<string, string | number | boolean | null>;
operationName?: string;
}
export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEditorProps }: Props) {
const editorViewRef = useRef<EditorView>(null);
const { schema, isLoading, error, refetch } = useIntrospectGraphQL(baseRequest);
const { query, variables } = useMemo<GraphQLBody>(() => {
if (defaultValue === undefined) {
return { query: '', variables: {} };
}
try {
const p = JSON.parse(defaultValue ?? '{}');
const query = p.query ?? '';
const variables = p.variables;
const operationName = p.operationName;
return { query, variables, operationName };
} catch (err) {
return { query: 'failed to parse' };
}
}, [defaultValue]);
const handleChange = useCallback(
(b: GraphQLBody) => onChange?.(JSON.stringify(b, null, 2)),
[onChange],
);
const handleChangeQuery = useCallback(
(query: string) => handleChange({ query, variables }),
[handleChange, variables],
);
const handleChangeVariables = useCallback(
(variables: string) => {
try {
handleChange({ query, variables: JSON.parse(variables) });
} catch (e) {
// Meh, not much we can do here
}
},
[handleChange, query],
);
// Refetch the schema when the URL changes
useEffect(() => {
if (editorViewRef.current === null) return;
updateSchema(editorViewRef.current, schema);
}, [schema]);
const dialog = useDialog();
return (
<div className="pb-2 h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
<Editor
contentType="application/graphql"
defaultValue={query ?? ''}
format={formatGraphQL}
heightMode="auto"
onChange={handleChangeQuery}
placeholder="..."
ref={editorViewRef}
actions={
(error || isLoading) && (
<Button
size="xs"
color={error ? 'danger' : 'gray'}
isLoading={isLoading}
onClick={() => {
dialog.show({
title: 'Introspection Failed',
size: 'sm',
id: 'introspection-failed',
render: () => (
<div className="whitespace-pre-wrap">
<FormattedError>{error ?? 'unknown'}</FormattedError>
<div className="w-full mt-3">
<Button
onClick={() => {
dialog.hide('introspection-failed');
refetch();
}}
className="ml-auto"
color="secondary"
size="sm"
>
Try Again
</Button>
</div>
</div>
),
});
}}
>
{error ? 'Introspection Failed' : 'Introspecting'}
</Button>
)
}
{...extraEditorProps}
/>
<Separator variant="primary" />
<p className="pt-1 text-gray-500 text-sm">Variables</p>
<Editor
contentType="application/json"
defaultValue={JSON.stringify(variables, null, 2)}
heightMode="auto"
onChange={handleChangeVariables}
placeholder="{}"
useTemplating
autocompleteVariables
{...extraEditorProps}
/>
</div>
);
}

View File

@@ -1,35 +0,0 @@
import classnames from 'classnames';
import {HTMLAttributes} from 'react';
const colsClasses = {
none: 'grid-cols-none',
1: 'grid-cols-1',
2: 'grid-cols-2',
};
const rowsClasses = {
none: 'grid-rows-none',
1: 'grid-rows-1',
2: 'grid-rows-2',
};
const gapClasses = {
0: 'gap-0',
1: 'gap-1',
2: 'gap-2',
};
type Props = HTMLAttributes<HTMLElement> & {
rows?: keyof typeof rowsClasses;
cols?: keyof typeof colsClasses;
gap?: keyof typeof gapClasses;
};
export function Grid({ className, cols, gap, ...props }: Props) {
return (
<div
className={classnames(className, 'grid', cols && colsClasses[cols], gap && gapClasses[gap])}
{...props}
/>
);
}

View File

@@ -0,0 +1,71 @@
import { charsets } from '../lib/data/charsets';
import { connections } from '../lib/data/connections';
import { encodings } from '../lib/data/encodings';
import { headerNames } from '../lib/data/headerNames';
import { mimeTypes } from '../lib/data/mimetypes';
import type { HttpRequest } from '../lib/models';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import type { PairEditorProps } from './core/PairEditor';
import { PairEditor } from './core/PairEditor';
type Props = {
forceUpdateKey: string;
headers: HttpRequest['headers'];
onChange: (headers: HttpRequest['headers']) => void;
};
export function HeaderEditor({ headers, onChange, forceUpdateKey }: Props) {
return (
<PairEditor
valueAutocompleteVariables
nameAutocompleteVariables
pairs={headers}
onChange={onChange}
forceUpdateKey={forceUpdateKey}
nameValidate={validateHttpHeader}
nameAutocomplete={nameAutocomplete}
valueAutocomplete={valueAutocomplete}
namePlaceholder="Header-Name"
/>
);
}
const MIN_MATCH = 3;
const headerOptionsMap: Record<string, string[]> = {
'content-type': mimeTypes,
accept: ['*/*', ...mimeTypes],
'accept-encoding': encodings,
connection: connections,
'accept-charset': charsets,
};
const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => {
const name = headerName.toLowerCase().trim();
const options: GenericCompletionConfig['options'] =
headerOptionsMap[name]?.map((o) => ({
label: o,
type: 'constant',
boost: 1, // Put above other completions
})) ?? [];
return { minMatch: MIN_MATCH, options };
};
const nameAutocomplete: PairEditorProps['nameAutocomplete'] = {
minMatch: MIN_MATCH,
options: headerNames.map((t) => ({
label: t,
type: 'constant',
boost: 1, // Put above other completions
})),
};
const validateHttpHeader = (v: string) => {
if (v === '') {
return true;
}
// Template strings are not allowed so we replace them with a valid example string
const withoutTemplateStrings = v.replace(/\$\{\[\s*[^\]\s]+\s*]}/gi, '123');
return withoutTemplateStrings.match(/^[a-zA-Z0-9-_]+$/) !== null;
};

View File

@@ -1,15 +0,0 @@
import { HTMLAttributes } from 'react';
import classnames from 'classnames';
export function HotKey({ children }: HTMLAttributes<HTMLSpanElement>) {
return (
<span
className={classnames(
'bg-gray-400 bg-opacity-20 px-1.5 py-0.5 rounded text-sm',
'font-mono text-gray-500 tracking-widest',
)}
>
{children}
</span>
);
}

View File

@@ -1,34 +0,0 @@
import { ComponentType } from 'react';
import {
ArchiveIcon,
CameraIcon,
ChevronDownIcon,
GearIcon,
HomeIcon,
TriangleDownIcon,
} from '@radix-ui/react-icons';
import classnames from 'classnames';
type IconName = 'archive' | 'home' | 'camera' | 'gear' | 'triangle-down';
const icons: Record<IconName, ComponentType> = {
archive: ArchiveIcon,
home: HomeIcon,
camera: CameraIcon,
gear: GearIcon,
'triangle-down': TriangleDownIcon,
};
export interface IconProps {
icon: IconName;
className?: string;
}
export function Icon({ icon, className }: IconProps) {
const Component = icons[icon];
return (
<div className={classnames(className, 'flex items-center')}>
<Component />
</div>
);
}

View File

@@ -1,16 +0,0 @@
import { forwardRef } from 'react';
import { Icon, IconProps } from './Icon';
import { Button, ButtonProps } from './Button';
type Props = ButtonProps & IconProps;
export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton(
{ icon, ...props }: Props,
ref,
) {
return (
<Button ref={ref} className="group" {...props}>
<Icon icon={icon} className="text-gray-700 group-hover:text-gray-900" />
</Button>
);
});

View File

@@ -1,34 +0,0 @@
import { InputHTMLAttributes } from 'react';
import classnames from 'classnames';
import { VStack } from './Stacks';
interface Props extends InputHTMLAttributes<HTMLInputElement> {
name: string;
label: string;
hideLabel?: boolean;
labelClassName?: string;
}
export function Input({ label, labelClassName, hideLabel, className, name, ...props }: Props) {
const id = `input-${name}`;
return (
<VStack className="w-full">
<label
htmlFor={name}
className={classnames(labelClassName, 'font-semibold text-sm uppercase text-gray-700', {
'sr-only': hideLabel,
})}
>
{label}
</label>
<input
id={id}
className={classnames(
className,
'w-0 min-w-[100%] border-2 border-gray-100 bg-gray-50 h-10 pl-3 pr-2 rounded-md text-sm focus:outline-none',
)}
{...props}
/>
</VStack>
);
}

View File

@@ -0,0 +1,55 @@
import classNames from 'classnames';
import FocusTrap from 'focus-trap-react';
import { motion } from 'framer-motion';
import type { ReactNode } from 'react';
import { Portal } from './Portal';
interface Props {
children: ReactNode;
portalName: string;
open: boolean;
onClose?: () => void;
zIndex?: keyof typeof zIndexes;
variant?: 'default' | 'transparent';
}
const zIndexes: Record<number, string> = {
10: 'z-10',
20: 'z-20',
30: 'z-30',
40: 'z-40',
50: 'z-50',
};
export function Overlay({
variant = 'default',
zIndex = 30,
open,
onClose,
portalName,
children,
}: Props) {
return (
<Portal name={portalName}>
{open && (
<FocusTrap>
<motion.div
className={classNames('fixed inset-0', zIndexes[zIndex])}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<div
aria-hidden
onClick={onClose}
className={classNames(
'absolute inset-0',
variant === 'default' && 'bg-gray-600/30 dark:bg-black/30 backdrop-blur-sm',
)}
/>
<div className="bg-red-100">{children}</div>
</motion.div>
</FocusTrap>
)}
</Portal>
);
}

Some files were not shown because too many files have changed in this diff Show More