From 394beb374e1870f0bef6731d60ce9fe31aacac3e Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 9 Feb 2024 05:01:00 -0800 Subject: [PATCH] gRPC Support (#20) --- package-lock.json | 312 +++- package.json | 3 + ...a97174431f008a9fb4ce39667d587a858b876.json | 12 - ...75514ea2a44504e3c7a568a9578c64b5713d1.json | 12 + ...458ffbcd8c0850aef16ba1f70e358623ac66a.json | 12 + ...52bf7213fe01b5346266c0a89dc0ac89eda64.json | 12 + ...0c1b18e8f1ba3c1fdfb38c91ffd7bada97f59.json | 68 + ...d4e40a1945d640962991f879928619950ef62.json | 12 + ...1067f4e70e85ef8829b7aaad5b1993c3d01e8.json | 68 + ...3fb65f42d3226cec27220469558e14973328c.json | 68 + ...df83d83af5d88ea0b84514fdc877a668c27cd.json | 12 + ...aa25054545503704e19f149987f99b1a0e4f0.json | 12 + ...6da92cc30919d5bdb0a0226ea5e30d5b30c0f.json | 12 - ...8a1d8003fa17d5585bc126debb18cae670460.json | 12 + ...9a5f7a3d82b379ff9af36645dcfb92548fdd.json} | 4 +- ...fff5e3ec4619b962d149fdd4d618fe02c680.json} | 28 +- ...4a1093739e9abba69f6fe5527d453fab4db8.json} | 4 +- ...b811317e66fc84ac0906bf5513d938121a078.json | 12 - ...88b4055fec4ba7d9102131bd255c037fa021.json} | 4 +- ...3041d9a55194bb97819225a2612fdeb60ad42.json | 92 + ...a9c1746c81ca50d2c413e540b74c8c8e908b7.json | 92 + ...aa0ade7e35a6b56cb058e2caf9ca005ce6407.json | 68 + ...31c2c6a3c24774a505280dcba169eb5b6b0fb.json | 12 - ...7029b6c78de227c6fa3a85d75d0a7f21e0e9.json} | 4 +- ...27c514bab4039ab5edadc79b77dfdfd63b208.json | 12 + ...fbb5e5ad0be952a0d832448d65cc5f0effc1.json} | 4 +- ...ae47591405e1b5bec1229f2e2734c73e66163.json | 12 + ...3bd109c070603396a890dc717e50020d006f5.json | 12 - ...4ef093a1696d7b7ecaf694d12e5fafd62b685.json | 12 + ...45deac2a5e9bfb814b60191f16b98ed49796.json} | 4 +- ...7e55127a5bce60cbb499b83d1701386a23cb.json} | 4 +- ...bc0a9c000a631ee0d751a9dc4f3e76de3d57c.json | 12 + ...7821bdb51fcfe747170bea41e7a366d736bda.json | 12 - ...241f06b49719085a695b897ef8ad409d2cef2.json | 12 - ...9187e9cc5fc647067eaa5c738cb24e2f081e5.json | 12 - ...4443342796cb72bdd43a821fae2994ae8e2f.json} | 22 +- ...d8e12cf29d6d243fffdd62ade2ab70c7bddda.json | 12 + ...fc30eaeffeed6883e712bda4b4d6ca49cf740.json | 12 + ...7edad59e4f5998b15c50ed6eae2e97064068.json} | 4 +- ...c1cbd668bcf00ebe1a5f36616700c84972b39.json | 12 + src-tauri/Cargo.lock | 600 +++++- src-tauri/Cargo.toml | 5 +- src-tauri/grpc/Cargo.toml | 22 + src-tauri/grpc/src/codec.rs | 52 + src-tauri/grpc/src/json_schema.rs | 179 ++ src-tauri/grpc/src/lib.rs | 40 + src-tauri/grpc/src/manager.rs | 280 +++ src-tauri/grpc/src/proto.rs | 226 +++ src-tauri/migrations/20240203164833_grpc.sql | 59 + .../migrations/20240206191206_grpc-protos.sql | 1 + src-tauri/src/analytics.rs | 27 +- src-tauri/src/http.rs | 36 +- src-tauri/src/main.rs | 1660 ++++++++++++----- src-tauri/src/models.rs | 957 +++++++--- src-tauri/src/updates.rs | 14 +- src-web/components/AppRouter.tsx | 4 +- src-web/components/BasicAuth.tsx | 4 +- src-web/components/BearerAuth.tsx | 4 +- src-web/components/GlobalHooks.tsx | 81 +- src-web/components/GraphQLEditor.tsx | 31 +- src-web/components/GrpcConnectionLayout.tsx | 119 ++ .../components/GrpcConnectionMessagesPane.tsx | 127 ++ .../components/GrpcConnectionSetupPane.tsx | 235 +++ src-web/components/GrpcEditor.tsx | 153 ++ src-web/components/GrpcProtoSelection.tsx | 148 ++ src-web/components/HttpRequestLayout.tsx | 29 + .../components/RecentConnectionsDropdown.tsx | 60 + src-web/components/RecentRequestsDropdown.tsx | 4 +- .../components/RecentResponsesDropdown.tsx | 8 +- src-web/components/RequestPane.tsx | 270 +-- src-web/components/RequestResponse.tsx | 139 -- src-web/components/ResizeHandle.tsx | 6 +- src-web/components/ResponsePane.tsx | 23 +- src-web/components/SettingsDialog.tsx | 33 +- src-web/components/SettingsDropdown.tsx | 2 +- src-web/components/Sidebar.tsx | 168 +- src-web/components/SidebarActions.tsx | 32 +- src-web/components/UrlBar.tsx | 93 +- src-web/components/Workspace.tsx | 21 +- .../components/WorkspaceActionsDropdown.tsx | 36 +- src-web/components/core/Button.tsx | 51 +- src-web/components/core/CountBadge.tsx | 2 +- src-web/components/core/Dropdown.tsx | 4 +- src-web/components/core/Editor/Editor.tsx | 2 +- src-web/components/core/Editor/extensions.ts | 4 +- src-web/components/core/FormattedError.tsx | 4 +- src-web/components/core/Icon.tsx | 21 +- src-web/components/core/InlineCode.tsx | 3 +- src-web/components/core/JsonAttributeTree.tsx | 122 ++ src-web/components/core/Link.tsx | 35 + src-web/components/core/Select.tsx | 15 +- src-web/components/core/Separator.tsx | 3 +- src-web/components/core/SplitLayout.tsx | 169 ++ src-web/components/core/Stacks.tsx | 4 +- .../components/responseViewers/JsonViewer.tsx | 25 + src-web/hooks/Confirm.tsx | 8 +- src-web/hooks/useActiveRequest.ts | 31 +- src-web/hooks/useAlert.ts | 29 +- src-web/hooks/useCookieJars.ts | 2 +- src-web/hooks/useCreateCookieJar.ts | 16 +- src-web/hooks/useCreateEnvironment.ts | 2 +- src-web/hooks/useCreateFolder.ts | 2 +- src-web/hooks/useCreateGrpcRequest.ts | 55 + ...eateRequest.ts => useCreateHttpRequest.ts} | 8 +- src-web/hooks/useCreateWorkspace.ts | 2 +- src-web/hooks/useDeleteAnyGrpcRequest.tsx | 43 + ...equest.tsx => useDeleteAnyHttpRequest.tsx} | 18 +- src-web/hooks/useDeleteCookieJar.tsx | 2 +- src-web/hooks/useDeleteEnvironment.tsx | 2 +- src-web/hooks/useDeleteFolder.tsx | 6 +- src-web/hooks/useDeleteGrpcConnection.ts | 21 + src-web/hooks/useDeleteGrpcConnections.ts | 19 + ...teResponse.ts => useDeleteHttpResponse.ts} | 8 +- ...Responses.ts => useDeleteHttpResponses.ts} | 8 +- src-web/hooks/useDeleteRequest.tsx | 4 +- src-web/hooks/useDeleteWorkspace.tsx | 8 +- src-web/hooks/useDuplicateGrpcRequest.ts | 41 + ...eRequest.ts => useDuplicateHttpRequest.ts} | 8 +- src-web/hooks/useEnvironments.ts | 2 +- src-web/hooks/useExportData.tsx | 2 +- src-web/hooks/useFilterResponse.ts | 2 +- src-web/hooks/useFolders.ts | 2 +- src-web/hooks/useGrpc.ts | 79 + src-web/hooks/useGrpcConnections.ts | 23 + src-web/hooks/useGrpcMessages.ts | 23 + src-web/hooks/useGrpcRequest.ts | 7 + src-web/hooks/useGrpcRequests.ts | 22 + src-web/hooks/useHotKey.ts | 57 +- src-web/hooks/useHttpRequest.ts | 7 + .../{useRequests.ts => useHttpRequests.ts} | 8 +- .../{useResponses.ts => useHttpResponses.ts} | 8 +- src-web/hooks/useImportData.tsx | 2 +- src-web/hooks/useIsResponseLoading.ts | 4 +- src-web/hooks/useKeyValue.ts | 17 +- src-web/hooks/useLatestGrpcConnection.ts | 7 + src-web/hooks/useLatestHttpResponse.ts | 7 + src-web/hooks/useLatestResponse.ts | 7 - src-web/hooks/useRequest.ts | 7 - src-web/hooks/useSendAnyRequest.ts | 6 +- src-web/hooks/useSettings.ts | 2 +- src-web/hooks/useUpdateAnyFolder.ts | 2 +- src-web/hooks/useUpdateAnyGrpcRequest.ts | 36 + ...yRequest.ts => useUpdateAnyHttpRequest.ts} | 14 +- src-web/hooks/useUpdateCookieJar.ts | 2 +- src-web/hooks/useUpdateEnvironment.ts | 2 +- src-web/hooks/useUpdateGrpcRequest.ts | 12 + ...dateRequest.ts => useUpdateHttpRequest.ts} | 6 +- src-web/hooks/useUpdateSettings.ts | 2 +- src-web/hooks/useUpdateWorkspace.ts | 2 +- src-web/hooks/useVariables.ts | 2 +- src-web/hooks/useWorkspaces.ts | 2 +- src-web/lib/analytics.ts | 5 +- src-web/lib/fallbackRequestName.ts | 23 +- src-web/lib/formatters.ts | 5 +- src-web/lib/keyValueStore.ts | 4 +- src-web/lib/minPromiseMillis.ts | 14 +- src-web/lib/models.ts | 35 + src-web/lib/sendEphemeralRequest.ts | 2 +- src-web/lib/store.ts | 33 +- src-web/main.css | 9 + src-web/main.tsx | 10 +- tailwind.config.cjs | 5 +- 162 files changed, 6670 insertions(+), 1770 deletions(-) delete mode 100644 src-tauri/.sqlx/query-02506ad41cc94cd937422ef1977a97174431f008a9fb4ce39667d587a858b876.json create mode 100644 src-tauri/.sqlx/query-11394af12419cca3be3a26dff9275514ea2a44504e3c7a568a9578c64b5713d1.json create mode 100644 src-tauri/.sqlx/query-12b265491d1ebba19e1ce8a660e458ffbcd8c0850aef16ba1f70e358623ac66a.json create mode 100644 src-tauri/.sqlx/query-13cb883199e81966174e6fda9c252bf7213fe01b5346266c0a89dc0ac89eda64.json create mode 100644 src-tauri/.sqlx/query-196ed792c8d96425d428cb9609b0c1b18e8f1ba3c1fdfb38c91ffd7bada97f59.json create mode 100644 src-tauri/.sqlx/query-2c9658a639c5e4994ae9c8ec30bd4e40a1945d640962991f879928619950ef62.json create mode 100644 src-tauri/.sqlx/query-3330be44d8851f8e3456c403b5d1067f4e70e85ef8829b7aaad5b1993c3d01e8.json create mode 100644 src-tauri/.sqlx/query-3c52c0fa3372cdd2657a775c3b93fb65f42d3226cec27220469558e14973328c.json create mode 100644 src-tauri/.sqlx/query-42bc0ded60b44dab19daf6d8fc7df83d83af5d88ea0b84514fdc877a668c27cd.json create mode 100644 src-tauri/.sqlx/query-48ec5fdf20f34add763c540061caa25054545503704e19f149987f99b1a0e4f0.json delete mode 100644 src-tauri/.sqlx/query-4a5fd6c81401ccafac64b05cb476da92cc30919d5bdb0a0226ea5e30d5b30c0f.json create mode 100644 src-tauri/.sqlx/query-4b45b681698cbfe8531a7c3ba368a1d8003fa17d5585bc126debb18cae670460.json rename src-tauri/.sqlx/{query-1428d25b6aa3d6ec55742a968571fa951da0406d7bb32408883c584eae7dd53c.json => query-558e72df3c6f2635c6b3d52a199f9a5f7a3d82b379ff9af36645dcfb92548fdd.json} (72%) rename src-tauri/.sqlx/{query-6483f3ffeb90e019e9078d98bb831b8e4fbedfb45751d6cd33bd42e518b634dd.json => query-573db23160de025e5c72efb90be7fff5e3ec4619b962d149fdd4d618fe02c680.json} (70%) rename src-tauri/.sqlx/{query-689bcc92b914f50c14921faa796c07a256deb84c832fc3d90200b393fb159417.json => query-5765e9565a8b89c5bc2d72197e0e4a1093739e9abba69f6fe5527d453fab4db8.json} (68%) delete mode 100644 src-tauri/.sqlx/query-587aedf827b00bb706c35457a75b811317e66fc84ac0906bf5513d938121a078.json rename src-tauri/.sqlx/{query-f2ba4708d4a9ff9ce74c407a730040bd7883e9a5c0eb79ef0d8a6782a8eae299.json => query-612efa9ac45723dc604a88f5e7e288b4055fec4ba7d9102131bd255c037fa021.json} (70%) create mode 100644 src-tauri/.sqlx/query-7398403d3de2dc5c5b4b6392f083041d9a55194bb97819225a2612fdeb60ad42.json create mode 100644 src-tauri/.sqlx/query-761d27c3ec425c37ad9abe9c732a9c1746c81ca50d2c413e540b74c8c8e908b7.json create mode 100644 src-tauri/.sqlx/query-80a85f83d0946d532a60f0add87aa0ade7e35a6b56cb058e2caf9ca005ce6407.json delete mode 100644 src-tauri/.sqlx/query-86a9d12d7b00217f3143671908c31c2c6a3c24774a505280dcba169eb5b6b0fb.json rename src-tauri/.sqlx/{query-20cf0f8b71e600bc40ee204b60a12b2f3728178f3b8788d2e5cc92821b74d470.json => query-8dfbae65ddec905ea3734448cc9f7029b6c78de227c6fa3a85d75d0a7f21e0e9.json} (71%) create mode 100644 src-tauri/.sqlx/query-9238f94c688d91f42627e5b73c627c514bab4039ab5edadc79b77dfdfd63b208.json rename src-tauri/.sqlx/{query-e08fa4f9b2929f20a01d1dc43d6847a309d3e8c5b324df2d039d1c6e07e6eb2f.json => query-9ba3f783238b77637ffded4171b2fbb5e5ad0be952a0d832448d65cc5f0effc1.json} (70%) create mode 100644 src-tauri/.sqlx/query-9d7bc2b0eb0c09652d9826db4a7ae47591405e1b5bec1229f2e2734c73e66163.json delete mode 100644 src-tauri/.sqlx/query-a1c9a862ca6a07476cb8e7d16d73bd109c070603396a890dc717e50020d006f5.json create mode 100644 src-tauri/.sqlx/query-a690a04cd1ebe8c3dbfd0cd98ae4ef093a1696d7b7ecaf694d12e5fafd62b685.json rename src-tauri/.sqlx/{query-1517b0f86c841b5f1247bd40c3a9b38ab001d846a410b6e3cd36f9e844d50ddb.json => query-ae98a7b35a5cb80a4bcd04faa22545deac2a5e9bfb814b60191f16b98ed49796.json} (71%) rename src-tauri/.sqlx/{query-3b3fb6271340c6ec21a10b4f1b20502c86c425e0b53ac07692f8a4ed0be09335.json => query-b32994b09ae7a06eb0f031069d327e55127a5bce60cbb499b83d1701386a23cb.json} (70%) create mode 100644 src-tauri/.sqlx/query-b3fae40a793a6724dd2286a9ca4bc0a9c000a631ee0d751a9dc4f3e76de3d57c.json delete mode 100644 src-tauri/.sqlx/query-b5ed4dc77f8cf21de1a06f146e47821bdb51fcfe747170bea41e7a366d736bda.json delete mode 100644 src-tauri/.sqlx/query-cae4e905515ddea1ec2cd685020241f06b49719085a695b897ef8ad409d2cef2.json delete mode 100644 src-tauri/.sqlx/query-dcc2f405f8e29d0599d86bcde509187e9cc5fc647067eaa5c738cb24e2f081e5.json rename src-tauri/.sqlx/{query-7a7bc4df7e52ad3a987c97af8f43b46381e2cc16ba0c553713d0b6c64354eb39.json => query-e61c0dddb3e86d271cb9399faa4e4443342796cb72bdd43a821fae2994ae8e2f.json} (70%) create mode 100644 src-tauri/.sqlx/query-e7124f5570076bfd65985744f48d8e12cf29d6d243fffdd62ade2ab70c7bddda.json create mode 100644 src-tauri/.sqlx/query-ee562f85ec28c554c607adde670fc30eaeffeed6883e712bda4b4d6ca49cf740.json rename src-tauri/.sqlx/{query-cb939b45a715d91f7631dea6b2d1bdc59fb3dffbd44ff99bc15adb34ea7093f7.json => query-f5f20f3b37d932617499a0da50997edad59e4f5998b15c50ed6eae2e97064068.json} (69%) create mode 100644 src-tauri/.sqlx/query-fe0652396bc30d926cf99083651c1cbd668bcf00ebe1a5f36616700c84972b39.json create mode 100644 src-tauri/grpc/Cargo.toml create mode 100644 src-tauri/grpc/src/codec.rs create mode 100644 src-tauri/grpc/src/json_schema.rs create mode 100644 src-tauri/grpc/src/lib.rs create mode 100644 src-tauri/grpc/src/manager.rs create mode 100644 src-tauri/grpc/src/proto.rs create mode 100644 src-tauri/migrations/20240203164833_grpc.sql create mode 100644 src-tauri/migrations/20240206191206_grpc-protos.sql create mode 100644 src-web/components/GrpcConnectionLayout.tsx create mode 100644 src-web/components/GrpcConnectionMessagesPane.tsx create mode 100644 src-web/components/GrpcConnectionSetupPane.tsx create mode 100644 src-web/components/GrpcEditor.tsx create mode 100644 src-web/components/GrpcProtoSelection.tsx create mode 100644 src-web/components/HttpRequestLayout.tsx create mode 100644 src-web/components/RecentConnectionsDropdown.tsx delete mode 100644 src-web/components/RequestResponse.tsx create mode 100644 src-web/components/core/JsonAttributeTree.tsx create mode 100644 src-web/components/core/Link.tsx create mode 100644 src-web/components/core/SplitLayout.tsx create mode 100644 src-web/components/responseViewers/JsonViewer.tsx create mode 100644 src-web/hooks/useCreateGrpcRequest.ts rename src-web/hooks/{useCreateRequest.ts => useCreateHttpRequest.ts} (87%) create mode 100644 src-web/hooks/useDeleteAnyGrpcRequest.tsx rename src-web/hooks/{useDeleteAnyRequest.tsx => useDeleteAnyHttpRequest.tsx} (67%) create mode 100644 src-web/hooks/useDeleteGrpcConnection.ts create mode 100644 src-web/hooks/useDeleteGrpcConnections.ts rename src-web/hooks/{useDeleteResponse.ts => useDeleteHttpResponse.ts} (65%) rename src-web/hooks/{useDeleteResponses.ts => useDeleteHttpResponses.ts} (63%) create mode 100644 src-web/hooks/useDuplicateGrpcRequest.ts rename src-web/hooks/{useDuplicateRequest.ts => useDuplicateHttpRequest.ts} (84%) create mode 100644 src-web/hooks/useGrpc.ts create mode 100644 src-web/hooks/useGrpcConnections.ts create mode 100644 src-web/hooks/useGrpcMessages.ts create mode 100644 src-web/hooks/useGrpcRequest.ts create mode 100644 src-web/hooks/useGrpcRequests.ts create mode 100644 src-web/hooks/useHttpRequest.ts rename src-web/hooks/{useRequests.ts => useHttpRequests.ts} (62%) rename src-web/hooks/{useResponses.ts => useHttpResponses.ts} (52%) create mode 100644 src-web/hooks/useLatestGrpcConnection.ts create mode 100644 src-web/hooks/useLatestHttpResponse.ts delete mode 100644 src-web/hooks/useLatestResponse.ts delete mode 100644 src-web/hooks/useRequest.ts create mode 100644 src-web/hooks/useUpdateAnyGrpcRequest.ts rename src-web/hooks/{useUpdateAnyRequest.ts => useUpdateAnyHttpRequest.ts} (68%) create mode 100644 src-web/hooks/useUpdateGrpcRequest.ts rename src-web/hooks/{useUpdateRequest.ts => useUpdateHttpRequest.ts} (62%) diff --git a/package-lock.json b/package-lock.json index 48ded9bf..02a63d27 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,9 @@ "classnames": "^2.3.2", "cm6-graphql": "^0.0.9", "codemirror": "^6.0.1", + "codemirror-json-schema": "^0.6.1", + "codemirror-json5": "^1.0.3", + "date-fns": "^3.3.1", "focus-trap-react": "^10.1.1", "format-graphql": "^1.4.0", "framer-motion": "^9.0.4", @@ -469,6 +472,30 @@ "node": ">=6.9.0" } }, + "node_modules/@changesets/changelog-github": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.4.8.tgz", + "integrity": "sha512-jR1DHibkMAb5v/8ym77E4AMNWZKB5NPzw5a5Wtqm1JepAuIF+hrKp2u04NKM14oBZhHglkCfrla9uq8ORnK/dw==", + "dependencies": { + "@changesets/get-github-info": "^0.5.2", + "@changesets/types": "^5.2.1", + "dotenv": "^8.1.0" + } + }, + "node_modules/@changesets/get-github-info": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.5.2.tgz", + "integrity": "sha512-JppheLu7S114aEs157fOZDjFqUDpm7eHdq5E8SSR0gUBTEK0cNSHsrSR5a66xs0z3RWuo46QvA3vawp8BxDHvg==", + "dependencies": { + "dataloader": "^1.4.0", + "node-fetch": "^2.5.0" + } + }, + "node_modules/@changesets/types": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-5.2.1.tgz", + "integrity": "sha512-myLfHbVOqaq9UtUKqR/nZA/OY7xFjQMdfgfqeZIBK4d0hA6pgxArvdv8M+6NUzzBsjWLOtvApv8YHr4qM+Kpfg==" + }, "node_modules/@codemirror/autocomplete": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.2.0.tgz", @@ -511,16 +538,6 @@ "@lezer/javascript": "^1.0.0" } }, - "node_modules/@codemirror/lang-javascript/node_modules/@codemirror/view": { - "version": "6.21.4", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.21.4.tgz", - "integrity": "sha512-WKVZ7nvN0lwWPfAf05WxWqTpwjC8YN3q5goj3CsSig7//DD81LULgOx3nBegqpqP0iygBqRmW8z0KSc2QTAdAg==", - "dependencies": { - "@codemirror/state": "^6.1.4", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, "node_modules/@codemirror/lang-json": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", @@ -556,9 +573,9 @@ } }, "node_modules/@codemirror/lint": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.2.1.tgz", - "integrity": "sha512-y1muai5U/uUPAGRyHMx9mHuHLypPcHWxzlZGknp/U5Mdb5Ol8Q5ZLp67UqyTbNFJJ3unVxZ8iX3g1fMN79S1JQ==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.4.2.tgz", + "integrity": "sha512-wzRkluWb1ptPKdzlsrbwwjYCPLgzU6N88YBAmlZi8WFyuiEduSd05MnJYNogzyc8rPK7pj6m95ptUApc8sHKVA==", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", @@ -576,17 +593,17 @@ } }, "node_modules/@codemirror/state": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz", - "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==" + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.0.tgz", + "integrity": "sha512-hm8XshYj5Fo30Bb922QX9hXB/bxOAVH+qaqHBzw5TKa72vOeslyGwd4X8M0c1dJ9JqxlaMceOQ8RsL9tC7gU0A==" }, "node_modules/@codemirror/view": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.2.1.tgz", - "integrity": "sha512-r1svbtAj2Lp/86F3yy1TfDAOAtJRGLINLSEqByETyUaGo1EnLS+P+bbGCVHV62z46BzZYm16noDid69+4bzn0g==", + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.23.1.tgz", + "integrity": "sha512-J2Xnn5lFYT1ZN/5ewEoMBCmLlL71lZ3mBdb7cUEuHhX2ESoSrNEucpsDXpX22EuTGm9LOgC9v4Z0wx+Ez8QmGA==", "dependencies": { - "@codemirror/state": "^6.0.0", - "style-mod": "^4.0.0", + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, @@ -1350,6 +1367,20 @@ } } }, + "node_modules/@sagold/json-pointer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sagold/json-pointer/-/json-pointer-5.1.1.tgz", + "integrity": "sha512-/iskWuyGNu09qy09HYmyLnvzpKryymH9T+vTBi2LdFp1TuKvERDADvPMv2ZkQKsrRklOzivmOz9QXof0dKqvgA==" + }, + "node_modules/@sagold/json-query": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@sagold/json-query/-/json-query-6.1.1.tgz", + "integrity": "sha512-5/Wu0rTnXmO5Uvtm9Of16Vx3mKjSnYA0Um9LgBtyPhIucYlppKgKC4N3g8gD0Fk00a5kizQTs4gwxKPXCpmeww==", + "dependencies": { + "@sagold/json-pointer": "^5.1.1", + "ebnf": "^1.9.1" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -2174,8 +2205,7 @@ "node_modules/@types/json-schema": { "version": "7.0.14", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", - "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", - "dev": true + "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -3537,6 +3567,53 @@ "@codemirror/view": "^6.0.0" } }, + "node_modules/codemirror-json-schema": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/codemirror-json-schema/-/codemirror-json-schema-0.6.1.tgz", + "integrity": "sha512-QG12Jy917eStZzxurpAE9QUQxF8SS/AYJ9DDteyJZcRGH8ePaBCfQ4KLCNtY6cUKjFeNBgcd5+c6FPAri6pPQg==", + "dependencies": { + "@changesets/changelog-github": "^0.4.8", + "@sagold/json-pointer": "^5.1.1", + "@types/json-schema": "^7.0.12", + "@types/node": "^20.4.2", + "json-schema": "^0.4.0", + "json-schema-library": "^9.1.2" + }, + "optionalDependencies": { + "@codemirror/lang-json": "^6.0.1", + "codemirror-json5": "^1.0.3", + "json5": "^2.2.3" + }, + "peerDependencies": { + "@codemirror/language": "^6.8.0", + "@codemirror/lint": "^6.4.0", + "@codemirror/state": "^6.2.1", + "@codemirror/view": "^6.14.1", + "@lezer/common": "^1.0.3" + } + }, + "node_modules/codemirror-json-schema/node_modules/@types/node": { + "version": "20.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.10.tgz", + "integrity": "sha512-rZEfe/hJSGYmdfX9tvcPMYeYPW2sNl50nsw4jZmRcaG0HIAb0WYEpsB05GOb53vjqpyE9GUhlDQ4jLSoB5q9kg==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/codemirror-json5": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/codemirror-json5/-/codemirror-json5-1.0.3.tgz", + "integrity": "sha512-HmmoYO2huQxoaoG5ARKjqQc9mz7/qmNPvMbISVfIE2Gk1+4vZQg9X3G6g49MYM5IK00Ol3aijd7OKrySuOkA7Q==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "json5": "^2.2.1", + "lezer-json5": "^2.0.2" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -3724,6 +3801,20 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/dataloader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-1.4.0.tgz", + "integrity": "sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==" + }, + "node_modules/date-fns": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz", + "integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3803,6 +3894,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -3885,6 +3984,11 @@ "node": ">=8" } }, + "node_modules/discontinuous-range": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", + "integrity": "sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==" + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -3924,6 +4028,14 @@ "node": ">=4" } }, + "node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "engines": { + "node": ">=10" + } + }, "node_modules/duplexer3": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", @@ -3936,6 +4048,14 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ebnf": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ebnf/-/ebnf-1.9.1.tgz", + "integrity": "sha512-uW2UKSsuty9ANJ3YByIQE4ANkD8nqUPO7r6Fwcc1ADKPe9FRdcPpMl3VEput4JSvKBJ4J86npIC2MLP0pYkCuw==", + "bin": { + "ebnf": "dist/bin.js" + } + }, "node_modules/electron": { "version": "23.3.13", "resolved": "https://registry.npmjs.org/electron/-/electron-23.3.13.tgz", @@ -4777,6 +4897,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/fast-copy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz", + "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6097,6 +6222,25 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, + "node_modules/json-schema-library": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/json-schema-library/-/json-schema-library-9.1.2.tgz", + "integrity": "sha512-uQnFb2V+VakLl6XIGGtUQzfjkP31f/dCT5lJq9NOUdypSSpjbWL/V0R2KvoNJp3hU8VErwh9DqVoZPqlC+B3IA==", + "dependencies": { + "@sagold/json-pointer": "^5.1.1", + "@sagold/json-query": "^6.1.1", + "deepmerge": "^4.3.1", + "fast-copy": "^3.0.1", + "fast-deep-equal": "^3.1.3", + "smtp-address-parser": "1.0.10", + "valid-url": "^1.0.9" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6120,7 +6264,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -6207,6 +6350,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lezer-json5": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lezer-json5/-/lezer-json5-2.0.2.tgz", + "integrity": "sha512-NRmtBlKW/f8mA7xatKq8IUOq045t8GVHI4kZXrUtYYUdiVeGiO6zKGAV7/nUAnf5q+rYTY+SWX/gvQdFXMjNxQ==", + "dependencies": { + "@lezer/lr": "^1.0.0" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -6590,6 +6741,11 @@ "ufo": "^1.3.0" } }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -6654,12 +6810,57 @@ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", "dev": true }, + "node_modules/nearley": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", + "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "dependencies": { + "commander": "^2.19.0", + "moo": "^0.5.0", + "railroad-diagrams": "^1.0.0", + "randexp": "0.4.6" + }, + "bin": { + "nearley-railroad": "bin/nearley-railroad.js", + "nearley-test": "bin/nearley-test.js", + "nearley-unparse": "bin/nearley-unparse.js", + "nearleyc": "bin/nearleyc.js" + }, + "funding": { + "type": "individual", + "url": "https://nearley.js.org/#give-to-nearley" + } + }, + "node_modules/nearley/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-releases": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", @@ -7606,6 +7807,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==" + }, + "node_modules/randexp": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", + "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "dependencies": { + "discontinuous-range": "1.0.0", + "ret": "~0.1.10" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -8114,6 +8332,14 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "engines": { + "node": ">=0.12" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -8523,6 +8749,17 @@ "node": ">=8.0.0" } }, + "node_modules/smtp-address-parser": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/smtp-address-parser/-/smtp-address-parser-1.0.10.tgz", + "integrity": "sha512-Osg9LmvGeAG/hyao4mldbflLOkkr3a+h4m1lwKCK5U8M6ZAr7tdXEz/+/vr752TSGE4MNUlUl9cIK2cB8cgzXg==", + "dependencies": { + "nearley": "^2.20.1" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -9253,6 +9490,11 @@ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-easing": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", @@ -9448,8 +9690,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "devOptional": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unique-string": { "version": "1.0.0", @@ -9578,6 +9819,11 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/valid-url": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz", + "integrity": "sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==" + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -9780,6 +10026,20 @@ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 0408d97b..7c0e80b7 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,9 @@ "classnames": "^2.3.2", "cm6-graphql": "^0.0.9", "codemirror": "^6.0.1", + "codemirror-json-schema": "^0.6.1", + "codemirror-json5": "^1.0.3", + "date-fns": "^3.3.1", "focus-trap-react": "^10.1.1", "format-graphql": "^1.4.0", "framer-motion": "^9.0.4", diff --git a/src-tauri/.sqlx/query-02506ad41cc94cd937422ef1977a97174431f008a9fb4ce39667d587a858b876.json b/src-tauri/.sqlx/query-02506ad41cc94cd937422ef1977a97174431f008a9fb4ce39667d587a858b876.json deleted file mode 100644 index 14b52a77..00000000 --- a/src-tauri/.sqlx/query-02506ad41cc94cd937422ef1977a97174431f008a9fb4ce39667d587a858b876.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO folders (\n id,\n workspace_id,\n folder_id,\n name,\n sort_priority\n )\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n sort_priority = excluded.sort_priority\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 5 - }, - "nullable": [] - }, - "hash": "02506ad41cc94cd937422ef1977a97174431f008a9fb4ce39667d587a858b876" -} diff --git a/src-tauri/.sqlx/query-11394af12419cca3be3a26dff9275514ea2a44504e3c7a568a9578c64b5713d1.json b/src-tauri/.sqlx/query-11394af12419cca3be3a26dff9275514ea2a44504e3c7a568a9578c64b5713d1.json new file mode 100644 index 00000000..c09da185 --- /dev/null +++ b/src-tauri/.sqlx/query-11394af12419cca3be3a26dff9275514ea2a44504e3c7a568a9578c64b5713d1.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO http_requests (\n id, workspace_id, folder_id, name, url, url_parameters, method, body, body_type,\n authentication, authentication_type, headers, sort_priority\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n authentication = excluded.authentication,\n authentication_type = excluded.authentication_type,\n url = excluded.url,\n url_parameters = excluded.url_parameters,\n sort_priority = excluded.sort_priority\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "11394af12419cca3be3a26dff9275514ea2a44504e3c7a568a9578c64b5713d1" +} diff --git a/src-tauri/.sqlx/query-12b265491d1ebba19e1ce8a660e458ffbcd8c0850aef16ba1f70e358623ac66a.json b/src-tauri/.sqlx/query-12b265491d1ebba19e1ce8a660e458ffbcd8c0850aef16ba1f70e358623ac66a.json new file mode 100644 index 00000000..007998de --- /dev/null +++ b/src-tauri/.sqlx/query-12b265491d1ebba19e1ce8a660e458ffbcd8c0850aef16ba1f70e358623ac66a.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO workspaces (\n id, name, description, variables, setting_request_timeout,\n setting_follow_redirects, setting_validate_certificates\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n description = excluded.description,\n variables = excluded.variables,\n setting_request_timeout = excluded.setting_request_timeout,\n setting_follow_redirects = excluded.setting_follow_redirects,\n setting_validate_certificates = excluded.setting_validate_certificates\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "12b265491d1ebba19e1ce8a660e458ffbcd8c0850aef16ba1f70e358623ac66a" +} diff --git a/src-tauri/.sqlx/query-13cb883199e81966174e6fda9c252bf7213fe01b5346266c0a89dc0ac89eda64.json b/src-tauri/.sqlx/query-13cb883199e81966174e6fda9c252bf7213fe01b5346266c0a89dc0ac89eda64.json new file mode 100644 index 00000000..e14799c2 --- /dev/null +++ b/src-tauri/.sqlx/query-13cb883199e81966174e6fda9c252bf7213fe01b5346266c0a89dc0ac89eda64.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO environments (\n id, workspace_id, name, variables\n )\n VALUES (?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n variables = excluded.variables\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "13cb883199e81966174e6fda9c252bf7213fe01b5346266c0a89dc0ac89eda64" +} diff --git a/src-tauri/.sqlx/query-196ed792c8d96425d428cb9609b0c1b18e8f1ba3c1fdfb38c91ffd7bada97f59.json b/src-tauri/.sqlx/query-196ed792c8d96425d428cb9609b0c1b18e8f1ba3c1fdfb38c91ffd7bada97f59.json new file mode 100644 index 00000000..572209d2 --- /dev/null +++ b/src-tauri/.sqlx/query-196ed792c8d96425d428cb9609b0c1b18e8f1ba3c1fdfb38c91ffd7bada97f59.json @@ -0,0 +1,68 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id, model, workspace_id, request_id, connection_id, created_at, message,\n is_server, is_info\n FROM grpc_messages\n WHERE connection_id = ?\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "model", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "workspace_id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "request_id", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "connection_id", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 5, + "type_info": "Datetime" + }, + { + "name": "message", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "is_server", + "ordinal": 7, + "type_info": "Bool" + }, + { + "name": "is_info", + "ordinal": 8, + "type_info": "Bool" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "196ed792c8d96425d428cb9609b0c1b18e8f1ba3c1fdfb38c91ffd7bada97f59" +} diff --git a/src-tauri/.sqlx/query-2c9658a639c5e4994ae9c8ec30bd4e40a1945d640962991f879928619950ef62.json b/src-tauri/.sqlx/query-2c9658a639c5e4994ae9c8ec30bd4e40a1945d640962991f879928619950ef62.json new file mode 100644 index 00000000..9f666e11 --- /dev/null +++ b/src-tauri/.sqlx/query-2c9658a639c5e4994ae9c8ec30bd4e40a1945d640962991f879928619950ef62.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO http_responses (\n id, request_id, workspace_id, elapsed, elapsed_headers, url, status, status_reason,\n content_length, body_path, headers, version, remote_addr\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 13 + }, + "nullable": [] + }, + "hash": "2c9658a639c5e4994ae9c8ec30bd4e40a1945d640962991f879928619950ef62" +} diff --git a/src-tauri/.sqlx/query-3330be44d8851f8e3456c403b5d1067f4e70e85ef8829b7aaad5b1993c3d01e8.json b/src-tauri/.sqlx/query-3330be44d8851f8e3456c403b5d1067f4e70e85ef8829b7aaad5b1993c3d01e8.json new file mode 100644 index 00000000..ea56c49f --- /dev/null +++ b/src-tauri/.sqlx/query-3330be44d8851f8e3456c403b5d1067f4e70e85ef8829b7aaad5b1993c3d01e8.json @@ -0,0 +1,68 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id, model, workspace_id, request_id, created_at, updated_at, service,\n method, elapsed\n FROM grpc_connections\n WHERE id = ?\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "model", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "workspace_id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "request_id", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "updated_at", + "ordinal": 5, + "type_info": "Datetime" + }, + { + "name": "service", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "method", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "elapsed", + "ordinal": 8, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "3330be44d8851f8e3456c403b5d1067f4e70e85ef8829b7aaad5b1993c3d01e8" +} diff --git a/src-tauri/.sqlx/query-3c52c0fa3372cdd2657a775c3b93fb65f42d3226cec27220469558e14973328c.json b/src-tauri/.sqlx/query-3c52c0fa3372cdd2657a775c3b93fb65f42d3226cec27220469558e14973328c.json new file mode 100644 index 00000000..c7bf9ae3 --- /dev/null +++ b/src-tauri/.sqlx/query-3c52c0fa3372cdd2657a775c3b93fb65f42d3226cec27220469558e14973328c.json @@ -0,0 +1,68 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id, model, workspace_id, request_id, connection_id, created_at, message,\n is_server, is_info\n FROM grpc_messages\n WHERE id = ?\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "model", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "workspace_id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "request_id", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "connection_id", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 5, + "type_info": "Datetime" + }, + { + "name": "message", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "is_server", + "ordinal": 7, + "type_info": "Bool" + }, + { + "name": "is_info", + "ordinal": 8, + "type_info": "Bool" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "3c52c0fa3372cdd2657a775c3b93fb65f42d3226cec27220469558e14973328c" +} diff --git a/src-tauri/.sqlx/query-42bc0ded60b44dab19daf6d8fc7df83d83af5d88ea0b84514fdc877a668c27cd.json b/src-tauri/.sqlx/query-42bc0ded60b44dab19daf6d8fc7df83d83af5d88ea0b84514fdc877a668c27cd.json new file mode 100644 index 00000000..4276a419 --- /dev/null +++ b/src-tauri/.sqlx/query-42bc0ded60b44dab19daf6d8fc7df83d83af5d88ea0b84514fdc877a668c27cd.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM grpc_connections\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "42bc0ded60b44dab19daf6d8fc7df83d83af5d88ea0b84514fdc877a668c27cd" +} diff --git a/src-tauri/.sqlx/query-48ec5fdf20f34add763c540061caa25054545503704e19f149987f99b1a0e4f0.json b/src-tauri/.sqlx/query-48ec5fdf20f34add763c540061caa25054545503704e19f149987f99b1a0e4f0.json new file mode 100644 index 00000000..a959caf8 --- /dev/null +++ b/src-tauri/.sqlx/query-48ec5fdf20f34add763c540061caa25054545503704e19f149987f99b1a0e4f0.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE settings SET (\n theme, appearance, update_channel\n ) = (?, ?, ?) WHERE id = 'default';\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "48ec5fdf20f34add763c540061caa25054545503704e19f149987f99b1a0e4f0" +} diff --git a/src-tauri/.sqlx/query-4a5fd6c81401ccafac64b05cb476da92cc30919d5bdb0a0226ea5e30d5b30c0f.json b/src-tauri/.sqlx/query-4a5fd6c81401ccafac64b05cb476da92cc30919d5bdb0a0226ea5e30d5b30c0f.json deleted file mode 100644 index fed65bea..00000000 --- a/src-tauri/.sqlx/query-4a5fd6c81401ccafac64b05cb476da92cc30919d5bdb0a0226ea5e30d5b30c0f.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n folder_id,\n name,\n url,\n url_parameters,\n method,\n body,\n body_type,\n authentication,\n authentication_type,\n headers,\n sort_priority\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n authentication = excluded.authentication,\n authentication_type = excluded.authentication_type,\n url = excluded.url,\n url_parameters = excluded.url_parameters,\n sort_priority = excluded.sort_priority\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 13 - }, - "nullable": [] - }, - "hash": "4a5fd6c81401ccafac64b05cb476da92cc30919d5bdb0a0226ea5e30d5b30c0f" -} diff --git a/src-tauri/.sqlx/query-4b45b681698cbfe8531a7c3ba368a1d8003fa17d5585bc126debb18cae670460.json b/src-tauri/.sqlx/query-4b45b681698cbfe8531a7c3ba368a1d8003fa17d5585bc126debb18cae670460.json new file mode 100644 index 00000000..0329c901 --- /dev/null +++ b/src-tauri/.sqlx/query-4b45b681698cbfe8531a7c3ba368a1d8003fa17d5585bc126debb18cae670460.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO grpc_messages (\n id, workspace_id, request_id, connection_id, message, is_server, is_info\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n message = excluded.message,\n is_server = excluded.is_server,\n is_info = excluded.is_info\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "4b45b681698cbfe8531a7c3ba368a1d8003fa17d5585bc126debb18cae670460" +} diff --git a/src-tauri/.sqlx/query-1428d25b6aa3d6ec55742a968571fa951da0406d7bb32408883c584eae7dd53c.json b/src-tauri/.sqlx/query-558e72df3c6f2635c6b3d52a199f9a5f7a3d82b379ff9af36645dcfb92548fdd.json similarity index 72% rename from src-tauri/.sqlx/query-1428d25b6aa3d6ec55742a968571fa951da0406d7bb32408883c584eae7dd53c.json rename to src-tauri/.sqlx/query-558e72df3c6f2635c6b3d52a199f9a5f7a3d82b379ff9af36645dcfb92548fdd.json index 591fed5f..4e9ece1c 100644 --- a/src-tauri/.sqlx/query-1428d25b6aa3d6ec55742a968571fa951da0406d7bb32408883c584eae7dd53c.json +++ b/src-tauri/.sqlx/query-558e72df3c6f2635c6b3d52a199f9a5f7a3d82b379ff9af36645dcfb92548fdd.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n folder_id,\n name,\n sort_priority\n FROM folders\n WHERE id = ?\n ", + "query": "\n SELECT\n id, model, workspace_id, created_at, updated_at, folder_id, name, sort_priority\n FROM folders\n WHERE workspace_id = ?\n ", "describe": { "columns": [ { @@ -58,5 +58,5 @@ false ] }, - "hash": "1428d25b6aa3d6ec55742a968571fa951da0406d7bb32408883c584eae7dd53c" + "hash": "558e72df3c6f2635c6b3d52a199f9a5f7a3d82b379ff9af36645dcfb92548fdd" } diff --git a/src-tauri/.sqlx/query-6483f3ffeb90e019e9078d98bb831b8e4fbedfb45751d6cd33bd42e518b634dd.json b/src-tauri/.sqlx/query-573db23160de025e5c72efb90be7fff5e3ec4619b962d149fdd4d618fe02c680.json similarity index 70% rename from src-tauri/.sqlx/query-6483f3ffeb90e019e9078d98bb831b8e4fbedfb45751d6cd33bd42e518b634dd.json rename to src-tauri/.sqlx/query-573db23160de025e5c72efb90be7fff5e3ec4619b962d149fdd4d618fe02c680.json index 9156664c..17d6f5d0 100644 --- a/src-tauri/.sqlx/query-6483f3ffeb90e019e9078d98bb831b8e4fbedfb45751d6cd33bd42e518b634dd.json +++ b/src-tauri/.sqlx/query-573db23160de025e5c72efb90be7fff5e3ec4619b962d149fdd4d618fe02c680.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n url_parameters AS \"url_parameters!: sqlx::types::Json>\",\n method,\n body AS \"body!: Json>\",\n body_type,\n authentication AS \"authentication!: Json>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_requests\n WHERE id = ?\n ", + "query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, url, method,\n body_type, authentication_type, sort_priority,\n url_parameters AS \"url_parameters!: sqlx::types::Json>\",\n body AS \"body!: Json>\",\n authentication AS \"authentication!: Json>\",\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_requests\n WHERE id = ?\n ", "describe": { "columns": [ { @@ -44,39 +44,39 @@ "type_info": "Text" }, { - "name": "url_parameters!: sqlx::types::Json>", + "name": "method", "ordinal": 8, "type_info": "Text" }, { - "name": "method", + "name": "body_type", "ordinal": 9, "type_info": "Text" }, { - "name": "body!: Json>", + "name": "authentication_type", "ordinal": 10, "type_info": "Text" }, { - "name": "body_type", + "name": "sort_priority", "ordinal": 11, - "type_info": "Text" + "type_info": "Float" }, { - "name": "authentication!: Json>", + "name": "url_parameters!: sqlx::types::Json>", "ordinal": 12, "type_info": "Text" }, { - "name": "authentication_type", + "name": "body!: Json>", "ordinal": 13, "type_info": "Text" }, { - "name": "sort_priority", + "name": "authentication!: Json>", "ordinal": 14, - "type_info": "Float" + "type_info": "Text" }, { "name": "headers!: sqlx::types::Json>", @@ -97,14 +97,14 @@ false, false, false, - false, - false, + true, true, false, - true, + false, + false, false, false ] }, - "hash": "6483f3ffeb90e019e9078d98bb831b8e4fbedfb45751d6cd33bd42e518b634dd" + "hash": "573db23160de025e5c72efb90be7fff5e3ec4619b962d149fdd4d618fe02c680" } diff --git a/src-tauri/.sqlx/query-689bcc92b914f50c14921faa796c07a256deb84c832fc3d90200b393fb159417.json b/src-tauri/.sqlx/query-5765e9565a8b89c5bc2d72197e0e4a1093739e9abba69f6fe5527d453fab4db8.json similarity index 68% rename from src-tauri/.sqlx/query-689bcc92b914f50c14921faa796c07a256deb84c832fc3d90200b393fb159417.json rename to src-tauri/.sqlx/query-5765e9565a8b89c5bc2d72197e0e4a1093739e9abba69f6fe5527d453fab4db8.json index a68986dd..13966583 100644 --- a/src-tauri/.sqlx/query-689bcc92b914f50c14921faa796c07a256deb84c832fc3d90200b393fb159417.json +++ b/src-tauri/.sqlx/query-5765e9565a8b89c5bc2d72197e0e4a1093739e9abba69f6fe5527d453fab4db8.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n variables AS \"variables!: sqlx::types::Json>\"\n FROM environments\n WHERE id = ?\n ", + "query": "\n SELECT\n id, model, workspace_id, created_at, updated_at, name,\n variables AS \"variables!: sqlx::types::Json>\"\n FROM environments\n WHERE id = ?\n ", "describe": { "columns": [ { @@ -52,5 +52,5 @@ false ] }, - "hash": "689bcc92b914f50c14921faa796c07a256deb84c832fc3d90200b393fb159417" + "hash": "5765e9565a8b89c5bc2d72197e0e4a1093739e9abba69f6fe5527d453fab4db8" } diff --git a/src-tauri/.sqlx/query-587aedf827b00bb706c35457a75b811317e66fc84ac0906bf5513d938121a078.json b/src-tauri/.sqlx/query-587aedf827b00bb706c35457a75b811317e66fc84ac0906bf5513d938121a078.json deleted file mode 100644 index ead64668..00000000 --- a/src-tauri/.sqlx/query-587aedf827b00bb706c35457a75b811317e66fc84ac0906bf5513d938121a078.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE http_responses SET (\n elapsed,\n elapsed_headers,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n error,\n headers,\n version,\n remote_addr,\n updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 12 - }, - "nullable": [] - }, - "hash": "587aedf827b00bb706c35457a75b811317e66fc84ac0906bf5513d938121a078" -} diff --git a/src-tauri/.sqlx/query-f2ba4708d4a9ff9ce74c407a730040bd7883e9a5c0eb79ef0d8a6782a8eae299.json b/src-tauri/.sqlx/query-612efa9ac45723dc604a88f5e7e288b4055fec4ba7d9102131bd255c037fa021.json similarity index 70% rename from src-tauri/.sqlx/query-f2ba4708d4a9ff9ce74c407a730040bd7883e9a5c0eb79ef0d8a6782a8eae299.json rename to src-tauri/.sqlx/query-612efa9ac45723dc604a88f5e7e288b4055fec4ba7d9102131bd255c037fa021.json index 4fb2112a..aa1534a3 100644 --- a/src-tauri/.sqlx/query-f2ba4708d4a9ff9ce74c407a730040bd7883e9a5c0eb79ef0d8a6782a8eae299.json +++ b/src-tauri/.sqlx/query-612efa9ac45723dc604a88f5e7e288b4055fec4ba7d9102131bd255c037fa021.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n workspace_id,\n name,\n cookies AS \"cookies!: sqlx::types::Json>\"\n FROM cookie_jars WHERE id = ?\n ", + "query": "\n SELECT\n id, model, created_at, updated_at, workspace_id, name,\n cookies AS \"cookies!: sqlx::types::Json>\"\n FROM cookie_jars WHERE workspace_id = ?\n ", "describe": { "columns": [ { @@ -52,5 +52,5 @@ false ] }, - "hash": "f2ba4708d4a9ff9ce74c407a730040bd7883e9a5c0eb79ef0d8a6782a8eae299" + "hash": "612efa9ac45723dc604a88f5e7e288b4055fec4ba7d9102131bd255c037fa021" } diff --git a/src-tauri/.sqlx/query-7398403d3de2dc5c5b4b6392f083041d9a55194bb97819225a2612fdeb60ad42.json b/src-tauri/.sqlx/query-7398403d3de2dc5c5b4b6392f083041d9a55194bb97819225a2612fdeb60ad42.json new file mode 100644 index 00000000..15e4f800 --- /dev/null +++ b/src-tauri/.sqlx/query-7398403d3de2dc5c5b4b6392f083041d9a55194bb97819225a2612fdeb60ad42.json @@ -0,0 +1,92 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,\n url, service, method, message,\n proto_files AS \"proto_files!: sqlx::types::Json>\"\n FROM grpc_requests\n WHERE id = ?\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "model", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "workspace_id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "folder_id", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "updated_at", + "ordinal": 5, + "type_info": "Datetime" + }, + { + "name": "name", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "sort_priority", + "ordinal": 7, + "type_info": "Float" + }, + { + "name": "url", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "service", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "method", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "message", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "proto_files!: sqlx::types::Json>", + "ordinal": 12, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false, + false, + true, + true, + false, + false + ] + }, + "hash": "7398403d3de2dc5c5b4b6392f083041d9a55194bb97819225a2612fdeb60ad42" +} diff --git a/src-tauri/.sqlx/query-761d27c3ec425c37ad9abe9c732a9c1746c81ca50d2c413e540b74c8c8e908b7.json b/src-tauri/.sqlx/query-761d27c3ec425c37ad9abe9c732a9c1746c81ca50d2c413e540b74c8c8e908b7.json new file mode 100644 index 00000000..c24f110b --- /dev/null +++ b/src-tauri/.sqlx/query-761d27c3ec425c37ad9abe9c732a9c1746c81ca50d2c413e540b74c8c8e908b7.json @@ -0,0 +1,92 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority,\n url, service, method, message,\n proto_files AS \"proto_files!: sqlx::types::Json>\"\n FROM grpc_requests\n WHERE workspace_id = ?\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "model", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "workspace_id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "folder_id", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "updated_at", + "ordinal": 5, + "type_info": "Datetime" + }, + { + "name": "name", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "sort_priority", + "ordinal": 7, + "type_info": "Float" + }, + { + "name": "url", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "service", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "method", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "message", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "proto_files!: sqlx::types::Json>", + "ordinal": 12, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true, + false, + false, + false, + false, + false, + true, + true, + false, + false + ] + }, + "hash": "761d27c3ec425c37ad9abe9c732a9c1746c81ca50d2c413e540b74c8c8e908b7" +} diff --git a/src-tauri/.sqlx/query-80a85f83d0946d532a60f0add87aa0ade7e35a6b56cb058e2caf9ca005ce6407.json b/src-tauri/.sqlx/query-80a85f83d0946d532a60f0add87aa0ade7e35a6b56cb058e2caf9ca005ce6407.json new file mode 100644 index 00000000..9cacb528 --- /dev/null +++ b/src-tauri/.sqlx/query-80a85f83d0946d532a60f0add87aa0ade7e35a6b56cb058e2caf9ca005ce6407.json @@ -0,0 +1,68 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id, model, workspace_id, request_id, created_at, updated_at, service,\n method, elapsed\n FROM grpc_connections\n WHERE request_id = ?\n ORDER BY created_at DESC\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "model", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "workspace_id", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "request_id", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "updated_at", + "ordinal": 5, + "type_info": "Datetime" + }, + { + "name": "service", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "method", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "elapsed", + "ordinal": 8, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "80a85f83d0946d532a60f0add87aa0ade7e35a6b56cb058e2caf9ca005ce6407" +} diff --git a/src-tauri/.sqlx/query-86a9d12d7b00217f3143671908c31c2c6a3c24774a505280dcba169eb5b6b0fb.json b/src-tauri/.sqlx/query-86a9d12d7b00217f3143671908c31c2c6a3c24774a505280dcba169eb5b6b0fb.json deleted file mode 100644 index 063c467d..00000000 --- a/src-tauri/.sqlx/query-86a9d12d7b00217f3143671908c31c2c6a3c24774a505280dcba169eb5b6b0fb.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n UPDATE settings SET (\n theme,\n appearance,\n update_channel\n ) = (?, ?, ?) WHERE id = 'default';\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "86a9d12d7b00217f3143671908c31c2c6a3c24774a505280dcba169eb5b6b0fb" -} diff --git a/src-tauri/.sqlx/query-20cf0f8b71e600bc40ee204b60a12b2f3728178f3b8788d2e5cc92821b74d470.json b/src-tauri/.sqlx/query-8dfbae65ddec905ea3734448cc9f7029b6c78de227c6fa3a85d75d0a7f21e0e9.json similarity index 71% rename from src-tauri/.sqlx/query-20cf0f8b71e600bc40ee204b60a12b2f3728178f3b8788d2e5cc92821b74d470.json rename to src-tauri/.sqlx/query-8dfbae65ddec905ea3734448cc9f7029b6c78de227c6fa3a85d75d0a7f21e0e9.json index a0ee5771..b6db584b 100644 --- a/src-tauri/.sqlx/query-20cf0f8b71e600bc40ee204b60a12b2f3728178f3b8788d2e5cc92821b74d470.json +++ b/src-tauri/.sqlx/query-8dfbae65ddec905ea3734448cc9f7029b6c78de227c6fa3a85d75d0a7f21e0e9.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n name,\n description,\n setting_request_timeout,\n setting_follow_redirects,\n setting_validate_certificates,\n variables AS \"variables!: sqlx::types::Json>\"\n FROM workspaces\n ", + "query": "\n SELECT\n id, model, created_at, updated_at, name, description, setting_request_timeout,\n setting_follow_redirects, setting_validate_certificates,\n variables AS \"variables!: sqlx::types::Json>\"\n FROM workspaces\n ", "describe": { "columns": [ { @@ -70,5 +70,5 @@ false ] }, - "hash": "20cf0f8b71e600bc40ee204b60a12b2f3728178f3b8788d2e5cc92821b74d470" + "hash": "8dfbae65ddec905ea3734448cc9f7029b6c78de227c6fa3a85d75d0a7f21e0e9" } diff --git a/src-tauri/.sqlx/query-9238f94c688d91f42627e5b73c627c514bab4039ab5edadc79b77dfdfd63b208.json b/src-tauri/.sqlx/query-9238f94c688d91f42627e5b73c627c514bab4039ab5edadc79b77dfdfd63b208.json new file mode 100644 index 00000000..ea8b6c1b --- /dev/null +++ b/src-tauri/.sqlx/query-9238f94c688d91f42627e5b73c627c514bab4039ab5edadc79b77dfdfd63b208.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO folders (\n id, workspace_id, folder_id, name, sort_priority\n )\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n sort_priority = excluded.sort_priority\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "9238f94c688d91f42627e5b73c627c514bab4039ab5edadc79b77dfdfd63b208" +} diff --git a/src-tauri/.sqlx/query-e08fa4f9b2929f20a01d1dc43d6847a309d3e8c5b324df2d039d1c6e07e6eb2f.json b/src-tauri/.sqlx/query-9ba3f783238b77637ffded4171b2fbb5e5ad0be952a0d832448d65cc5f0effc1.json similarity index 70% rename from src-tauri/.sqlx/query-e08fa4f9b2929f20a01d1dc43d6847a309d3e8c5b324df2d039d1c6e07e6eb2f.json rename to src-tauri/.sqlx/query-9ba3f783238b77637ffded4171b2fbb5e5ad0be952a0d832448d65cc5f0effc1.json index ef10d925..0ef3ad69 100644 --- a/src-tauri/.sqlx/query-e08fa4f9b2929f20a01d1dc43d6847a309d3e8c5b324df2d039d1c6e07e6eb2f.json +++ b/src-tauri/.sqlx/query-9ba3f783238b77637ffded4171b2fbb5e5ad0be952a0d832448d65cc5f0effc1.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n name,\n description,\n setting_request_timeout,\n setting_follow_redirects,\n setting_validate_certificates,\n variables AS \"variables!: sqlx::types::Json>\"\n FROM workspaces WHERE id = ?\n ", + "query": "\n SELECT\n id, model, created_at, updated_at, name, description, setting_request_timeout,\n setting_follow_redirects, setting_validate_certificates,\n variables AS \"variables!: sqlx::types::Json>\"\n FROM workspaces WHERE id = ?\n ", "describe": { "columns": [ { @@ -70,5 +70,5 @@ false ] }, - "hash": "e08fa4f9b2929f20a01d1dc43d6847a309d3e8c5b324df2d039d1c6e07e6eb2f" + "hash": "9ba3f783238b77637ffded4171b2fbb5e5ad0be952a0d832448d65cc5f0effc1" } diff --git a/src-tauri/.sqlx/query-9d7bc2b0eb0c09652d9826db4a7ae47591405e1b5bec1229f2e2734c73e66163.json b/src-tauri/.sqlx/query-9d7bc2b0eb0c09652d9826db4a7ae47591405e1b5bec1229f2e2734c73e66163.json new file mode 100644 index 00000000..e6d6e8bb --- /dev/null +++ b/src-tauri/.sqlx/query-9d7bc2b0eb0c09652d9826db4a7ae47591405e1b5bec1229f2e2734c73e66163.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO grpc_connections (\n id, workspace_id, request_id, service, method, elapsed\n )\n VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n service = excluded.service,\n method = excluded.method,\n elapsed = excluded.elapsed\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "9d7bc2b0eb0c09652d9826db4a7ae47591405e1b5bec1229f2e2734c73e66163" +} diff --git a/src-tauri/.sqlx/query-a1c9a862ca6a07476cb8e7d16d73bd109c070603396a890dc717e50020d006f5.json b/src-tauri/.sqlx/query-a1c9a862ca6a07476cb8e7d16d73bd109c070603396a890dc717e50020d006f5.json deleted file mode 100644 index 95d3285a..00000000 --- a/src-tauri/.sqlx/query-a1c9a862ca6a07476cb8e7d16d73bd109c070603396a890dc717e50020d006f5.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n elapsed_headers,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n headers,\n version,\n remote_addr\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 13 - }, - "nullable": [] - }, - "hash": "a1c9a862ca6a07476cb8e7d16d73bd109c070603396a890dc717e50020d006f5" -} diff --git a/src-tauri/.sqlx/query-a690a04cd1ebe8c3dbfd0cd98ae4ef093a1696d7b7ecaf694d12e5fafd62b685.json b/src-tauri/.sqlx/query-a690a04cd1ebe8c3dbfd0cd98ae4ef093a1696d7b7ecaf694d12e5fafd62b685.json new file mode 100644 index 00000000..2ada8fc0 --- /dev/null +++ b/src-tauri/.sqlx/query-a690a04cd1ebe8c3dbfd0cd98ae4ef093a1696d7b7ecaf694d12e5fafd62b685.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE grpc_connections\n SET (elapsed) = (-1)\n WHERE elapsed = 0;\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 0 + }, + "nullable": [] + }, + "hash": "a690a04cd1ebe8c3dbfd0cd98ae4ef093a1696d7b7ecaf694d12e5fafd62b685" +} diff --git a/src-tauri/.sqlx/query-1517b0f86c841b5f1247bd40c3a9b38ab001d846a410b6e3cd36f9e844d50ddb.json b/src-tauri/.sqlx/query-ae98a7b35a5cb80a4bcd04faa22545deac2a5e9bfb814b60191f16b98ed49796.json similarity index 71% rename from src-tauri/.sqlx/query-1517b0f86c841b5f1247bd40c3a9b38ab001d846a410b6e3cd36f9e844d50ddb.json rename to src-tauri/.sqlx/query-ae98a7b35a5cb80a4bcd04faa22545deac2a5e9bfb814b60191f16b98ed49796.json index 54e99b1f..58aec4c3 100644 --- a/src-tauri/.sqlx/query-1517b0f86c841b5f1247bd40c3a9b38ab001d846a410b6e3cd36f9e844d50ddb.json +++ b/src-tauri/.sqlx/query-ae98a7b35a5cb80a4bcd04faa22545deac2a5e9bfb814b60191f16b98ed49796.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n folder_id,\n name,\n sort_priority\n FROM folders\n WHERE workspace_id = ?\n ", + "query": "\n SELECT\n id, model, workspace_id, created_at, updated_at, folder_id, name, sort_priority\n FROM folders\n WHERE id = ?\n ", "describe": { "columns": [ { @@ -58,5 +58,5 @@ false ] }, - "hash": "1517b0f86c841b5f1247bd40c3a9b38ab001d846a410b6e3cd36f9e844d50ddb" + "hash": "ae98a7b35a5cb80a4bcd04faa22545deac2a5e9bfb814b60191f16b98ed49796" } diff --git a/src-tauri/.sqlx/query-3b3fb6271340c6ec21a10b4f1b20502c86c425e0b53ac07692f8a4ed0be09335.json b/src-tauri/.sqlx/query-b32994b09ae7a06eb0f031069d327e55127a5bce60cbb499b83d1701386a23cb.json similarity index 70% rename from src-tauri/.sqlx/query-3b3fb6271340c6ec21a10b4f1b20502c86c425e0b53ac07692f8a4ed0be09335.json rename to src-tauri/.sqlx/query-b32994b09ae7a06eb0f031069d327e55127a5bce60cbb499b83d1701386a23cb.json index ca60855e..aed140c8 100644 --- a/src-tauri/.sqlx/query-3b3fb6271340c6ec21a10b4f1b20502c86c425e0b53ac07692f8a4ed0be09335.json +++ b/src-tauri/.sqlx/query-b32994b09ae7a06eb0f031069d327e55127a5bce60cbb499b83d1701386a23cb.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n theme,\n appearance,\n update_channel\n FROM settings\n WHERE id = 'default'\n ", + "query": "\n SELECT\n id, model, created_at, updated_at, theme, appearance, update_channel\n FROM settings\n WHERE id = 'default'\n ", "describe": { "columns": [ { @@ -52,5 +52,5 @@ false ] }, - "hash": "3b3fb6271340c6ec21a10b4f1b20502c86c425e0b53ac07692f8a4ed0be09335" + "hash": "b32994b09ae7a06eb0f031069d327e55127a5bce60cbb499b83d1701386a23cb" } diff --git a/src-tauri/.sqlx/query-b3fae40a793a6724dd2286a9ca4bc0a9c000a631ee0d751a9dc4f3e76de3d57c.json b/src-tauri/.sqlx/query-b3fae40a793a6724dd2286a9ca4bc0a9c000a631ee0d751a9dc4f3e76de3d57c.json new file mode 100644 index 00000000..c12ff6f4 --- /dev/null +++ b/src-tauri/.sqlx/query-b3fae40a793a6724dd2286a9ca4bc0a9c000a631ee0d751a9dc4f3e76de3d57c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO cookie_jars (\n id, workspace_id, name, cookies\n )\n VALUES (?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n cookies = excluded.cookies\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "b3fae40a793a6724dd2286a9ca4bc0a9c000a631ee0d751a9dc4f3e76de3d57c" +} diff --git a/src-tauri/.sqlx/query-b5ed4dc77f8cf21de1a06f146e47821bdb51fcfe747170bea41e7a366d736bda.json b/src-tauri/.sqlx/query-b5ed4dc77f8cf21de1a06f146e47821bdb51fcfe747170bea41e7a366d736bda.json deleted file mode 100644 index f038039f..00000000 --- a/src-tauri/.sqlx/query-b5ed4dc77f8cf21de1a06f146e47821bdb51fcfe747170bea41e7a366d736bda.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO cookie_jars (\n id,\n workspace_id,\n name,\n cookies\n )\n VALUES (?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n cookies = excluded.cookies\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "b5ed4dc77f8cf21de1a06f146e47821bdb51fcfe747170bea41e7a366d736bda" -} diff --git a/src-tauri/.sqlx/query-cae4e905515ddea1ec2cd685020241f06b49719085a695b897ef8ad409d2cef2.json b/src-tauri/.sqlx/query-cae4e905515ddea1ec2cd685020241f06b49719085a695b897ef8ad409d2cef2.json deleted file mode 100644 index 3eb6e5d8..00000000 --- a/src-tauri/.sqlx/query-cae4e905515ddea1ec2cd685020241f06b49719085a695b897ef8ad409d2cef2.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO workspaces (\n id,\n name,\n description,\n variables,\n setting_request_timeout,\n setting_follow_redirects,\n setting_validate_certificates\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n description = excluded.description,\n variables = excluded.variables,\n setting_request_timeout = excluded.setting_request_timeout,\n setting_follow_redirects = excluded.setting_follow_redirects,\n setting_validate_certificates = excluded.setting_validate_certificates\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 7 - }, - "nullable": [] - }, - "hash": "cae4e905515ddea1ec2cd685020241f06b49719085a695b897ef8ad409d2cef2" -} diff --git a/src-tauri/.sqlx/query-dcc2f405f8e29d0599d86bcde509187e9cc5fc647067eaa5c738cb24e2f081e5.json b/src-tauri/.sqlx/query-dcc2f405f8e29d0599d86bcde509187e9cc5fc647067eaa5c738cb24e2f081e5.json deleted file mode 100644 index 8cd4e019..00000000 --- a/src-tauri/.sqlx/query-dcc2f405f8e29d0599d86bcde509187e9cc5fc647067eaa5c738cb24e2f081e5.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\n INSERT INTO environments (\n id,\n workspace_id,\n name,\n variables\n )\n VALUES (?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n variables = excluded.variables\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 4 - }, - "nullable": [] - }, - "hash": "dcc2f405f8e29d0599d86bcde509187e9cc5fc647067eaa5c738cb24e2f081e5" -} diff --git a/src-tauri/.sqlx/query-7a7bc4df7e52ad3a987c97af8f43b46381e2cc16ba0c553713d0b6c64354eb39.json b/src-tauri/.sqlx/query-e61c0dddb3e86d271cb9399faa4e4443342796cb72bdd43a821fae2994ae8e2f.json similarity index 70% rename from src-tauri/.sqlx/query-7a7bc4df7e52ad3a987c97af8f43b46381e2cc16ba0c553713d0b6c64354eb39.json rename to src-tauri/.sqlx/query-e61c0dddb3e86d271cb9399faa4e4443342796cb72bdd43a821fae2994ae8e2f.json index e919ca51..25fe3a26 100644 --- a/src-tauri/.sqlx/query-7a7bc4df7e52ad3a987c97af8f43b46381e2cc16ba0c553713d0b6c64354eb39.json +++ b/src-tauri/.sqlx/query-e61c0dddb3e86d271cb9399faa4e4443342796cb72bdd43a821fae2994ae8e2f.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n url_parameters AS \"url_parameters!: sqlx::types::Json>\",\n method,\n body AS \"body!: Json>\",\n body_type,\n authentication AS \"authentication!: Json>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_requests\n WHERE workspace_id = ?\n ", + "query": "\n SELECT\n id, model, workspace_id, folder_id, created_at, updated_at, name, url,\n url_parameters AS \"url_parameters!: sqlx::types::Json>\",\n method, body_type, authentication_type, sort_priority,\n body AS \"body!: Json>\",\n authentication AS \"authentication!: Json>\",\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_requests\n WHERE workspace_id = ?\n ", "describe": { "columns": [ { @@ -54,29 +54,29 @@ "type_info": "Text" }, { - "name": "body!: Json>", + "name": "body_type", "ordinal": 10, "type_info": "Text" }, { - "name": "body_type", + "name": "authentication_type", "ordinal": 11, "type_info": "Text" }, { - "name": "authentication!: Json>", + "name": "sort_priority", "ordinal": 12, - "type_info": "Text" + "type_info": "Float" }, { - "name": "authentication_type", + "name": "body!: Json>", "ordinal": 13, "type_info": "Text" }, { - "name": "sort_priority", + "name": "authentication!: Json>", "ordinal": 14, - "type_info": "Float" + "type_info": "Text" }, { "name": "headers!: sqlx::types::Json>", @@ -98,13 +98,13 @@ false, false, false, - false, + true, true, false, - true, + false, false, false ] }, - "hash": "7a7bc4df7e52ad3a987c97af8f43b46381e2cc16ba0c553713d0b6c64354eb39" + "hash": "e61c0dddb3e86d271cb9399faa4e4443342796cb72bdd43a821fae2994ae8e2f" } diff --git a/src-tauri/.sqlx/query-e7124f5570076bfd65985744f48d8e12cf29d6d243fffdd62ade2ab70c7bddda.json b/src-tauri/.sqlx/query-e7124f5570076bfd65985744f48d8e12cf29d6d243fffdd62ade2ab70c7bddda.json new file mode 100644 index 00000000..3b49c691 --- /dev/null +++ b/src-tauri/.sqlx/query-e7124f5570076bfd65985744f48d8e12cf29d6d243fffdd62ade2ab70c7bddda.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE http_responses SET (\n elapsed, elapsed_headers, url, status, status_reason, content_length, body_path,\n error, headers, version, remote_addr, updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 12 + }, + "nullable": [] + }, + "hash": "e7124f5570076bfd65985744f48d8e12cf29d6d243fffdd62ade2ab70c7bddda" +} diff --git a/src-tauri/.sqlx/query-ee562f85ec28c554c607adde670fc30eaeffeed6883e712bda4b4d6ca49cf740.json b/src-tauri/.sqlx/query-ee562f85ec28c554c607adde670fc30eaeffeed6883e712bda4b4d6ca49cf740.json new file mode 100644 index 00000000..c1183863 --- /dev/null +++ b/src-tauri/.sqlx/query-ee562f85ec28c554c607adde670fc30eaeffeed6883e712bda4b4d6ca49cf740.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO grpc_requests (\n id, name, workspace_id, folder_id, sort_priority, url, service, method, message,\n proto_files\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n sort_priority = excluded.sort_priority,\n url = excluded.url,\n service = excluded.service,\n method = excluded.method,\n message = excluded.message,\n proto_files = excluded.proto_files\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 10 + }, + "nullable": [] + }, + "hash": "ee562f85ec28c554c607adde670fc30eaeffeed6883e712bda4b4d6ca49cf740" +} diff --git a/src-tauri/.sqlx/query-cb939b45a715d91f7631dea6b2d1bdc59fb3dffbd44ff99bc15adb34ea7093f7.json b/src-tauri/.sqlx/query-f5f20f3b37d932617499a0da50997edad59e4f5998b15c50ed6eae2e97064068.json similarity index 69% rename from src-tauri/.sqlx/query-cb939b45a715d91f7631dea6b2d1bdc59fb3dffbd44ff99bc15adb34ea7093f7.json rename to src-tauri/.sqlx/query-f5f20f3b37d932617499a0da50997edad59e4f5998b15c50ed6eae2e97064068.json index ddd49a3b..edc922c8 100644 --- a/src-tauri/.sqlx/query-cb939b45a715d91f7631dea6b2d1bdc59fb3dffbd44ff99bc15adb34ea7093f7.json +++ b/src-tauri/.sqlx/query-f5f20f3b37d932617499a0da50997edad59e4f5998b15c50ed6eae2e97064068.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n workspace_id,\n name,\n cookies AS \"cookies!: sqlx::types::Json>\"\n FROM cookie_jars WHERE workspace_id = ?\n ", + "query": "\n SELECT\n id, model, created_at, updated_at, workspace_id, name,\n cookies AS \"cookies!: sqlx::types::Json>\"\n FROM cookie_jars WHERE id = ?\n ", "describe": { "columns": [ { @@ -52,5 +52,5 @@ false ] }, - "hash": "cb939b45a715d91f7631dea6b2d1bdc59fb3dffbd44ff99bc15adb34ea7093f7" + "hash": "f5f20f3b37d932617499a0da50997edad59e4f5998b15c50ed6eae2e97064068" } diff --git a/src-tauri/.sqlx/query-fe0652396bc30d926cf99083651c1cbd668bcf00ebe1a5f36616700c84972b39.json b/src-tauri/.sqlx/query-fe0652396bc30d926cf99083651c1cbd668bcf00ebe1a5f36616700c84972b39.json new file mode 100644 index 00000000..a009d61a --- /dev/null +++ b/src-tauri/.sqlx/query-fe0652396bc30d926cf99083651c1cbd668bcf00ebe1a5f36616700c84972b39.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM grpc_requests\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "fe0652396bc30d926cf99083651c1cbd668bcf00ebe1a5f36616700c84972b39" +} diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 88fa03eb..6f33f539 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -77,9 +77,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "async-compression" @@ -95,6 +95,39 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "atk" version = "0.15.1" @@ -128,12 +161,67 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-write-file" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436" +dependencies = [ + "nix", + "rand 0.8.5", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa 1.0.9", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -1073,12 +1161,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1596,6 +1684,28 @@ dependencies = [ "system-deps 6.2.0", ] +[[package]] +name = "grpc" +version = "0.1.0" +dependencies = [ + "anyhow", + "hyper", + "hyper-rustls", + "log", + "once_cell", + "prost", + "prost-reflect", + "prost-types", + "protoc-bin-vendored", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tonic", + "tonic-reflection", + "uuid", +] + [[package]] name = "gtk" version = "0.15.5" @@ -1761,20 +1871,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "html5ever" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" -dependencies = [ - "log", - "mac", - "markup5ever 0.10.1", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "html5ever" version = "0.26.0" @@ -1783,7 +1879,7 @@ checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" dependencies = [ "log", "mac", - "markup5ever 0.11.0", + "markup5ever", "proc-macro2", "quote", "syn 1.0.109", @@ -1853,6 +1949,34 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -2233,18 +2357,6 @@ dependencies = [ "treediff", ] -[[package]] -name = "kuchiki" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358" -dependencies = [ - "cssparser", - "html5ever 0.25.2", - "matches", - "selectors", -] - [[package]] name = "kuchikiki" version = "0.8.2" @@ -2252,7 +2364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" dependencies = [ "cssparser", - "html5ever 0.26.0", + "html5ever", "indexmap 1.9.3", "matches", "selectors", @@ -2269,9 +2381,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.150" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libm" @@ -2292,9 +2404,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.26.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" dependencies = [ "cc", "pkg-config", @@ -2312,9 +2424,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "litemap" @@ -2380,20 +2492,6 @@ dependencies = [ "libc", ] -[[package]] -name = "markup5ever" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" -dependencies = [ - "log", - "phf 0.8.0", - "phf_codegen 0.8.0", - "string_cache", - "string_cache_codegen", - "tendril", -] - [[package]] name = "markup5ever" version = "0.11.0" @@ -2423,6 +2521,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "md-5" version = "0.10.6" @@ -2549,6 +2653,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "libc", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -2839,6 +2954,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "os_info" version = "3.7.0" @@ -3074,6 +3198,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -3230,6 +3374,114 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "prost-reflect" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057237efdb71cf4b3f9396302a3d6599a92fa94063ba537b66130980ea9909f3" +dependencies = [ + "base64 0.21.5", + "once_cell", + "prost", + "prost-reflect-derive", + "prost-types", + "serde", + "serde-value", +] + +[[package]] +name = "prost-reflect-derive" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "172da1212c02be2c94901440cb27183cd92bff00ebacca5c323bf7520b8f9c04" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ca8623e5633e298ad1f917d8be0a44bcf406bf3cde3b80e63003e49a3f27d" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb9fc9cce84c8694b6ea01cc6296617b288b703719b725b8c9c65f7c5874435" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d2a07dcf7173a04d49974930ccbfb7fd4d74df30ecfc8762cf2f895a094516" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54fef0b04fcacba64d1d80eed74a20356d96847da8497a59b0a0a436c9165b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8782f2ce7d43a9a5c74ea4936f001e9e8442205c244f7a3d4286bd4c37bc924" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5de656c7ee83f08e0ae5b81792ccfdc1d04e7876b1d9a38e6876a9e09e02537" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9653c3ed92974e34c5a6e0a510864dab979760481714c172e0a34e437cb98804" + [[package]] name = "psl-types" version = "2.0.11" @@ -3577,15 +3829,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.21" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3594,11 +3846,24 @@ version = "0.21.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" dependencies = [ + "log", "ring", "rustls-webpki", "sct", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -3736,18 +4001,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] -name = "serde_derive" -version = "1.0.195" +name = "serde-value" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", @@ -3756,9 +4031,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.111" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa 1.0.9", "ryu", @@ -4018,9 +4293,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" +checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" dependencies = [ "sqlx-core", "sqlx-macros", @@ -4031,9 +4306,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" +checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" dependencies = [ "ahash", "atoi", @@ -4076,9 +4351,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" dependencies = [ "proc-macro2", "quote", @@ -4089,10 +4364,11 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" +checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" dependencies = [ + "atomic-write-file", "dotenvy", "either", "heck 0.4.1", @@ -4115,9 +4391,9 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" +checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" dependencies = [ "atoi", "base64 0.21.5", @@ -4159,9 +4435,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" dependencies = [ "atoi", "base64 0.21.5", @@ -4200,9 +4476,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" dependencies = [ "atoi", "chrono", @@ -4220,6 +4496,7 @@ dependencies = [ "time", "tracing", "url", + "urlencoding", ] [[package]] @@ -4314,6 +4591,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "synstructure" version = "0.13.0" @@ -4469,9 +4752,9 @@ checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" [[package]] name = "tauri" -version = "1.5.2" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bfe673cf125ef364d6f56b15e8ce7537d9ca7e4dae1cf6fbbdeed2e024db3d9" +checksum = "fd27c04b9543776a972c86ccf70660b517ecabbeced9fb58d8b961a13ad129af" dependencies = [ "anyhow", "base64 0.21.5", @@ -4544,9 +4827,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b3475e55acec0b4a50fb96435f19631fb58cbcd31923e1a213de5c382536bbb" +checksum = "a1554c5857f65dbc377cefb6b97c8ac77b1cb2a90d30d3448114d5d6b48a77fc" dependencies = [ "base64 0.21.5", "brotli", @@ -4570,9 +4853,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "1.4.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613740228de92d9196b795ac455091d3a5fbdac2654abb8bb07d010b62ab43af" +checksum = "277abf361a3a6993ec16bcbb179de0d6518009b851090a01adfea12ac89fa875" dependencies = [ "heck 0.4.1", "proc-macro2", @@ -4613,9 +4896,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07f8e9e53e00e9f41212c115749e87d5cd2a9eebccafca77a19722eeecd56d43" +checksum = "cf2d0652aa2891ff3e9caa2401405257ea29ab8372cce01f186a5825f1bd0e76" dependencies = [ "gtk", "http", @@ -4634,9 +4917,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8141d72b6b65f2008911e9ef5b98a68d1e3413b7a1464e8f85eb3673bb19a895" +checksum = "6cae61fbc731f690a4899681c9052dde6d05b159b44563ace8186fc1bfb7d158" dependencies = [ "cocoa 0.24.1", "gtk", @@ -4663,7 +4946,7 @@ dependencies = [ "dunce", "glob", "heck 0.4.1", - "html5ever 0.26.0", + "html5ever", "infer", "json-patch", "kuchikiki", @@ -4695,15 +4978,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.1" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.4.1", "rustix", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4829,9 +5111,31 @@ dependencies = [ "num_cpus", "pin-project-lite", "socket2 0.5.5", + "tokio-macros", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" @@ -4842,6 +5146,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -4935,6 +5249,72 @@ dependencies = [ "winnow", ] +[[package]] +name = "tonic" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.21.5", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-reflection" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa37c513df1339d197f4ba21d28c918b9ef1ac1768265f11ecb6b7f1cba1b76" +dependencies = [ + "prost", + "prost-types", + "tokio", + "tokio-stream", + "tonic", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -5096,6 +5476,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -5122,9 +5508,9 @@ checksum = "64a8922555b9500e3d865caed19330172cd67cbf82203f1a3311d8c305cc9f33" [[package]] name = "uuid" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ "getrandom 0.2.11", ] @@ -5360,12 +5746,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.24.0" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" -dependencies = [ - "rustls-webpki", -] +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webview2-com" @@ -5558,6 +5941,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -5847,9 +6239,9 @@ checksum = "dad7bb64b8ef9c0aa27b6da38b452b0ee9fd82beaf276a87dd796fb55cbae14e" [[package]] name = "wry" -version = "0.24.4" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ef04bdad49eba2e01f06e53688c8413bd6a87b0bc14b72284465cf96e3578e" +checksum = "6ad85d0e067359e409fcb88903c3eac817c392e5d638258abfb3da5ad8ba6fc4" dependencies = [ "base64 0.13.1", "block", @@ -5861,9 +6253,9 @@ dependencies = [ "gio", "glib", "gtk", - "html5ever 0.25.2", + "html5ever", "http", - "kuchiki", + "kuchikiki", "libc", "log", "objc", @@ -5925,6 +6317,7 @@ dependencies = [ "cookie 0.18.0", "datetime", "futures", + "grpc", "http", "log", "objc", @@ -5940,6 +6333,7 @@ dependencies = [ "tauri-plugin-log", "tauri-plugin-window-state", "tokio", + "tokio-stream", "uuid", "window-shadows", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f1f1d448..2f38ebc0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,3 +1,4 @@ +workspace = { members = ["grpc"] } [package] name = "yaak-app" version = "0.0.0" @@ -32,7 +33,7 @@ reqwest = { version = "0.11.23", features = ["multipart", "cookies", "gzip", "br cookie = { version = "0.18.0" } serde = { version = "1.0.195", features = ["derive"] } serde_json = { version = "1.0.111", features = ["raw_value"] } -sqlx = { version = "0.7.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] } +sqlx = { version = "0.7.3", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] } tauri = { version = "1.5.2", features = [ "config-toml", "devtools", @@ -59,6 +60,8 @@ log = "0.4.20" datetime = "0.5.2" window-shadows = "0.2.2" reqwest_cookie_store = "0.6.0" +grpc = { path = "./grpc" } +tokio-stream = "0.1.14" [features] # by default Tauri runs in production mode diff --git a/src-tauri/grpc/Cargo.toml b/src-tauri/grpc/Cargo.toml new file mode 100644 index 00000000..4f32754c --- /dev/null +++ b/src-tauri/grpc/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "grpc" +version = "0.1.0" +edition = "2021" + +[dependencies] +tonic = "0.10.2" +prost = "0.12" +tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "fs"] } +tonic-reflection = "0.10.2" +tokio-stream = "0.1.14" +prost-types = "0.12.3" +serde = { version = "1.0.196", features = ["derive"] } +serde_json = "1.0.113" +prost-reflect = { version = "0.12.0", features = ["serde", "derive"] } +log = "0.4.20" +once_cell = { version = "1.19.0", features = [] } +anyhow = "1.0.79" +hyper = { version = "0.14" } +hyper-rustls = { version = "0.24.0", features = ["http2"] } +protoc-bin-vendored = "3.0.0" +uuid = { version = "1.7.0", features = ["v4"] } diff --git a/src-tauri/grpc/src/codec.rs b/src-tauri/grpc/src/codec.rs new file mode 100644 index 00000000..f01ce87d --- /dev/null +++ b/src-tauri/grpc/src/codec.rs @@ -0,0 +1,52 @@ +use prost_reflect::prost::Message; +use prost_reflect::{DynamicMessage, MethodDescriptor}; +use tonic::codec::{Codec, DecodeBuf, Decoder, EncodeBuf, Encoder}; +use tonic::Status; + +#[derive(Clone)] +pub struct DynamicCodec(MethodDescriptor); + +impl DynamicCodec { + #[allow(dead_code)] + pub fn new(md: MethodDescriptor) -> Self { + Self(md) + } +} + +impl Codec for DynamicCodec { + type Encode = DynamicMessage; + type Decode = DynamicMessage; + type Encoder = Self; + type Decoder = Self; + + fn encoder(&mut self) -> Self::Encoder { + self.clone() + } + + fn decoder(&mut self) -> Self::Decoder { + self.clone() + } +} + +impl Encoder for DynamicCodec { + type Item = DynamicMessage; + type Error = Status; + + fn encode(&mut self, item: Self::Item, dst: &mut EncodeBuf<'_>) -> Result<(), Self::Error> { + item.encode(dst) + .expect("buffer is too small to decode this message"); + Ok(()) + } +} + +impl Decoder for DynamicCodec { + type Item = DynamicMessage; + type Error = Status; + + fn decode(&mut self, src: &mut DecodeBuf<'_>) -> Result, Self::Error> { + let mut msg = DynamicMessage::new(self.0.output()); + msg.merge(src) + .map_err(|err| Status::internal(err.to_string()))?; + Ok(Some(msg)) + } +} diff --git a/src-tauri/grpc/src/json_schema.rs b/src-tauri/grpc/src/json_schema.rs new file mode 100644 index 00000000..b915c6c0 --- /dev/null +++ b/src-tauri/grpc/src/json_schema.rs @@ -0,0 +1,179 @@ +use prost_reflect::{DescriptorPool, MessageDescriptor}; +use prost_types::field_descriptor_proto; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Default, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct JsonSchemaEntry { + #[serde(skip_serializing_if = "Option::is_none")] + title: Option, + + #[serde(rename = "type")] + type_: JsonType, + + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + properties: Option>, + + #[serde(rename = "enum", skip_serializing_if = "Option::is_none")] + enum_: Option>, + + /// Don't allow any other properties in the object + additional_properties: bool, + + /// Set all properties to required + #[serde(skip_serializing_if = "Option::is_none")] + required: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + items: Option>, +} + +enum JsonType { + String, + Number, + Object, + Array, + Boolean, + Null, + _UNKNOWN, +} + +impl Default for JsonType { + fn default() -> Self { + JsonType::_UNKNOWN + } +} + +impl serde::Serialize for JsonType { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + JsonType::String => serializer.serialize_str("string"), + JsonType::Number => serializer.serialize_str("number"), + JsonType::Object => serializer.serialize_str("object"), + JsonType::Array => serializer.serialize_str("array"), + JsonType::Boolean => serializer.serialize_str("boolean"), + JsonType::Null => serializer.serialize_str("null"), + JsonType::_UNKNOWN => serializer.serialize_str("unknown"), + } + } +} + +impl<'de> serde::Deserialize<'de> for JsonType { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + "string" => Ok(JsonType::String), + "number" => Ok(JsonType::Number), + "object" => Ok(JsonType::Object), + "array" => Ok(JsonType::Array), + "boolean" => Ok(JsonType::Boolean), + "null" => Ok(JsonType::Null), + _ => Ok(JsonType::_UNKNOWN), + } + } +} + +pub fn message_to_json_schema( + pool: &DescriptorPool, + message: MessageDescriptor, +) -> JsonSchemaEntry { + let mut schema = JsonSchemaEntry { + title: Some(message.name().to_string()), + type_: JsonType::Object, // Messages are objects + ..Default::default() + }; + + let mut properties = HashMap::new(); + message.fields().for_each(|f| match f.kind() { + prost_reflect::Kind::Message(m) => { + properties.insert(f.name().to_string(), message_to_json_schema(pool, m)); + } + prost_reflect::Kind::Enum(e) => { + properties.insert( + f.name().to_string(), + JsonSchemaEntry { + type_: map_proto_type_to_json_type(f.field_descriptor_proto().r#type()), + enum_: Some(e.values().map(|v| v.name().to_string()).collect::>()), + ..Default::default() + }, + ); + } + _ => { + // TODO: Handle repeated label + match f.field_descriptor_proto().label() { + field_descriptor_proto::Label::Repeated => { + // TODO: Handle more complex repeated types. This just handles primitives for now + properties.insert( + f.name().to_string(), + JsonSchemaEntry { + type_: JsonType::Array, + items: Some(Box::new(JsonSchemaEntry { + type_: map_proto_type_to_json_type( + f.field_descriptor_proto().r#type(), + ), + ..Default::default() + })), + ..Default::default() + }, + ); + } + _ => { + // Regular JSON field + properties.insert( + f.name().to_string(), + JsonSchemaEntry { + type_: map_proto_type_to_json_type(f.field_descriptor_proto().r#type()), + ..Default::default() + }, + ); + } + }; + } + }); + + schema.properties = Some(properties); + + // All proto 3 fields are optional, so maybe we could + // make this a setting? + // schema.required = Some( + // message + // .fields() + // .map(|f| f.name().to_string()) + // .collect::>(), + // ); + + schema +} + +fn map_proto_type_to_json_type(proto_type: field_descriptor_proto::Type) -> JsonType { + match proto_type { + field_descriptor_proto::Type::Double => JsonType::Number, + field_descriptor_proto::Type::Float => JsonType::Number, + field_descriptor_proto::Type::Int64 => JsonType::Number, + field_descriptor_proto::Type::Uint64 => JsonType::Number, + field_descriptor_proto::Type::Int32 => JsonType::Number, + field_descriptor_proto::Type::Fixed64 => JsonType::Number, + field_descriptor_proto::Type::Fixed32 => JsonType::Number, + field_descriptor_proto::Type::Bool => JsonType::Boolean, + field_descriptor_proto::Type::String => JsonType::String, + field_descriptor_proto::Type::Group => JsonType::_UNKNOWN, + field_descriptor_proto::Type::Message => JsonType::Object, + field_descriptor_proto::Type::Bytes => JsonType::String, + field_descriptor_proto::Type::Uint32 => JsonType::Number, + field_descriptor_proto::Type::Enum => JsonType::String, + field_descriptor_proto::Type::Sfixed32 => JsonType::Number, + field_descriptor_proto::Type::Sfixed64 => JsonType::Number, + field_descriptor_proto::Type::Sint32 => JsonType::Number, + field_descriptor_proto::Type::Sint64 => JsonType::Number, + } +} diff --git a/src-tauri/grpc/src/lib.rs b/src-tauri/grpc/src/lib.rs new file mode 100644 index 00000000..8bb10da9 --- /dev/null +++ b/src-tauri/grpc/src/lib.rs @@ -0,0 +1,40 @@ +use prost_reflect::{DynamicMessage, SerializeOptions}; +use serde::{Deserialize, Serialize}; + +mod codec; +mod json_schema; +pub mod manager; +mod proto; + +pub fn serialize_options() -> SerializeOptions { + SerializeOptions::new().skip_default_fields(false) +} + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(default, rename_all = "camelCase")] +pub struct ServiceDefinition { + pub name: String, + pub methods: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(default, rename_all = "camelCase")] +pub struct MethodDefinition { + pub name: String, + pub schema: String, + pub client_streaming: bool, + pub server_streaming: bool, +} + +static SERIALIZE_OPTIONS: &'static SerializeOptions = &SerializeOptions::new() + .skip_default_fields(false) + .stringify_64_bit_integers(false); + +pub fn serialize_message(msg: &DynamicMessage) -> Result { + let mut buf = Vec::new(); + let mut se = serde_json::Serializer::pretty(&mut buf); + msg.serialize_with_options(&mut se, SERIALIZE_OPTIONS) + .map_err(|e| e.to_string())?; + let s = String::from_utf8(buf).expect("serde_json to emit valid utf8"); + Ok(s) +} diff --git a/src-tauri/grpc/src/manager.rs b/src-tauri/grpc/src/manager.rs new file mode 100644 index 00000000..e6c8fce6 --- /dev/null +++ b/src-tauri/grpc/src/manager.rs @@ -0,0 +1,280 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use hyper::client::HttpConnector; +use hyper::Client; +use hyper_rustls::HttpsConnector; +use prost_reflect::DescriptorPool; +pub use prost_reflect::DynamicMessage; +use serde_json::Deserializer; +use tokio_stream::wrappers::ReceiverStream; +use tokio_stream::StreamExt; +use tonic::body::BoxBody; +use tonic::transport::Uri; +use tonic::{IntoRequest, IntoStreamingRequest, Streaming}; + +use crate::codec::DynamicCodec; +use crate::proto::{fill_pool, fill_pool_from_files, get_transport, method_desc_to_path}; +use crate::{json_schema, MethodDefinition, ServiceDefinition}; + +#[derive(Clone)] +pub struct GrpcConnection { + pool: DescriptorPool, + conn: Client, BoxBody>, + pub uri: Uri, +} + +impl GrpcConnection { + pub async fn unary( + &self, + service: &str, + method: &str, + message: &str, + ) -> Result { + let service = self.pool.get_service_by_name(service).unwrap(); + let method = &service.methods().find(|m| m.name() == method).unwrap(); + let input_message = method.input(); + + let mut deserializer = Deserializer::from_str(message); + let req_message = DynamicMessage::deserialize(input_message, &mut deserializer) + .map_err(|e| e.to_string())?; + deserializer.end().unwrap(); + + let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()); + + let req = req_message.into_request(); + let path = method_desc_to_path(method); + let codec = DynamicCodec::new(method.clone()); + client.ready().await.unwrap(); + + Ok(client + .unary(req, path, codec) + .await + .map_err(|e| e.to_string())? + .into_inner()) + } + + pub async fn streaming( + &self, + service: &str, + method: &str, + stream: ReceiverStream, + ) -> Result, String> { + let service = self.pool.get_service_by_name(service).unwrap(); + let method = &service.methods().find(|m| m.name() == method).unwrap(); + + let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()); + + let method2 = method.clone(); + let req = stream + .map(move |s| { + let mut deserializer = Deserializer::from_str(&s); + let req_message = DynamicMessage::deserialize(method2.input(), &mut deserializer) + .map_err(|e| e.to_string()) + .unwrap(); + deserializer.end().unwrap(); + req_message + }) + .into_streaming_request(); + let path = method_desc_to_path(method); + let codec = DynamicCodec::new(method.clone()); + client.ready().await.unwrap(); + Ok(client + .streaming(req, path, codec) + .await + .map_err(|s| s.to_string())? + .into_inner()) + } + + pub async fn client_streaming( + &self, + service: &str, + method: &str, + stream: ReceiverStream, + ) -> Result { + let service = self.pool.get_service_by_name(service).unwrap(); + let method = &service.methods().find(|m| m.name() == method).unwrap(); + let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()); + + let req = { + let method = method.clone(); + stream + .map(move |s| { + let mut deserializer = Deserializer::from_str(&s); + let req_message = + DynamicMessage::deserialize(method.input(), &mut deserializer) + .map_err(|e| e.to_string()) + .unwrap(); + deserializer.end().unwrap(); + req_message + }) + .into_streaming_request() + }; + let path = method_desc_to_path(method); + let codec = DynamicCodec::new(method.clone()); + client.ready().await.unwrap(); + Ok(client + .client_streaming(req, path, codec) + .await + .map_err(|s| s.to_string())? + .into_inner()) + } + + pub async fn server_streaming( + &self, + service: &str, + method: &str, + message: &str, + ) -> Result, String> { + let service = self.pool.get_service_by_name(service).unwrap(); + let method = &service.methods().find(|m| m.name() == method).unwrap(); + let input_message = method.input(); + + let mut deserializer = Deserializer::from_str(message); + let req_message = DynamicMessage::deserialize(input_message, &mut deserializer) + .map_err(|e| e.to_string())?; + deserializer.end().unwrap(); + + let mut client = tonic::client::Grpc::with_origin(self.conn.clone(), self.uri.clone()); + + let req = req_message.into_request(); + let path = method_desc_to_path(method); + let codec = DynamicCodec::new(method.clone()); + client.ready().await.unwrap(); + Ok(client + .server_streaming(req, path, codec) + .await + .map_err(|s| s.to_string())? + .into_inner()) + } +} + +pub struct GrpcHandle { + pools: HashMap, +} + +impl Default for GrpcHandle { + fn default() -> Self { + let pools = HashMap::new(); + Self { pools } + } +} + +impl GrpcHandle { + pub async fn services_from_files( + &mut self, + id: &str, + uri: &Uri, + paths: Vec, + ) -> Result, String> { + let pool = fill_pool_from_files(paths).await?; + self.pools.insert(self.get_pool_key(id, uri), pool.clone()); + Ok(self.services_from_pool(&pool)) + } + pub async fn services_from_reflection( + &mut self, + id: &str, + uri: &Uri, + ) -> Result, String> { + let pool = fill_pool(uri).await?; + self.pools.insert(self.get_pool_key(id, uri), pool.clone()); + Ok(self.services_from_pool(&pool)) + } + + fn get_pool_key(&self, id: &str, uri: &Uri) -> String { + format!("{}-{}", id, uri) + } + + fn services_from_pool(&self, pool: &DescriptorPool) -> Vec { + pool.services() + .map(|s| { + let mut def = ServiceDefinition { + name: s.full_name().to_string(), + methods: vec![], + }; + for method in s.methods() { + let input_message = method.input(); + def.methods.push(MethodDefinition { + name: method.name().to_string(), + server_streaming: method.is_server_streaming(), + client_streaming: method.is_client_streaming(), + schema: serde_json::to_string_pretty(&json_schema::message_to_json_schema( + &pool, + input_message, + )) + .unwrap(), + }) + } + def + }) + .collect::>() + } + + pub async fn server_streaming( + &mut self, + id: &str, + uri: Uri, + proto_files: Vec, + service: &str, + method: &str, + message: &str, + ) -> Result, String> { + self.connect(id, uri, proto_files) + .await? + .server_streaming(service, method, message) + .await + } + + pub async fn client_streaming( + &mut self, + id: &str, + uri: Uri, + proto_files: Vec, + service: &str, + method: &str, + stream: ReceiverStream, + ) -> Result { + self.connect(id, uri, proto_files) + .await? + .client_streaming(service, method, stream) + .await + } + + pub async fn streaming( + &mut self, + id: &str, + uri: Uri, + proto_files: Vec, + service: &str, + method: &str, + stream: ReceiverStream, + ) -> Result, String> { + self.connect(id, uri, proto_files) + .await? + .streaming(service, method, stream) + .await + } + + pub async fn connect( + &mut self, + id: &str, + uri: Uri, + proto_files: Vec, + ) -> Result { + let pool = match self.pools.get(id) { + Some(p) => p.clone(), + None => match proto_files.len() { + 0 => fill_pool(&uri).await?, + _ => { + let pool = fill_pool_from_files(proto_files).await?; + self.pools.insert(id.to_string(), pool.clone()); + pool + } + }, + }; + + let conn = get_transport(); + let connection = GrpcConnection { pool, conn, uri }; + Ok(connection) + } +} diff --git a/src-tauri/grpc/src/proto.rs b/src-tauri/grpc/src/proto.rs new file mode 100644 index 00000000..7219c93e --- /dev/null +++ b/src-tauri/grpc/src/proto.rs @@ -0,0 +1,226 @@ +use std::env::temp_dir; +use std::ops::Deref; +use std::path::PathBuf; +use std::process::Command; +use std::str::FromStr; + +use anyhow::anyhow; +use hyper::client::HttpConnector; +use hyper::Client; +use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; +use log::{debug, warn}; +use prost::Message; +use prost_reflect::{DescriptorPool, MethodDescriptor}; +use prost_types::{FileDescriptorProto, FileDescriptorSet}; +use tokio::fs; +use tokio_stream::StreamExt; +use tonic::body::BoxBody; +use tonic::codegen::http::uri::PathAndQuery; +use tonic::transport::Uri; +use tonic::Request; +use tonic_reflection::pb::server_reflection_client::ServerReflectionClient; +use tonic_reflection::pb::server_reflection_request::MessageRequest; +use tonic_reflection::pb::server_reflection_response::MessageResponse; +use tonic_reflection::pb::ServerReflectionRequest; + +pub async fn fill_pool_from_files(paths: Vec) -> Result { + let mut pool = DescriptorPool::new(); + let random_file_name = format!("{}.desc", uuid::Uuid::new_v4()); + let desc_path = temp_dir().join(random_file_name); + let bin = protoc_bin_vendored::protoc_bin_path().unwrap(); + + let mut cmd = Command::new(bin.clone()); + cmd.arg("--include_imports") + .arg("--include_source_info") + .arg("-o") + .arg(&desc_path); + + for p in paths { + if p.as_path().exists() { + cmd.arg(p.as_path().to_string_lossy().as_ref()); + } else { + continue; + } + + let parent = p.as_path().parent(); + if let Some(parent_path) = parent { + cmd.arg("-I").arg(parent_path); + cmd.arg("-I").arg(parent_path.parent().unwrap()); + } else { + debug!("ignoring {:?} since it does not exist.", parent) + } + } + + let output = cmd.output().map_err(|e| e.to_string())?; + if !output.status.success() { + return Err(format!( + "protoc failed: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + + let bytes = fs::read(desc_path.as_path()) + .await + .map_err(|e| e.to_string())?; + let fdp = FileDescriptorSet::decode(bytes.deref()).map_err(|e| e.to_string())?; + pool.add_file_descriptor_set(fdp) + .map_err(|e| e.to_string())?; + + fs::remove_file(desc_path.as_path()) + .await + .map_err(|e| e.to_string())?; + + Ok(pool) +} + +pub async fn fill_pool(uri: &Uri) -> Result { + let mut pool = DescriptorPool::new(); + let mut client = ServerReflectionClient::with_origin(get_transport(), uri.clone()); + + for service in list_services(&mut client).await? { + if service == "grpc.reflection.v1alpha.ServerReflection" { + continue; + } + file_descriptor_set_from_service_name(&service, &mut pool, &mut client).await; + } + + Ok(pool) +} + +pub fn get_transport() -> Client, BoxBody> { + let connector = HttpsConnectorBuilder::new().with_native_roots(); + let connector = connector.https_or_http().enable_http2().wrap_connector({ + let mut http_connector = HttpConnector::new(); + http_connector.enforce_http(false); + http_connector + }); + Client::builder() + .pool_max_idle_per_host(0) + .http2_only(true) + .build(connector) +} + +async fn list_services( + reflect_client: &mut ServerReflectionClient, BoxBody>>, +) -> Result, String> { + let response = + send_reflection_request(reflect_client, MessageRequest::ListServices("".into())).await?; + + let list_services_response = match response { + MessageResponse::ListServicesResponse(resp) => resp, + _ => panic!("Expected a ListServicesResponse variant"), + }; + + Ok(list_services_response + .service + .iter() + .map(|s| s.name.clone()) + .collect::>()) +} + +async fn file_descriptor_set_from_service_name( + service_name: &str, + pool: &mut DescriptorPool, + client: &mut ServerReflectionClient, BoxBody>>, +) { + let response = match send_reflection_request( + client, + MessageRequest::FileContainingSymbol(service_name.into()), + ) + .await + { + Ok(resp) => resp, + Err(e) => { + warn!( + "Error fetching file descriptor for service {}: {}", + service_name, e + ); + return; + } + }; + + let file_descriptor_response = match response { + MessageResponse::FileDescriptorResponse(resp) => resp, + _ => panic!("Expected a FileDescriptorResponse variant"), + }; + + for fd in file_descriptor_response.file_descriptor_proto { + let fdp = FileDescriptorProto::decode(fd.deref()).unwrap(); + + // Add deps first or else we'll get an error + for dep_name in fdp.clone().dependency { + file_descriptor_set_by_filename(&dep_name, pool, client).await; + } + + pool.add_file_descriptor_proto(fdp) + .expect("add file descriptor proto"); + } +} + +async fn file_descriptor_set_by_filename( + filename: &str, + pool: &mut DescriptorPool, + client: &mut ServerReflectionClient, BoxBody>>, +) { + // We already fetched this file + if let Some(_) = pool.get_file_by_name(filename) { + return; + } + + let response = + send_reflection_request(client, MessageRequest::FileByFilename(filename.into())).await; + let file_descriptor_response = match response { + Ok(MessageResponse::FileDescriptorResponse(resp)) => resp, + Ok(_) => { + panic!("Expected a FileDescriptorResponse variant") + } + Err(e) => { + warn!("Error fetching file descriptor for {}: {}", filename, e); + return; + } + }; + + for fd in file_descriptor_response.file_descriptor_proto { + let fdp = FileDescriptorProto::decode(fd.deref()).unwrap(); + pool.add_file_descriptor_proto(fdp) + .expect("add file descriptor proto"); + } +} + +async fn send_reflection_request( + client: &mut ServerReflectionClient, BoxBody>>, + message: MessageRequest, +) -> Result { + let reflection_request = ServerReflectionRequest { + host: "".into(), // Doesn't matter + message_request: Some(message), + }; + + let request = Request::new(tokio_stream::once(reflection_request)); + + client + .server_reflection_info(request) + .await + .map_err(|e| match e.code() { + tonic::Code::Unavailable => "Failed to connect to endpoint".to_string(), + tonic::Code::Unauthenticated => "Authentication failed".to_string(), + tonic::Code::DeadlineExceeded => "Deadline exceeded".to_string(), + _ => e.to_string(), + })? + .into_inner() + .next() + .await + .expect("steamed response") + .map_err(|e| e.to_string())? + .message_response + .ok_or("No reflection response".to_string()) +} + +pub fn method_desc_to_path(md: &MethodDescriptor) -> PathAndQuery { + let full_name = md.full_name(); + let (namespace, method_name) = full_name + .rsplit_once('.') + .ok_or_else(|| anyhow!("invalid method path")) + .expect("invalid method path"); + PathAndQuery::from_str(&format!("/{}/{}", namespace, method_name)).expect("invalid method path") +} diff --git a/src-tauri/migrations/20240203164833_grpc.sql b/src-tauri/migrations/20240203164833_grpc.sql new file mode 100644 index 00000000..f7dc2d63 --- /dev/null +++ b/src-tauri/migrations/20240203164833_grpc.sql @@ -0,0 +1,59 @@ +CREATE TABLE grpc_requests +( + id TEXT NOT NULL + PRIMARY KEY, + model TEXT DEFAULT 'grpc_request' NOT NULL, + workspace_id TEXT NOT NULL + REFERENCES workspaces + ON DELETE CASCADE, + folder_id TEXT NULL + REFERENCES folders + ON DELETE CASCADE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + name TEXT NOT NULL, + sort_priority REAL NOT NULL, + url TEXT NOT NULL, + service TEXT NULL, + method TEXT NULL, + message TEXT NOT NULL +); + +CREATE TABLE grpc_connections +( + id TEXT NOT NULL + PRIMARY KEY, + model TEXT DEFAULT 'grpc_connection' NOT NULL, + workspace_id TEXT NOT NULL + REFERENCES workspaces + ON DELETE CASCADE, + request_id TEXT NOT NULL + REFERENCES grpc_requests + ON DELETE CASCADE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + service TEXT NOT NULL, + method TEXT NOT NULL, + elapsed INTEGER NOT NULL +); + +CREATE TABLE grpc_messages +( + id TEXT NOT NULL + PRIMARY KEY, + model TEXT DEFAULT 'grpc_message' NOT NULL, + workspace_id TEXT NOT NULL + REFERENCES workspaces + ON DELETE CASCADE, + request_id TEXT NOT NULL + REFERENCES grpc_requests + ON DELETE CASCADE, + connection_id TEXT NOT NULL + REFERENCES grpc_connections + ON DELETE CASCADE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + is_server BOOLEAN NOT NULL, + is_info BOOLEAN NOT NULL, + message TEXT NOT NULL +); diff --git a/src-tauri/migrations/20240206191206_grpc-protos.sql b/src-tauri/migrations/20240206191206_grpc-protos.sql new file mode 100644 index 00000000..9ad50ba1 --- /dev/null +++ b/src-tauri/migrations/20240206191206_grpc-protos.sql @@ -0,0 +1 @@ +ALTER TABLE grpc_requests ADD COLUMN proto_files TEXT DEFAULT '[]' NOT NULL; diff --git a/src-tauri/src/analytics.rs b/src-tauri/src/analytics.rs index b00da023..5388c003 100644 --- a/src-tauri/src/analytics.rs +++ b/src-tauri/src/analytics.rs @@ -1,10 +1,8 @@ use log::{debug, warn}; use serde::{Deserialize, Serialize}; use serde_json::json; -use sqlx::{Pool, Sqlite}; use sqlx::types::JsonValue; -use tauri::{AppHandle, Manager, State}; -use tokio::sync::Mutex; +use tauri::{AppHandle, Manager}; use crate::{is_dev, models}; @@ -16,6 +14,9 @@ pub enum AnalyticsResource { Dialog, Environment, Folder, + GrpcConnection, + GrpcMessage, + GrpcRequest, HttpRequest, HttpResponse, KeyValue, @@ -31,6 +32,9 @@ impl AnalyticsResource { "CookieJar" => Some(AnalyticsResource::CookieJar), "Environment" => Some(AnalyticsResource::Environment), "Folder" => Some(AnalyticsResource::Folder), + "GrpcConnection" => Some(AnalyticsResource::GrpcConnection), + "GrpcMessage" => Some(AnalyticsResource::GrpcMessage), + "GrpcRequest" => Some(AnalyticsResource::GrpcRequest), "HttpRequest" => Some(AnalyticsResource::HttpRequest), "HttpResponse" => Some(AnalyticsResource::HttpResponse), "KeyValue" => Some(AnalyticsResource::KeyValue), @@ -90,6 +94,9 @@ fn resource_name(resource: AnalyticsResource) -> &'static str { AnalyticsResource::Dialog => "dialog", AnalyticsResource::Environment => "environment", AnalyticsResource::Folder => "folder", + AnalyticsResource::GrpcRequest => "grpc_request", + AnalyticsResource::GrpcConnection => "grpc_connection", + AnalyticsResource::GrpcMessage => "grpc_message", AnalyticsResource::HttpRequest => "http_request", AnalyticsResource::HttpResponse => "http_response", AnalyticsResource::KeyValue => "key_value", @@ -129,14 +136,13 @@ pub struct LaunchEventInfo { pub async fn track_launch_event(app_handle: &AppHandle) -> LaunchEventInfo { let namespace = "analytics"; let last_tracked_version_key = "last_tracked_version"; - let db_instance: State<'_, Mutex>> = app_handle.state(); - let pool = &*db_instance.lock().await; let mut info = LaunchEventInfo::default(); - info.num_launches = models::get_key_value_int(namespace, "num_launches", 0, pool).await + 1; + info.num_launches = + models::get_key_value_int(app_handle, namespace, "num_launches", 0).await + 1; info.previous_version = - models::get_key_value_string(namespace, last_tracked_version_key, "", pool).await; + models::get_key_value_string(app_handle, namespace, last_tracked_version_key, "").await; info.current_version = app_handle.package_info().version.to_string(); if info.previous_version.is_empty() { @@ -167,19 +173,18 @@ pub async fn track_launch_event(app_handle: &AppHandle) -> LaunchEventInfo { AnalyticsAction::Launch, Some(json!({ "num_launches": info.num_launches })), ) - .await; - + .await; // Update key values models::set_key_value_string( + app_handle, namespace, last_tracked_version_key, info.current_version.as_str(), - pool, ) .await; - models::set_key_value_int(namespace, "num_launches", info.num_launches, pool).await; + models::set_key_value_int(app_handle, namespace, "num_launches", info.num_launches).await; info } diff --git a/src-tauri/src/http.rs b/src-tauri/src/http.rs index 77561d2b..0ac86b42 100644 --- a/src-tauri/src/http.rs +++ b/src-tauri/src/http.rs @@ -1,5 +1,5 @@ -use std::fs::{create_dir_all, File}; use std::fs; +use std::fs::{create_dir_all, File}; use std::io::Write; use std::path::PathBuf; use std::str::FromStr; @@ -7,28 +7,26 @@ use std::sync::Arc; use std::time::Duration; use base64::Engine; -use http::{HeaderMap, HeaderName, HeaderValue, Method}; use http::header::{ACCEPT, USER_AGENT}; +use http::{HeaderMap, HeaderName, HeaderValue, Method}; use log::{error, info, warn}; -use reqwest::{multipart, Url}; use reqwest::redirect::Policy; -use sqlx::{Pool, Sqlite}; +use reqwest::{multipart, Url}; use sqlx::types::{Json, JsonValue}; -use tauri::{AppHandle, Wry}; +use tauri::AppHandle; -use crate::{emit_side_effect, models, render, response_err}; +use crate::{models, render, response_err}; pub async fn send_http_request( + app_handle: &AppHandle, request: models::HttpRequest, response: &models::HttpResponse, environment: Option, cookie_jar: Option, - app_handle: &AppHandle, - pool: &Pool, download_path: Option, ) -> Result { let environment_ref = environment.as_ref(); - let workspace = models::get_workspace(&request.workspace_id, pool) + let workspace = models::get_workspace(app_handle, &request.workspace_id) .await .expect("Failed to get Workspace"); @@ -88,7 +86,7 @@ pub async fn send_http_request( let url = match Url::from_str(url_string.as_str()) { Ok(u) => u, Err(e) => { - return response_err(response, e.to_string(), app_handle, pool).await; + return response_err(response, e.to_string(), app_handle).await; } }; @@ -293,7 +291,7 @@ pub async fn send_http_request( let sendable_req = match request_builder.build() { Ok(r) => r, Err(e) => { - return response_err(response, e.to_string(), app_handle, pool).await; + return response_err(response, e.to_string(), app_handle).await; } }; @@ -362,12 +360,9 @@ pub async fn send_http_request( ); } - response = models::update_response_if_id(&response, pool) + response = models::update_response_if_id(app_handle, &response) .await .expect("Failed to update response"); - if !request.id.is_empty() { - emit_side_effect(app_handle, "updated_model", &response); - } // Copy response to download path, if specified match (download_path, response.body_path.clone()) { @@ -397,18 +392,13 @@ pub async fn send_http_request( .collect::>(), ); cookie_jar.cookies = json_cookies; - match models::upsert_cookie_jar(pool, &cookie_jar).await { - Ok(updated_jar) => { - emit_side_effect(app_handle, "updated_model", &updated_jar); - } - Err(e) => { - error!("Failed to update cookie jar: {}", e); - } + if let Err(e) = models::upsert_cookie_jar(&app_handle, &cookie_jar).await { + error!("Failed to update cookie jar: {}", e); }; } Ok(response) } - Err(e) => response_err(response, e.to_string(), app_handle, pool).await, + Err(e) => response_err(response, e.to_string(), app_handle).await, } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f29dacc9..afa58d3a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -11,12 +11,16 @@ extern crate objc; use std::collections::HashMap; use std::env::current_dir; use std::fs::{create_dir_all, read_to_string, File}; +use std::path::PathBuf; use std::process::exit; +use std::str::FromStr; +use ::http::uri::InvalidUri; +use ::http::Uri; use fern::colors::ColoredLevelConfig; +use futures::StreamExt; use log::{debug, error, info, warn}; use rand::random; -use serde::Serialize; use serde_json::{json, Value}; use sqlx::migrate::Migrator; use sqlx::types::Json; @@ -31,10 +35,27 @@ use tokio::sync::Mutex; use tokio::time::sleep; use window_shadows::set_shadow; +use grpc::manager::GrpcHandle; +use grpc::{serialize_message, ServiceDefinition}; use window_ext::TrafficLightWindowExt; use crate::analytics::{AnalyticsAction, AnalyticsResource}; use crate::http::send_http_request; +use crate::models::{ + cancel_pending_grpc_connections, cancel_pending_responses, create_response, + delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, + delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, + delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, + get_cookie_jar, get_environment, get_folder, get_grpc_request, get_http_request, + get_http_response, get_key_value_raw, get_or_create_settings, get_workspace, + get_workspace_export_resources, list_cookie_jars, list_environments, list_folders, + list_grpc_connections, list_grpc_messages, list_grpc_requests, list_requests, list_responses, + list_workspaces, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar, + upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_message, + upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar, Environment, + EnvironmentVariable, Folder, GrpcConnection, GrpcMessage, GrpcRequest, HttpRequest, + HttpResponse, KeyValue, Settings, Workspace, +}; use crate::plugin::{ImportResources, ImportResult}; use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater}; @@ -59,11 +80,8 @@ pub struct CustomResponse { pub status_reason: Option<&'static str>, } -async fn migrate_db( - app_handle: AppHandle, - db_instance: &Mutex>, -) -> Result<(), String> { - let pool = &*db_instance.lock().await; +async fn migrate_db(app_handle: AppHandle, db: &Mutex>) -> Result<(), String> { + let pool = &*db.lock().await; let p = app_handle .path_resolver() .resolve_resource("migrations") @@ -76,19 +94,776 @@ async fn migrate_db( } #[tauri::command] -async fn send_ephemeral_request( - mut request: models::HttpRequest, +async fn cmd_grpc_reflect( + request_id: &str, + app_handle: AppHandle, + grpc_handle: State<'_, Mutex>, +) -> Result, String> { + let req = get_grpc_request(&app_handle, request_id) + .await + .map_err(|e| e.to_string())?; + let uri = safe_uri(&req.url).map_err(|e| e.to_string())?; + if req.proto_files.0.len() > 0 { + grpc_handle + .lock() + .await + .services_from_files( + &req.id, + &uri, + req.proto_files + .0 + .iter() + .map(|p| PathBuf::from_str(p).unwrap()) + .collect(), + ) + .await + } else { + grpc_handle + .lock() + .await + .services_from_reflection(&req.id, &uri) + .await + } +} + +#[tauri::command] +async fn cmd_grpc_call_unary( + request_id: &str, + app_handle: AppHandle, + grpc_handle: State<'_, Mutex>, +) -> Result { + let req = get_grpc_request(&app_handle, request_id) + .await + .map_err(|e| e.to_string())?; + let conn = { + let req = req.clone(); + upsert_grpc_connection( + &app_handle, + &GrpcConnection { + workspace_id: req.workspace_id, + request_id: req.id, + ..Default::default() + }, + ) + .await + .map_err(|e| e.to_string())? + }; + + { + let req = req.clone(); + let conn = conn.clone(); + upsert_grpc_message( + &app_handle, + &GrpcMessage { + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.id, + is_info: true, + message: format!("Initiating connection to {}", req.url), + ..Default::default() + }, + ) + .await + .map_err(|e| e.to_string())?; + }; + + let uri = safe_uri(&req.url).map_err(|e| e.to_string())?; + let start = std::time::Instant::now(); + let msg = match grpc_handle + .lock() + .await + .connect( + &req.clone().id, + uri, + req.proto_files + .0 + .iter() + .map(|p| PathBuf::from_str(p).unwrap()) + .collect(), + ) + .await? + .unary( + &req.service.unwrap_or_default(), + &req.method.unwrap_or_default(), + &req.message, + ) + .await + { + Ok(msg) => { + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: serialize_message(&msg)?, + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.clone().id, + is_server: true, + ..Default::default() + }, + ) + .await + } + Err(e) => return Err(e.to_string()), + }; + + upsert_grpc_connection( + &app_handle, + &GrpcConnection { + elapsed: start.elapsed().as_millis() as i64, + ..conn + }, + ) + .await + .map_err(|e| e.to_string())?; + + msg.map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_grpc_client_streaming( + request_id: &str, + app_handle: AppHandle, +) -> Result { + let req = get_grpc_request(&app_handle, request_id) + .await + .map_err(|e| e.to_string())?; + let conn = { + let req = req.clone(); + upsert_grpc_connection( + &app_handle, + &GrpcConnection { + workspace_id: req.workspace_id, + request_id: req.id, + ..Default::default() + }, + ) + .await + .map_err(|e| e.to_string())? + }; + + { + let conn = conn.clone(); + let req = req.clone(); + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: "Initiating connection".to_string(), + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.id, + is_info: true, + ..Default::default() + }, + ) + .await + .unwrap(); + }; + + let (in_msg_tx, in_msg_rx) = tauri::async_runtime::channel::(16); + let maybe_in_msg_tx = std::sync::Mutex::new(Some(in_msg_tx.clone())); + let (cancelled_tx, mut cancelled_rx) = tokio::sync::watch::channel(false); + + let uri = safe_uri(&req.url).map_err(|e| e.to_string())?; + + let in_msg_stream = tokio_stream::wrappers::ReceiverStream::new(in_msg_rx); + + let (service, method) = { + let req = req.clone(); + match (req.service, req.method) { + (Some(service), Some(method)) => (service, method), + _ => return Err("Service and method are required".to_string()), + } + }; + + #[derive(serde::Deserialize)] + enum IncomingMsg { + Message(String), + Commit, + Cancel, + } + + let cb = { + let cancelled_rx = cancelled_rx.clone(); + let app_handle = app_handle.clone(); + let conn = conn.clone(); + let req = req.clone(); + + move |ev: tauri::Event| { + if *cancelled_rx.borrow() { + // Stream is cancelled + return; + } + + let mut maybe_in_msg_tx = maybe_in_msg_tx + .lock() + .expect("previous holder not to panic"); + let in_msg_tx = if let Some(in_msg_tx) = maybe_in_msg_tx.as_ref() { + in_msg_tx + } else { + // This would mean that the stream is already committed because + // we have already dropped the sending half + return; + }; + + match serde_json::from_str::(ev.payload().unwrap()) { + Ok(IncomingMsg::Message(msg)) => { + in_msg_tx.try_send(msg.clone()).unwrap(); + let app_handle = app_handle.clone(); + let req = req.clone(); + let conn = conn.clone(); + tauri::async_runtime::spawn(async move { + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: msg, + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.id, + ..Default::default() + }, + ) + .await + .unwrap(); + }); + } + Ok(IncomingMsg::Commit) => { + maybe_in_msg_tx.take(); + } + Ok(IncomingMsg::Cancel) => { + cancelled_tx.send_replace(true); + } + Err(e) => { + error!("Failed to parse gRPC message: {:?}", e); + } + } + } + }; + let event_handler = + app_handle.listen_global(format!("grpc_client_msg_{}", conn.id).as_str(), cb); + + let start = std::time::Instant::now(); + let grpc_listen = { + let app_handle = app_handle.clone(); + let conn = conn.clone(); + let req = req.clone(); + async move { + let grpc_handle = app_handle.state::>(); + let msg = grpc_handle + .lock() + .await + .connect( + &req.clone().id, + uri, + req.proto_files + .0 + .iter() + .map(|p| PathBuf::from_str(p).unwrap()) + .collect(), + ) + .await + .unwrap() + .client_streaming(&service, &method, in_msg_stream) + .await + .unwrap(); + let message = serialize_message(&msg).unwrap(); + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message, + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.id, + is_server: true, + ..Default::default() + }, + ) + .await + .unwrap(); + } + }; + + { + let conn = conn.clone(); + let app_handle = app_handle.clone(); + tauri::async_runtime::spawn(async move { + tokio::select! { + _ = grpc_listen => { + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: "Connection completed".to_string(), + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.clone().id, + is_info: true, + ..Default::default() + }, + ) + .await.unwrap(); + upsert_grpc_connection( + &app_handle, + &GrpcConnection { + elapsed: start.elapsed().as_millis() as i64, + ..conn + }, + ) + .await + .unwrap(); + }, + _ = cancelled_rx.changed() => { + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: "Connection cancelled".to_string(), + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.clone().id, + is_info: true, + ..Default::default() + }, + ) + .await.unwrap(); + + upsert_grpc_connection( + &app_handle, + &GrpcConnection { + elapsed: start.elapsed().as_millis() as i64, + ..conn + }, + ) + .await + .unwrap(); + }, + } + app_handle.unlisten(event_handler); + }); + }; + + Ok(conn) +} + +#[tauri::command] +async fn cmd_grpc_streaming( + request_id: &str, + app_handle: AppHandle, + grpc_handle: State<'_, Mutex>, +) -> Result { + let req = get_grpc_request(&app_handle, request_id) + .await + .map_err(|e| e.to_string())?; + let conn = { + let req = req.clone(); + upsert_grpc_connection( + &app_handle, + &GrpcConnection { + workspace_id: req.workspace_id, + request_id: req.id, + ..Default::default() + }, + ) + .await + .map_err(|e| e.to_string())? + }; + + { + let conn = conn.clone(); + let req = req.clone(); + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: "Initiating connection".to_string(), + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.id, + is_info: true, + ..Default::default() + }, + ) + .await + .expect("Failed to upsert message"); + }; + + let (in_msg_tx, in_msg_rx) = tauri::async_runtime::channel::(16); + let (cancelled_tx, mut cancelled_rx) = tokio::sync::watch::channel(false); + + let uri = safe_uri(&req.url).map_err(|e| e.to_string())?; + + let in_msg_stream = tokio_stream::wrappers::ReceiverStream::new(in_msg_rx); + + let (service, method) = { + let req = req.clone(); + match (req.service, req.method) { + (Some(service), Some(method)) => (service, method), + _ => return Err("Service and method are required".to_string()), + } + }; + + let start = std::time::Instant::now(); + let mut stream = grpc_handle + .lock() + .await + .connect( + &req.clone().id, + uri, + req.proto_files + .0 + .iter() + .map(|p| PathBuf::from_str(p).unwrap()) + .collect(), + ) + .await? + .streaming(&service, &method, in_msg_stream) + .await + .unwrap(); + + #[derive(serde::Deserialize)] + enum IncomingMsg { + Message(String), + Cancel, + } + + let cb = { + let cancelled_rx = cancelled_rx.clone(); + let app_handle = app_handle.clone(); + let conn = conn.clone(); + let req = req.clone(); + + move |ev: tauri::Event| { + if *cancelled_rx.borrow() { + // Stream is cancelled + return; + } + + match serde_json::from_str::(ev.payload().unwrap()) { + Ok(IncomingMsg::Message(msg)) => { + in_msg_tx.try_send(msg.clone()).unwrap(); + let app_handle = app_handle.clone(); + let req = req.clone(); + let conn = conn.clone(); + tauri::async_runtime::spawn(async move { + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: msg, + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.id, + ..Default::default() + }, + ) + .await + .map_err(|e| e.to_string()) + .unwrap(); + }); + } + Ok(IncomingMsg::Cancel) => { + cancelled_tx.send_replace(true); + } + Err(e) => { + error!("Failed to parse gRPC message: {:?}", e); + } + } + } + }; + let event_handler = + app_handle.listen_global(format!("grpc_client_msg_{}", conn.id).as_str(), cb); + + let grpc_listen = { + let app_handle = app_handle.clone(); + let conn = conn.clone(); + let req = req.clone(); + async move { + loop { + match stream.next().await { + Some(Ok(item)) => { + let item = serialize_message(&item).unwrap(); + let req = req.clone(); + let conn = conn.clone(); + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: item, + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.id, + is_server: true, + ..Default::default() + }, + ) + .await + .unwrap(); + } + Some(Err(e)) => { + error!("gRPC stream error: {:?}", e); + // TODO: Handle error + } + None => { + info!("gRPC stream closed by sender"); + break; + } + } + } + } + }; + + { + let conn = conn.clone(); + tauri::async_runtime::spawn(async move { + let app_handle = app_handle.clone(); + tokio::select! { + _ = grpc_listen => { + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: "Connection completed".to_string(), + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.clone().id, + is_info: true, + ..Default::default() + }, + ) + .await.unwrap(); + upsert_grpc_connection( + &app_handle, + &GrpcConnection{ + elapsed: start.elapsed().as_millis() as i64, + ..conn + }, + ).await.unwrap(); + }, + _ = cancelled_rx.changed() => { + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: "Connection cancelled".to_string(), + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.clone().id, + is_info: true, + ..Default::default() + }, + ) + .await.unwrap(); + upsert_grpc_connection( + &app_handle, + &GrpcConnection{ + elapsed: start.elapsed().as_millis() as i64, + ..conn + }, + ).await.unwrap(); + }, + } + app_handle.unlisten(event_handler); + }); + }; + + Ok(conn.id) +} + +#[tauri::command] +async fn cmd_grpc_server_streaming( + request_id: &str, + app_handle: AppHandle, + grpc_handle: State<'_, Mutex>, +) -> Result { + let req = get_grpc_request(&app_handle, request_id) + .await + .map_err(|e| e.to_string())?; + + let conn = { + let req = req.clone(); + upsert_grpc_connection( + &app_handle, + &GrpcConnection { + workspace_id: req.workspace_id, + request_id: req.id, + ..Default::default() + }, + ) + .await + .map_err(|e| e.to_string())? + }; + + { + let req = req.clone(); + let conn = conn.clone(); + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: "Initiating connection".to_string(), + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.id, + is_info: true, + ..Default::default() + }, + ) + .await + .unwrap(); + } + + let (cancelled_tx, mut cancelled_rx) = tokio::sync::watch::channel(false); + + let (service, method) = match (&req.service, &req.method) { + (Some(service), Some(method)) => (service, method), + _ => return Err("Service and method are required".to_string()), + }; + + let uri = safe_uri(&req.url).map_err(|e| e.to_string())?; + let mut stream = grpc_handle + .lock() + .await + .connect( + &req.clone().id, + uri, + req.proto_files + .0 + .iter() + .map(|p| PathBuf::from_str(p).unwrap()) + .collect(), + ) + .await? + .server_streaming(&service, &method, &req.message) + .await + .expect("FAILED"); + + #[derive(serde::Deserialize)] + enum IncomingMsg { + Cancel, + } + + let cb = { + let cancelled_rx = cancelled_rx.clone(); + + move |ev: tauri::Event| { + if *cancelled_rx.borrow() { + // Stream is cancelled + return; + } + + match serde_json::from_str::(ev.payload().unwrap()) { + Ok(IncomingMsg::Cancel) => { + cancelled_tx.send_replace(true); + } + Err(e) => { + error!("Failed to parse gRPC message: {:?}", e); + } + } + } + }; + let event_handler = + app_handle.listen_global(format!("grpc_client_msg_{}", conn.id).as_str(), cb); + + let start = std::time::Instant::now(); + let grpc_listen = { + let conn_id = conn.clone().id; + let app_handle = app_handle.clone(); + let req = req.clone(); + async move { + loop { + let req = req.clone(); + let conn_id = conn_id.clone(); + let app_handle = app_handle.clone(); + match stream.next().await { + Some(Ok(item)) => { + let item = serialize_message(&item).unwrap(); + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: item, + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn_id, + is_server: true, + ..Default::default() + }, + ) + .await + .map_err(|e| e.to_string()) + .expect("Failed to upsert message"); + } + Some(Err(e)) => { + error!("gRPC stream error: {:?}", e); + // TODO: Handle error + } + None => { + info!("gRPC stream closed by sender"); + break; + } + } + } + } + }; + + { + let conn = conn.clone(); + let req = req.clone(); + let app_handle = app_handle.clone(); + tauri::async_runtime::spawn(async move { + tokio::select! { + _ = grpc_listen => { + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: "Connection completed".to_string(), + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.clone().id, + is_info: true, + ..Default::default() + }, + ) + .await.unwrap(); + upsert_grpc_connection( + &app_handle, + &GrpcConnection{ + elapsed: start.elapsed().as_millis() as i64, + ..conn + }, + ).await.unwrap(); + }, + _ = cancelled_rx.changed() => { + upsert_grpc_message( + &app_handle, + &GrpcMessage { + message: "Connection cancelled".to_string(), + workspace_id: req.workspace_id, + request_id: req.id, + connection_id: conn.clone().id, + is_info: true, + ..Default::default() + }, + ) + .await.unwrap(); + upsert_grpc_connection( + &app_handle, + &GrpcConnection{ + elapsed: start.elapsed().as_millis() as i64, + ..conn + }, + ).await.unwrap(); + }, + } + app_handle.unlisten(event_handler); + }); + } + + Ok(conn) +} + +#[tauri::command] +async fn cmd_send_ephemeral_request( + mut request: HttpRequest, environment_id: Option<&str>, cookie_jar_id: Option<&str>, - app_handle: AppHandle, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - let response = models::HttpResponse::new(); + app_handle: AppHandle, +) -> Result { + let response = HttpResponse::new(); request.id = "".to_string(); let environment = match environment_id { Some(id) => Some( - models::get_environment(id, pool) + get_environment(&app_handle, id) .await .expect("Failed to get environment"), ), @@ -96,7 +871,7 @@ async fn send_ephemeral_request( }; let cookie_jar = match cookie_jar_id { Some(id) => Some( - models::get_cookie_jar(id, pool) + get_cookie_jar(&app_handle, id) .await .expect("Failed to get cookie jar"), ), @@ -105,26 +880,24 @@ async fn send_ephemeral_request( // let cookie_jar_id2 = cookie_jar_id.unwrap_or("").to_string(); send_http_request( + &app_handle, request, &response, environment, cookie_jar, - &app_handle, - pool, None, ) .await } #[tauri::command] -async fn filter_response( +async fn cmd_filter_response( window: Window, - db_instance: State<'_, Mutex>>, + app_handle: AppHandle, response_id: &str, filter: &str, ) -> Result { - let pool = &*db_instance.lock().await; - let response = models::get_response(response_id, pool) + let response = get_http_response(&app_handle, response_id) .await .expect("Failed to get response"); @@ -155,12 +928,11 @@ async fn filter_response( } #[tauri::command] -async fn import_data( +async fn cmd_import_data( window: Window, - db_instance: State<'_, Mutex>>, + app_handle: AppHandle, file_paths: Vec<&str>, ) -> Result { - let pool = &*db_instance.lock().await; let mut result: Option = None; let plugins = vec!["importer-yaak", "importer-insomnia", "importer-postman"]; for plugin_name in plugins { @@ -190,7 +962,7 @@ async fn import_data( info!("Importing resources"); for w in r.resources.workspaces { - let x = models::upsert_workspace(pool, w) + let x = upsert_workspace(&app_handle, w) .await .expect("Failed to create workspace"); imported_resources.workspaces.push(x.clone()); @@ -198,7 +970,7 @@ async fn import_data( } for e in r.resources.environments { - let x = models::upsert_environment(pool, e) + let x = upsert_environment(&app_handle, e) .await .expect("Failed to create environment"); imported_resources.environments.push(x.clone()); @@ -206,7 +978,7 @@ async fn import_data( } for f in r.resources.folders { - let x = models::upsert_folder(pool, f) + let x = upsert_folder(&app_handle, f) .await .expect("Failed to create folder"); imported_resources.folders.push(x.clone()); @@ -214,7 +986,7 @@ async fn import_data( } for r in r.resources.requests { - let x = models::upsert_request(pool, r) + let x = upsert_http_request(&app_handle, r) .await .expect("Failed to create request"); imported_resources.requests.push(x.clone()); @@ -227,14 +999,12 @@ async fn import_data( } #[tauri::command] -async fn export_data( - app_handle: AppHandle, - db_instance: State<'_, Mutex>>, +async fn cmd_export_data( + app_handle: AppHandle, export_path: &str, workspace_id: &str, ) -> Result<(), String> { - let pool = &*db_instance.lock().await; - let export_data = models::get_workspace_export_resources(&app_handle, pool, workspace_id).await; + let export_data = get_workspace_export_resources(&app_handle, workspace_id).await; let f = File::options() .create(true) .truncate(true) @@ -259,24 +1029,20 @@ async fn export_data( } #[tauri::command] -async fn send_request( - window: Window, - db_instance: State<'_, Mutex>>, +async fn cmd_send_request( + app_handle: AppHandle, request_id: &str, environment_id: Option<&str>, cookie_jar_id: Option<&str>, download_dir: Option<&str>, -) -> Result { - let pool = &*db_instance.lock().await; - let app_handle = window.app_handle(); - - let request = models::get_request(request_id, pool) +) -> Result { + let request = get_http_request(&app_handle, request_id) .await .expect("Failed to get request"); let environment = match environment_id { Some(id) => Some( - models::get_environment(id, pool) + get_environment(&app_handle, id) .await .expect("Failed to get environment"), ), @@ -285,14 +1051,15 @@ async fn send_request( let cookie_jar = match cookie_jar_id { Some(id) => Some( - models::get_cookie_jar(id, pool) + get_cookie_jar(&app_handle, id) .await .expect("Failed to get cookie jar"), ), None => None, }; - let response = models::create_response( + let response = create_response( + &app_handle, &request.id, 0, 0, @@ -304,7 +1071,6 @@ async fn send_request( vec![], None, None, - pool, ) .await .expect("Failed to create response"); @@ -315,38 +1081,33 @@ async fn send_request( None }; - emit_side_effect(&app_handle, "created_model", response.clone()); - send_http_request( + &app_handle, request.clone(), &response, environment, cookie_jar, - &app_handle, - &pool, download_path, ) .await } async fn response_err( - response: &models::HttpResponse, + response: &HttpResponse, error: String, - app_handle: &AppHandle, - pool: &Pool, -) -> Result { + app_handle: &AppHandle, +) -> Result { let mut response = response.clone(); response.elapsed = -1; response.error = Some(error.clone()); - response = models::update_response_if_id(&response, pool) + response = update_response_if_id(&app_handle, &response) .await .expect("Failed to update response"); - emit_side_effect(app_handle, "updated_model", &response); Ok(response) } #[tauri::command] -async fn track_event( +async fn cmd_track_event( window: Window, resource: &str, action: &str, @@ -368,121 +1129,88 @@ async fn track_event( } #[tauri::command] -async fn set_update_mode( - update_mode: &str, - window: Window, - db_instance: State<'_, Mutex>>, -) -> Result { - set_key_value("app", "update_mode", update_mode, window, db_instance).await +async fn cmd_set_update_mode(update_mode: &str, app_handle: AppHandle) -> Result { + cmd_set_key_value("app", "update_mode", update_mode, app_handle) + .await + .map_err(|e| e.to_string()) } #[tauri::command] -async fn get_key_value( +async fn cmd_get_key_value( namespace: &str, key: &str, - db_instance: State<'_, Mutex>>, -) -> Result, ()> { - let pool = &*db_instance.lock().await; - let result = models::get_key_value_raw(namespace, key, pool).await; + app_handle: AppHandle, +) -> Result, ()> { + let result = get_key_value_raw(&app_handle, namespace, key).await; Ok(result) } #[tauri::command] -async fn set_key_value( +async fn cmd_set_key_value( namespace: &str, key: &str, value: &str, - window: Window, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - let (key_value, created) = models::set_key_value_raw(namespace, key, value, pool).await; - - if created { - emit_and_return(&window, "created_model", key_value) - } else { - emit_and_return(&window, "updated_model", key_value) - } + app_handle: AppHandle, +) -> Result { + let (key_value, _created) = set_key_value_raw(&app_handle, namespace, key, value).await; + Ok(key_value) } #[tauri::command] -async fn create_workspace( - name: &str, - window: Window, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - let created_workspace = - models::upsert_workspace(pool, models::Workspace::new(name.to_string())) - .await - .expect("Failed to create Workspace"); - - emit_and_return(&window, "created_model", created_workspace) -} - -#[tauri::command] -async fn update_cookie_jar( - cookie_jar: models::CookieJar, - window: Window, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - println!("Updating cookie jar {}", cookie_jar.cookies.len()); - - let updated = models::upsert_cookie_jar(pool, &cookie_jar) +async fn cmd_create_workspace(name: &str, app_handle: AppHandle) -> Result { + upsert_workspace(&app_handle, Workspace::new(name.to_string())) .await - .expect("Failed to update cookie jar"); - - emit_and_return(&window, "updated_model", updated) + .map_err(|e| e.to_string()) } #[tauri::command] -async fn delete_cookie_jar( - window: Window, - db_instance: State<'_, Mutex>>, +async fn cmd_update_cookie_jar( + cookie_jar: CookieJar, + app_handle: AppHandle, +) -> Result { + upsert_cookie_jar(&app_handle, &cookie_jar) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_delete_cookie_jar( + app_handle: AppHandle, cookie_jar_id: &str, -) -> Result { - let pool = &*db_instance.lock().await; - let req = models::delete_cookie_jar(cookie_jar_id, pool) +) -> Result { + delete_cookie_jar(&app_handle, cookie_jar_id) .await - .expect("Failed to delete cookie jar"); - emit_and_return(&window, "deleted_model", req) + .map_err(|e| e.to_string()) } #[tauri::command] -async fn create_cookie_jar( +async fn cmd_create_cookie_jar( workspace_id: &str, name: &str, - window: Window, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - let created_cookie_jar = models::upsert_cookie_jar( - pool, - &models::CookieJar { + app_handle: AppHandle, +) -> Result { + upsert_cookie_jar( + &app_handle, + &CookieJar { name: name.to_string(), workspace_id: workspace_id.to_string(), ..Default::default() }, ) .await - .expect("Failed to create cookie jar"); - - emit_and_return(&window, "created_model", created_cookie_jar) + .map_err(|e| e.to_string()) } #[tauri::command] -async fn create_environment( +async fn cmd_create_environment( workspace_id: &str, name: &str, - variables: Vec, - window: Window, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - let created_environment = models::upsert_environment( - pool, - models::Environment { + variables: Vec, + app_handle: AppHandle, +) -> Result { + upsert_environment( + &app_handle, + Environment { workspace_id: workspace_id.to_string(), name: name.to_string(), variables: Json(variables), @@ -490,24 +1218,52 @@ async fn create_environment( }, ) .await - .expect("Failed to create environment"); - - emit_and_return(&window, "created_model", created_environment) + .map_err(|e| e.to_string()) } #[tauri::command] -async fn create_request( +async fn cmd_create_grpc_request( workspace_id: &str, name: &str, sort_priority: f64, folder_id: Option<&str>, - window: Window, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - let created_request = models::upsert_request( - pool, - models::HttpRequest { + app_handle: AppHandle, +) -> Result { + upsert_grpc_request( + &app_handle, + &GrpcRequest { + workspace_id: workspace_id.to_string(), + name: name.to_string(), + folder_id: folder_id.map(|s| s.to_string()), + sort_priority, + ..Default::default() + }, + ) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_duplicate_grpc_request( + id: &str, + app_handle: AppHandle, +) -> Result { + duplicate_grpc_request(&app_handle, id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_create_http_request( + workspace_id: &str, + name: &str, + sort_priority: f64, + folder_id: Option<&str>, + app_handle: AppHandle, +) -> Result { + upsert_http_request( + &app_handle, + HttpRequest { workspace_id: workspace_id.to_string(), name: name.to_string(), method: "GET".to_string(), @@ -517,104 +1273,100 @@ async fn create_request( }, ) .await - .expect("Failed to create request"); - - emit_and_return(&window, "created_model", created_request) + .map_err(|e| e.to_string()) } #[tauri::command] -async fn duplicate_request( +async fn cmd_duplicate_http_request( id: &str, - window: Window, - db_instance: State<'_, Mutex>>, -) -> Result { - 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, - db_instance: State<'_, Mutex>>, -) -> Result { - 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, - db_instance: State<'_, Mutex>>, -) -> Result { - 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, - db_instance: State<'_, Mutex>>, -) -> Result { - 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, - db_instance: State<'_, Mutex>>, - request_id: &str, -) -> Result { - 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>>, -) -> Result, String> { - let pool = &*db_instance.lock().await; - models::find_folders(workspace_id, pool) + app_handle: AppHandle, +) -> Result { + duplicate_http_request(&app_handle, id) .await .map_err(|e| e.to_string()) } #[tauri::command] -async fn create_folder( +async fn cmd_update_workspace( + workspace: Workspace, + app_handle: AppHandle, +) -> Result { + upsert_workspace(&app_handle, workspace) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_update_environment( + environment: Environment, + app_handle: AppHandle, +) -> Result { + upsert_environment(&app_handle, environment) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_update_grpc_request( + request: GrpcRequest, + app_handle: AppHandle, +) -> Result { + upsert_grpc_request(&app_handle, &request) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_update_http_request( + request: HttpRequest, + app_handle: AppHandle, +) -> Result { + upsert_http_request(&app_handle, request) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_delete_grpc_request( + app_handle: AppHandle, + request_id: &str, +) -> Result { + delete_grpc_request(&app_handle, request_id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_delete_http_request( + app_handle: AppHandle, + request_id: &str, +) -> Result { + delete_http_request(&app_handle, request_id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_list_folders( + workspace_id: &str, + app_handle: AppHandle, +) -> Result, String> { + list_folders(&app_handle, workspace_id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_create_folder( workspace_id: &str, name: &str, sort_priority: f64, folder_id: Option<&str>, - window: Window, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - let created_request = models::upsert_folder( - pool, - models::Folder { + app_handle: AppHandle, +) -> Result { + upsert_folder( + &app_handle, + Folder { workspace_id: workspace_id.to_string(), name: name.to_string(), folder_id: folder_id.map(|s| s.to_string()), @@ -623,57 +1375,70 @@ async fn create_folder( }, ) .await - .expect("Failed to create folder"); - - emit_and_return(&window, "created_model", created_request) + .map_err(|e| e.to_string()) } #[tauri::command] -async fn update_folder( - folder: models::Folder, - window: Window, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - let updated_folder = models::upsert_folder(pool, folder) +async fn cmd_update_folder(folder: Folder, app_handle: AppHandle) -> Result { + upsert_folder(&app_handle, folder) .await - .expect("Failed to update request"); - emit_and_return(&window, "updated_model", updated_folder) + .map_err(|e| e.to_string()) } #[tauri::command] -async fn delete_folder( - window: Window, - db_instance: State<'_, Mutex>>, - folder_id: &str, -) -> Result { - let pool = &*db_instance.lock().await; - let req = models::delete_folder(folder_id, pool) +async fn cmd_delete_folder(app_handle: AppHandle, folder_id: &str) -> Result { + delete_folder(&app_handle, folder_id) .await - .expect("Failed to delete folder"); - emit_and_return(&window, "deleted_model", req) + .map_err(|e| e.to_string()) } #[tauri::command] -async fn delete_environment( - window: Window, - db_instance: State<'_, Mutex>>, +async fn cmd_delete_environment( + app_handle: AppHandle, environment_id: &str, -) -> Result { - let pool = &*db_instance.lock().await; - let req = models::delete_environment(environment_id, pool) +) -> Result { + delete_environment(&app_handle, environment_id) .await - .expect("Failed to delete environment"); - emit_and_return(&window, "deleted_model", req) + .map_err(|e| e.to_string()) } #[tauri::command] -async fn list_requests( +async fn cmd_list_grpc_connections( + request_id: &str, + app_handle: AppHandle, +) -> Result, String> { + list_grpc_connections(&app_handle, request_id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_list_grpc_messages( + connection_id: &str, + app_handle: AppHandle, +) -> Result, String> { + list_grpc_messages(&app_handle, connection_id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_list_grpc_requests( workspace_id: &str, - db_instance: State<'_, Mutex>>, -) -> Result, String> { - let pool = &*db_instance.lock().await; - let requests = models::find_requests(workspace_id, pool) + app_handle: AppHandle, +) -> Result, String> { + let requests = list_grpc_requests(&app_handle, workspace_id) + .await + .map_err(|e| e.to_string())?; + Ok(requests) +} + +#[tauri::command] +async fn cmd_list_http_requests( + workspace_id: &str, + app_handle: AppHandle, +) -> Result, String> { + let requests = list_requests(&app_handle, workspace_id) .await .expect("Failed to find requests"); // .map_err(|e| e.to_string()) @@ -681,12 +1446,11 @@ async fn list_requests( } #[tauri::command] -async fn list_environments( +async fn cmd_list_environments( workspace_id: &str, - db_instance: State<'_, Mutex>>, -) -> Result, String> { - let pool = &*db_instance.lock().await; - let environments = models::find_environments(workspace_id, pool) + app_handle: AppHandle, +) -> Result, String> { + let environments = list_environments(&app_handle, workspace_id) .await .expect("Failed to find environments"); @@ -694,73 +1458,59 @@ async fn list_environments( } #[tauri::command] -async fn get_settings(db_instance: State<'_, Mutex>>) -> Result { - let pool = &*db_instance.lock().await; - Ok(models::get_or_create_settings(pool).await) +async fn cmd_get_settings(app_handle: AppHandle) -> Result { + Ok(get_or_create_settings(&app_handle).await) } #[tauri::command] -async fn update_settings( - settings: models::Settings, - window: Window, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - - let updated_settings = models::update_settings(pool, settings) - .await - .expect("Failed to update settings"); - - emit_and_return(&window, "updated_model", updated_settings) -} - -#[tauri::command] -async fn get_folder( - id: &str, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - models::get_folder(id, pool) +async fn cmd_update_settings( + settings: Settings, + app_handle: AppHandle, +) -> Result { + update_settings(&app_handle, settings) .await .map_err(|e| e.to_string()) } #[tauri::command] -async fn get_request( - id: &str, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - models::get_request(id, pool) +async fn cmd_get_folder(id: &str, app_handle: AppHandle) -> Result { + get_folder(&app_handle, id).await.map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_get_grpc_request(id: &str, app_handle: AppHandle) -> Result { + get_grpc_request(&app_handle, id) .await .map_err(|e| e.to_string()) } #[tauri::command] -async fn get_cookie_jar( - id: &str, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - models::get_cookie_jar(id, pool) +async fn cmd_get_http_request(id: &str, app_handle: AppHandle) -> Result { + get_http_request(&app_handle, id) .await .map_err(|e| e.to_string()) } #[tauri::command] -async fn list_cookie_jars( +async fn cmd_get_cookie_jar(id: &str, app_handle: AppHandle) -> Result { + get_cookie_jar(&app_handle, id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_list_cookie_jars( workspace_id: &str, - db_instance: State<'_, Mutex>>, -) -> Result, String> { - let pool = &*db_instance.lock().await; - let cookie_jars = models::find_cookie_jars(workspace_id, pool) + app_handle: AppHandle, +) -> Result, String> { + let cookie_jars = list_cookie_jars(&app_handle, workspace_id) .await .expect("Failed to find cookie jars"); if cookie_jars.is_empty() { - let cookie_jar = models::upsert_cookie_jar( - pool, - &models::CookieJar { + let cookie_jar = upsert_cookie_jar( + &app_handle, + &CookieJar { name: "Default".to_string(), workspace_id: workspace_id.to_string(), ..Default::default() @@ -775,75 +1525,76 @@ async fn list_cookie_jars( } #[tauri::command] -async fn get_environment( - id: &str, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - models::get_environment(id, pool) +async fn cmd_get_environment(id: &str, app_handle: AppHandle) -> Result { + get_environment(&app_handle, id) .await .map_err(|e| e.to_string()) } #[tauri::command] -async fn get_workspace( - id: &str, - db_instance: State<'_, Mutex>>, -) -> Result { - let pool = &*db_instance.lock().await; - models::get_workspace(id, pool) +async fn cmd_get_workspace(id: &str, app_handle: AppHandle) -> Result { + get_workspace(&app_handle, id) .await .map_err(|e| e.to_string()) } #[tauri::command] -async fn list_responses( +async fn cmd_list_http_responses( request_id: &str, limit: Option, - db_instance: State<'_, Mutex>>, -) -> Result, String> { - let pool = &*db_instance.lock().await; - models::find_responses(request_id, limit, pool) + app_handle: AppHandle, +) -> Result, String> { + list_responses(&app_handle, request_id, limit) .await .map_err(|e| e.to_string()) } #[tauri::command] -async fn delete_response( +async fn cmd_delete_http_response(id: &str, app_handle: AppHandle) -> Result { + delete_http_response(&app_handle, id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_delete_grpc_connection( id: &str, - window: Window, - db_instance: State<'_, Mutex>>, -) -> Result { - 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>>, -) -> Result<(), String> { - let pool = &*db_instance.lock().await; - models::delete_all_responses(request_id, pool) + app_handle: AppHandle, +) -> Result { + delete_grpc_connection(&app_handle, id) .await .map_err(|e| e.to_string()) } #[tauri::command] -async fn list_workspaces( - db_instance: State<'_, Mutex>>, -) -> Result, String> { - let pool = &*db_instance.lock().await; - let workspaces = models::find_workspaces(pool) +async fn cmd_delete_all_grpc_connections( + request_id: &str, + app_handle: AppHandle, +) -> Result<(), String> { + delete_all_grpc_connections(&app_handle, request_id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_delete_all_http_responses( + request_id: &str, + app_handle: AppHandle, +) -> Result<(), String> { + delete_all_http_responses(&app_handle, request_id) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn cmd_list_workspaces(app_handle: AppHandle) -> Result, String> { + let workspaces = list_workspaces(&app_handle) .await .expect("Failed to find workspaces"); if workspaces.is_empty() { - let workspace = models::upsert_workspace( - pool, - models::Workspace { + let workspace = upsert_workspace( + &app_handle, + Workspace { name: "Yaak".to_string(), ..Default::default() }, @@ -857,32 +1608,27 @@ async fn list_workspaces( } #[tauri::command] -async fn new_window(window: Window, url: &str) -> Result<(), String> { +async fn cmd_new_window(window: Window, url: &str) -> Result<(), String> { create_window(&window.app_handle(), Some(url)); Ok(()) } #[tauri::command] -async fn delete_workspace( - window: Window, - db_instance: State<'_, Mutex>>, +async fn cmd_delete_workspace( + app_handle: AppHandle, workspace_id: &str, -) -> Result { - let pool = &*db_instance.lock().await; - let workspace = models::delete_workspace(workspace_id, pool) +) -> Result { + delete_workspace(&app_handle, workspace_id) .await - .expect("Failed to delete Workspace"); - emit_and_return(&window, "deleted_model", workspace) + .map_err(|e| e.to_string()) } #[tauri::command] -async fn check_for_updates( - app_handle: AppHandle, - db_instance: State<'_, Mutex>>, +async fn cmd_check_for_updates( + app_handle: AppHandle, yaak_updater: State<'_, Mutex>, ) -> Result { - let pool = &*db_instance.lock().await; - let update_mode = get_update_mode(pool).await; + let update_mode = get_update_mode(&app_handle).await; yaak_updater .lock() .await @@ -893,21 +1639,25 @@ async fn check_for_updates( fn main() { tauri::Builder::default() + .plugin(tauri_plugin_window_state::Builder::default().build()) .plugin( tauri_plugin_log::Builder::default() .targets([LogTarget::LogDir, LogTarget::Stdout, LogTarget::Webview]) - .level_for("tao", log::LevelFilter::Info) - .level_for("sqlx", log::LevelFilter::Warn) - .level_for("hyper", log::LevelFilter::Info) - .level_for("tracing", log::LevelFilter::Info) - .level_for("reqwest", log::LevelFilter::Info) - .level_for("tokio_util", log::LevelFilter::Info) .level_for("cookie_store", log::LevelFilter::Info) + .level_for("h2", log::LevelFilter::Info) + .level_for("hyper", log::LevelFilter::Info) + .level_for("hyper_rustls", log::LevelFilter::Info) + .level_for("reqwest", log::LevelFilter::Info) + .level_for("sqlx", log::LevelFilter::Warn) + .level_for("tao", log::LevelFilter::Info) + .level_for("tokio_util", log::LevelFilter::Info) + .level_for("tonic", log::LevelFilter::Info) + .level_for("tower", log::LevelFilter::Info) + .level_for("tracing", log::LevelFilter::Info) .with_colors(ColoredLevelConfig::default()) .level(log::LevelFilter::Trace) .build(), ) - .plugin(tauri_plugin_window_state::Builder::default().build()) .setup(|app| { let app_data_dir = app.path_resolver().app_data_dir().unwrap(); let app_config_dir = app.path_resolver().app_config_dir().unwrap(); @@ -933,69 +1683,89 @@ fn main() { let url = format!("sqlite://{}?mode=rwc", p_string); info!("Connecting to database at {}", url); + // Add updater + let yaak_updater = YaakUpdater::new(); + app.manage(Mutex::new(yaak_updater)); + + // Add GRPC manager + let grpc_handle = GrpcHandle::default(); + app.manage(Mutex::new(grpc_handle)); + + // Add DB handle tauri::async_runtime::block_on(async move { let pool = SqlitePool::connect(p.to_str().unwrap()) .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 h = app.handle(); + let _ = cancel_pending_responses(&h).await; + let _ = cancel_pending_grpc_connections(&h).await; + }); - let yaak_updater = YaakUpdater::new(); - app.manage(Mutex::new(yaak_updater)); - - let _ = models::cancel_pending_responses(&pool).await; - - Ok(()) - }) + Ok(()) }) .invoke_handler(tauri::generate_handler![ - check_for_updates, - create_cookie_jar, - create_environment, - create_folder, - create_request, - create_workspace, - delete_all_responses, - delete_cookie_jar, - delete_environment, - delete_folder, - delete_request, - delete_response, - delete_workspace, - duplicate_request, - export_data, - filter_response, - get_cookie_jar, - get_environment, - get_folder, - get_key_value, - get_request, - get_settings, - get_workspace, - import_data, - list_cookie_jars, - list_environments, - list_folders, - list_requests, - list_responses, - list_workspaces, - new_window, - send_ephemeral_request, - send_request, - set_key_value, - set_update_mode, - track_event, - update_cookie_jar, - update_environment, - update_folder, - update_request, - update_settings, - update_workspace, + cmd_check_for_updates, + cmd_create_cookie_jar, + cmd_create_environment, + cmd_create_folder, + cmd_create_grpc_request, + cmd_create_http_request, + cmd_create_workspace, + cmd_delete_all_http_responses, + cmd_delete_all_grpc_connections, + cmd_delete_cookie_jar, + cmd_delete_environment, + cmd_delete_folder, + cmd_delete_grpc_request, + cmd_delete_grpc_connection, + cmd_delete_http_request, + cmd_delete_http_response, + cmd_delete_workspace, + cmd_duplicate_http_request, + cmd_duplicate_grpc_request, + cmd_export_data, + cmd_filter_response, + cmd_get_cookie_jar, + cmd_get_environment, + cmd_get_folder, + cmd_get_key_value, + cmd_get_http_request, + cmd_get_grpc_request, + cmd_get_settings, + cmd_get_workspace, + cmd_grpc_call_unary, + cmd_grpc_client_streaming, + cmd_grpc_server_streaming, + cmd_grpc_streaming, + cmd_grpc_reflect, + cmd_import_data, + cmd_list_cookie_jars, + cmd_list_environments, + cmd_list_folders, + cmd_list_http_requests, + cmd_list_grpc_requests, + cmd_list_grpc_connections, + cmd_list_grpc_messages, + cmd_list_http_responses, + cmd_list_workspaces, + cmd_new_window, + cmd_send_ephemeral_request, + cmd_send_request, + cmd_set_key_value, + cmd_set_update_mode, + cmd_track_event, + cmd_update_cookie_jar, + cmd_update_environment, + cmd_update_folder, + cmd_update_grpc_request, + cmd_update_http_request, + cmd_update_settings, + cmd_update_workspace, ]) .build(tauri::generate_context!()) .expect("error while running tauri application") @@ -1050,9 +1820,7 @@ fn main() { // Run update check whenever window is focused tauri::async_runtime::spawn(async move { let val: State<'_, Mutex> = h.state(); - let db_instance: State<'_, Mutex>> = h.state(); - let pool = &*db_instance.lock().await; - let update_mode = get_update_mode(pool).await; + let update_mode = get_update_mode(&h).await; _ = val.lock().await.check(&h, update_mode).await; }); } @@ -1072,7 +1840,7 @@ fn is_dev() -> bool { } } -fn create_window(handle: &AppHandle, url: Option<&str>) -> Window { +fn create_window(handle: &AppHandle, url: Option<&str>) -> Window { let app_menu = window_menu::os_default("Yaak".to_string().as_str()); let window_num = handle.windows().len(); let window_id = format!("wnd_{}", window_num); @@ -1162,22 +1930,16 @@ fn create_window(handle: &AppHandle, url: Option<&str>) -> Window { win } -/// Emit an event to all windows, with a source window -fn emit_and_return( - current_window: &Window, - event: &str, - payload: S, -) -> Result { - 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(app_handle: &AppHandle, event: &str, payload: S) { - app_handle.emit_all(event, &payload).unwrap(); -} - -async fn get_update_mode(pool: &Pool) -> UpdateMode { - let settings = models::get_or_create_settings(pool).await; +async fn get_update_mode(app_handle: &AppHandle) -> UpdateMode { + let settings = get_or_create_settings(app_handle).await; update_mode_from_str(settings.update_channel.as_str()) } + +fn safe_uri(endpoint: &str) -> Result { + let uri = if endpoint.starts_with("http://") || endpoint.starts_with("https://") { + Uri::from_str(endpoint)? + } else { + Uri::from_str(&format!("http://{}", endpoint))? + }; + Ok(uri) +} diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index d33fd3ce..5620ca7b 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -4,10 +4,11 @@ use std::fs; use log::error; use rand::distributions::{Alphanumeric, DistString}; use serde::{Deserialize, Serialize}; -use sqlx::{Pool, Sqlite}; -use sqlx::types::{Json, JsonValue}; use sqlx::types::chrono::NaiveDateTime; -use tauri::AppHandle; +use sqlx::types::{Json, JsonValue}; +use sqlx::{Pool, Sqlite}; +use tauri::{AppHandle, Manager}; +use tokio::sync::Mutex; fn default_true() -> bool { true @@ -58,9 +59,7 @@ impl Workspace { } #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] -pub struct CookieX { - -} +pub struct CookieX {} #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] @@ -95,6 +94,19 @@ pub struct EnvironmentVariable { pub value: String, } +#[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, + pub model: String, + pub name: String, + pub sort_priority: f64, +} + #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct HttpRequestHeader { @@ -139,19 +151,6 @@ pub struct HttpRequest { pub headers: Json>, } -#[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, - pub model: String, - pub name: String, - pub sort_priority: f64, -} - #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct HttpResponseHeader { @@ -190,6 +189,65 @@ impl HttpResponse { } } +#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default, rename_all = "camelCase")] +pub struct GrpcRequest { + pub id: String, + pub model: String, + pub workspace_id: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub folder_id: Option, + pub name: String, + pub sort_priority: f64, + pub url: String, + pub service: Option, + pub method: Option, + pub message: String, + pub proto_files: Json>, +} + +#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default, rename_all = "camelCase")] +pub struct GrpcConnection { + pub id: String, + pub model: String, + pub workspace_id: String, + pub request_id: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub service: String, + pub method: String, + pub elapsed: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default, rename_all = "camelCase")] +pub struct GrpcMessage { + pub id: String, + pub model: String, + pub workspace_id: String, + pub request_id: String, + pub connection_id: String, + pub created_at: NaiveDateTime, + pub message: String, + pub is_server: bool, + pub is_info: bool, +} + +#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] +#[serde(default, rename_all = "camelCase")] +pub struct GrpcResponse { + pub id: String, + pub model: String, + pub workspace_id: String, + pub grpc_endpoint_id: String, + pub grpc_connection_id: String, + pub request_id: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, +} + #[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)] #[serde(default, rename_all = "camelCase")] pub struct KeyValue { @@ -202,32 +260,32 @@ pub struct KeyValue { } pub async fn set_key_value_string( + app_handle: &AppHandle, namespace: &str, key: &str, value: &str, - pool: &Pool, ) -> (KeyValue, bool) { let encoded = serde_json::to_string(value); - set_key_value_raw(namespace, key, &encoded.unwrap(), pool).await + set_key_value_raw(app_handle, namespace, key, &encoded.unwrap()).await } pub async fn set_key_value_int( + app_handle: &AppHandle, namespace: &str, key: &str, value: i32, - pool: &Pool, ) -> (KeyValue, bool) { let encoded = serde_json::to_string(&value); - set_key_value_raw(namespace, key, &encoded.unwrap(), pool).await + set_key_value_raw(app_handle, namespace, key, &encoded.unwrap()).await } pub async fn get_key_value_string( + app_handle: &AppHandle, namespace: &str, key: &str, default: &str, - pool: &Pool, ) -> String { - match get_key_value_raw(namespace, key, pool).await { + match get_key_value_raw(app_handle, namespace, key).await { None => default.to_string(), Some(v) => { let result = serde_json::from_str(&v.value); @@ -238,17 +296,17 @@ pub async fn get_key_value_string( default.to_string() } } - }, + } } } pub async fn get_key_value_int( + app_handle: &AppHandle, namespace: &str, key: &str, default: i32, - pool: &Pool, ) -> i32 { - match get_key_value_raw(namespace, key, pool).await { + match get_key_value_raw(app_handle, namespace, key).await { None => default.clone(), Some(v) => { let result = serde_json::from_str(&v.value); @@ -259,17 +317,18 @@ pub async fn get_key_value_int( default.clone() } } - }, + } } } pub async fn set_key_value_raw( + app_handle: &AppHandle, namespace: &str, key: &str, value: &str, - pool: &Pool, ) -> (KeyValue, bool) { - let existing = get_key_value_raw(namespace, key, pool).await; + let db = get_db(app_handle).await; + let existing = get_key_value_raw(app_handle, namespace, key).await; sqlx::query!( r#" INSERT INTO key_values (namespace, key, value) @@ -281,17 +340,22 @@ pub async fn set_key_value_raw( key, value, ) - .execute(pool) + .execute(&db) .await .expect("Failed to insert key value"); - let kv = get_key_value_raw(namespace, key, pool) + let kv = get_key_value_raw(app_handle, namespace, key) .await .expect("Failed to get key value"); (kv, existing.is_none()) } -pub async fn get_key_value_raw(namespace: &str, key: &str, pool: &Pool) -> Option { +pub async fn get_key_value_raw( + app_handle: &AppHandle, + namespace: &str, + key: &str, +) -> Option { + let db = get_db(app_handle).await; sqlx::query_as!( KeyValue, r#" @@ -302,58 +366,47 @@ pub async fn get_key_value_raw(namespace: &str, key: &str, pool: &Pool) namespace, key, ) - .fetch_one(pool) + .fetch_one(&db) .await .ok() } -pub async fn find_workspaces(pool: &Pool) -> Result, sqlx::Error> { +pub async fn list_workspaces(app_handle: &AppHandle) -> Result, sqlx::Error> { + let db = get_db(app_handle).await; sqlx::query_as!( Workspace, r#" SELECT - id, - model, - created_at, - updated_at, - name, - description, - setting_request_timeout, - setting_follow_redirects, - setting_validate_certificates, + id, model, created_at, updated_at, name, description, setting_request_timeout, + setting_follow_redirects, setting_validate_certificates, variables AS "variables!: sqlx::types::Json>" FROM workspaces "#, ) - .fetch_all(pool) + .fetch_all(&db) .await } -pub async fn get_workspace(id: &str, pool: &Pool) -> Result { +pub async fn get_workspace(app_handle: &AppHandle, id: &str) -> Result { + let db = get_db(app_handle).await; sqlx::query_as!( Workspace, r#" SELECT - id, - model, - created_at, - updated_at, - name, - description, - setting_request_timeout, - setting_follow_redirects, - setting_validate_certificates, + id, model, created_at, updated_at, name, description, setting_request_timeout, + setting_follow_redirects, setting_validate_certificates, variables AS "variables!: sqlx::types::Json>" FROM workspaces WHERE id = ? "#, id, ) - .fetch_one(pool) + .fetch_one(&db) .await } -pub async fn delete_workspace(id: &str, pool: &Pool) -> Result { - let workspace = get_workspace(id, pool).await?; +pub async fn delete_workspace(app_handle: &AppHandle, id: &str) -> Result { + let db = get_db(app_handle).await; + let workspace = get_workspace(app_handle, id).await?; let _ = sqlx::query!( r#" DELETE FROM workspaces @@ -361,58 +414,54 @@ pub async fn delete_workspace(id: &str, pool: &Pool) -> Result) -> Result { +pub async fn get_cookie_jar(app_handle: &AppHandle, id: &str) -> Result { + let db = get_db(app_handle).await; sqlx::query_as!( CookieJar, r#" SELECT - id, - model, - created_at, - updated_at, - workspace_id, - name, + id, model, created_at, updated_at, workspace_id, name, cookies AS "cookies!: sqlx::types::Json>" FROM cookie_jars WHERE id = ? "#, id, ) - .fetch_one(pool) - .await + .fetch_one(&db) + .await } -pub async fn find_cookie_jars(workspace_id: &str, pool: &Pool) -> Result, sqlx::Error> { +pub async fn list_cookie_jars( + app_handle: &AppHandle, + workspace_id: &str, +) -> Result, sqlx::Error> { + let db = get_db(app_handle).await; sqlx::query_as!( CookieJar, r#" SELECT - id, - model, - created_at, - updated_at, - workspace_id, - name, + id, model, created_at, updated_at, workspace_id, name, cookies AS "cookies!: sqlx::types::Json>" FROM cookie_jars WHERE workspace_id = ? "#, workspace_id, ) - .fetch_all(pool) - .await + .fetch_all(&db) + .await } -pub async fn delete_cookie_jar(id: &str, pool: &Pool) -> Result { - let cookie_jar = get_cookie_jar(id, pool).await?; +pub async fn delete_cookie_jar(app_handle: &AppHandle, id: &str) -> Result { + let cookie_jar = get_cookie_jar(app_handle, id).await?; + let db = get_db(app_handle).await; let _ = sqlx::query!( r#" @@ -421,14 +470,269 @@ pub async fn delete_cookie_jar(id: &str, pool: &Pool) -> Result Result { + let mut request = get_grpc_request(app_handle, id).await?.clone(); + request.id = "".to_string(); + upsert_grpc_request(app_handle, &request).await +} + +pub async fn upsert_grpc_request( + app_handle: &AppHandle, + request: &GrpcRequest, +) -> Result { + let db = get_db(app_handle).await; + let id = match request.id.as_str() { + "" => generate_id(Some("gr")), + _ => request.id.to_string(), + }; + let trimmed_name = request.name.trim(); + sqlx::query!( + r#" + INSERT INTO grpc_requests ( + id, name, workspace_id, folder_id, sort_priority, url, service, method, message, + proto_files + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (id) DO UPDATE SET + updated_at = CURRENT_TIMESTAMP, + name = excluded.name, + folder_id = excluded.folder_id, + sort_priority = excluded.sort_priority, + url = excluded.url, + service = excluded.service, + method = excluded.method, + message = excluded.message, + proto_files = excluded.proto_files + "#, + id, + trimmed_name, + request.workspace_id, + request.folder_id, + request.sort_priority, + request.url, + request.service, + request.method, + request.message, + request.proto_files, + ) + .execute(&db) + .await?; + + match get_grpc_request(app_handle, &id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } +} + +pub async fn get_grpc_request( + app_handle: &AppHandle, + id: &str, +) -> Result { + let db = get_db(app_handle).await; + sqlx::query_as!( + GrpcRequest, + r#" + SELECT + id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority, + url, service, method, message, + proto_files AS "proto_files!: sqlx::types::Json>" + FROM grpc_requests + WHERE id = ? + "#, + id, + ) + .fetch_one(&db) + .await +} + +pub async fn list_grpc_requests( + app_handle: &AppHandle, + workspace_id: &str, +) -> Result, sqlx::Error> { + let db = get_db(app_handle).await; + sqlx::query_as!( + GrpcRequest, + r#" + SELECT + id, model, workspace_id, folder_id, created_at, updated_at, name, sort_priority, + url, service, method, message, + proto_files AS "proto_files!: sqlx::types::Json>" + FROM grpc_requests + WHERE workspace_id = ? + "#, + workspace_id, + ) + .fetch_all(&db) + .await +} + +pub async fn upsert_grpc_connection( + app_handle: &AppHandle, + connection: &GrpcConnection, +) -> Result { + let db = get_db(&app_handle).await; + let id = match connection.id.as_str() { + "" => generate_id(Some("gc")), + _ => connection.id.to_string(), + }; + sqlx::query!( + r#" + INSERT INTO grpc_connections ( + id, workspace_id, request_id, service, method, elapsed + ) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT (id) DO UPDATE SET + updated_at = CURRENT_TIMESTAMP, + service = excluded.service, + method = excluded.method, + elapsed = excluded.elapsed + "#, + id, + connection.workspace_id, + connection.request_id, + connection.service, + connection.method, + connection.elapsed, + ) + .execute(&db) + .await?; + + match get_grpc_connection(app_handle, &id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } +} + +pub async fn get_grpc_connection( + app_handle: &AppHandle, + id: &str, +) -> Result { + let db = get_db(&app_handle).await; + sqlx::query_as!( + GrpcConnection, + r#" + SELECT + id, model, workspace_id, request_id, created_at, updated_at, service, + method, elapsed + FROM grpc_connections + WHERE id = ? + "#, + id, + ) + .fetch_one(&db) + .await +} + +pub async fn list_grpc_connections( + app_handle: &AppHandle, + request_id: &str, +) -> Result, sqlx::Error> { + let db = get_db(app_handle).await; + sqlx::query_as!( + GrpcConnection, + r#" + SELECT + id, model, workspace_id, request_id, created_at, updated_at, service, + method, elapsed + FROM grpc_connections + WHERE request_id = ? + ORDER BY created_at DESC + "#, + request_id, + ) + .fetch_all(&db) + .await +} + +pub async fn upsert_grpc_message( + app_handle: &AppHandle, + message: &GrpcMessage, +) -> Result { + let db = get_db(app_handle).await; + let id = match message.id.as_str() { + "" => generate_id(Some("gm")), + _ => message.id.to_string(), + }; + sqlx::query!( + r#" + INSERT INTO grpc_messages ( + id, workspace_id, request_id, connection_id, message, is_server, is_info + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (id) DO UPDATE SET + updated_at = CURRENT_TIMESTAMP, + message = excluded.message, + is_server = excluded.is_server, + is_info = excluded.is_info + "#, + id, + message.workspace_id, + message.request_id, + message.connection_id, + message.message, + message.is_server, + message.is_info, + ) + .execute(&db) + .await?; + + match get_grpc_message(app_handle, &id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } +} + +pub async fn get_grpc_message( + app_handle: &AppHandle, + id: &str, +) -> Result { + let db = get_db(app_handle).await; + sqlx::query_as!( + GrpcMessage, + r#" + SELECT + id, model, workspace_id, request_id, connection_id, created_at, message, + is_server, is_info + FROM grpc_messages + WHERE id = ? + "#, + id, + ) + .fetch_one(&db) + .await +} + +pub async fn list_grpc_messages( + app_handle: &AppHandle, + connection_id: &str, +) -> Result, sqlx::Error> { + let db = get_db(app_handle).await; + sqlx::query_as!( + GrpcMessage, + r#" + SELECT + id, model, workspace_id, request_id, connection_id, created_at, message, + is_server, is_info + FROM grpc_messages + WHERE connection_id = ? + "#, + connection_id, + ) + .fetch_all(&db) + .await } pub async fn upsert_cookie_jar( - pool: &Pool, + app_handle: &AppHandle, cookie_jar: &CookieJar, ) -> Result { let id = match cookie_jar.id.as_str() { @@ -436,13 +740,12 @@ pub async fn upsert_cookie_jar( _ => cookie_jar.id.to_string(), }; let trimmed_name = cookie_jar.name.trim(); + + let db = get_db(app_handle).await; sqlx::query!( r#" INSERT INTO cookie_jars ( - id, - workspace_id, - name, - cookies + id, workspace_id, name, cookies ) VALUES (?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET @@ -455,16 +758,20 @@ pub async fn upsert_cookie_jar( trimmed_name, cookie_jar.cookies, ) - .execute(pool) - .await?; + .execute(&db) + .await?; - get_cookie_jar(&id, pool).await + match get_cookie_jar(app_handle, &id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } -pub async fn find_environments( +pub async fn list_environments( + app_handle: &AppHandle, workspace_id: &str, - pool: &Pool, ) -> Result, sqlx::Error> { + let db = get_db(app_handle).await; sqlx::query_as!( Environment, r#" @@ -475,12 +782,16 @@ pub async fn find_environments( "#, workspace_id, ) - .fetch_all(pool) + .fetch_all(&db) .await } -pub async fn delete_environment(id: &str, pool: &Pool) -> Result { - let env = get_environment(id, pool).await?; +pub async fn delete_environment( + app_handle: &AppHandle, + id: &str, +) -> Result { + let db = get_db(app_handle).await; + let env = get_environment(app_handle, id).await?; let _ = sqlx::query!( r#" DELETE FROM environments @@ -488,71 +799,74 @@ pub async fn delete_environment(id: &str, pool: &Pool) -> Result) -> Result { +async fn get_settings(app_handle: &AppHandle) -> Result { + let db = get_db(app_handle).await; sqlx::query_as!( Settings, r#" SELECT - id, - model, - created_at, - updated_at, - theme, - appearance, - update_channel + id, model, created_at, updated_at, theme, appearance, update_channel FROM settings WHERE id = 'default' "#, ) - .fetch_one(pool) + .fetch_one(&db) .await } -pub async fn get_or_create_settings(pool: &Pool) -> Settings { - if let Ok(settings) = get_settings(pool).await { - settings - } else { - sqlx::query!( - r#" +pub async fn get_or_create_settings(app_handle: &AppHandle) -> Settings { + if let Ok(settings) = get_settings(app_handle).await { + return settings; + } + + let db = get_db(app_handle).await; + sqlx::query!( + r#" INSERT INTO settings (id) VALUES ('default') "#, - ) - .execute(pool) - .await.expect("Failed to insert settings"); - get_settings(pool).await.expect("Failed to get settings") - } + ) + .execute(&db) + .await + .expect("Failed to insert settings"); + + get_settings(&app_handle) + .await + .expect("Failed to get settings") } pub async fn update_settings( - pool: &Pool, + app_handle: &AppHandle, settings: Settings, ) -> Result { + let db = get_db(app_handle).await; sqlx::query!( r#" UPDATE settings SET ( - theme, - appearance, - update_channel + theme, appearance, update_channel ) = (?, ?, ?) WHERE id = 'default'; "#, settings.theme, settings.appearance, settings.update_channel ) - .execute(pool) + .execute(&db) .await?; - get_settings(pool).await + + match get_settings(app_handle).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } pub async fn upsert_environment( - pool: &Pool, + app_handle: &AppHandle, environment: Environment, ) -> Result { let id = match environment.id.as_str() { @@ -560,13 +874,11 @@ pub async fn upsert_environment( _ => environment.id.to_string(), }; let trimmed_name = environment.name.trim(); + let db = get_db(app_handle).await; sqlx::query!( r#" INSERT INTO environments ( - id, - workspace_id, - name, - variables + id, workspace_id, name, variables ) VALUES (?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET @@ -579,81 +891,70 @@ pub async fn upsert_environment( trimmed_name, environment.variables, ) - .execute(pool) + .execute(&db) .await?; - get_environment(&id, pool).await + + match get_environment(app_handle, &id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } -pub async fn get_environment(id: &str, pool: &Pool) -> Result { +pub async fn get_environment(app_handle: &AppHandle, id: &str) -> Result { + let db = get_db(app_handle).await; sqlx::query_as!( Environment, r#" SELECT - id, - model, - workspace_id, - created_at, - updated_at, - name, + id, model, workspace_id, created_at, updated_at, name, variables AS "variables!: sqlx::types::Json>" FROM environments WHERE id = ? "#, id, ) - .fetch_one(pool) + .fetch_one(&db) .await } -pub async fn get_folder(id: &str, pool: &Pool) -> Result { +pub async fn get_folder(app_handle: &AppHandle, id: &str) -> Result { + let db = get_db(app_handle).await; sqlx::query_as!( Folder, r#" SELECT - id, - model, - workspace_id, - created_at, - updated_at, - folder_id, - name, - sort_priority + id, model, workspace_id, created_at, updated_at, folder_id, name, sort_priority FROM folders WHERE id = ? "#, id, ) - .fetch_one(pool) + .fetch_one(&db) .await } -pub async fn find_folders( +pub async fn list_folders( + app_handle: &AppHandle, workspace_id: &str, - pool: &Pool, ) -> Result, sqlx::Error> { + let db = get_db(app_handle).await; sqlx::query_as!( Folder, r#" SELECT - id, - model, - workspace_id, - created_at, - updated_at, - folder_id, - name, - sort_priority + id, model, workspace_id, created_at, updated_at, folder_id, name, sort_priority FROM folders WHERE workspace_id = ? "#, workspace_id, ) - .fetch_all(pool) + .fetch_all(&db) .await } -pub async fn delete_folder(id: &str, pool: &Pool) -> Result { - let env = get_folder(id, pool).await?; +pub async fn delete_folder(app_handle: &AppHandle, id: &str) -> Result { + let folder = get_folder(app_handle, id).await?; + let db = get_db(app_handle).await; let _ = sqlx::query!( r#" DELETE FROM folders @@ -661,27 +962,24 @@ pub async fn delete_folder(id: &str, pool: &Pool) -> Result, r: Folder) -> Result { +pub async fn upsert_folder(app_handle: &AppHandle, r: Folder) -> Result { let id = match r.id.as_str() { "" => generate_id(Some("fl")), _ => r.id.to_string(), }; let trimmed_name = r.name.trim(); + let db = get_db(app_handle).await; sqlx::query!( r#" INSERT INTO folders ( - id, - workspace_id, - folder_id, - name, - sort_priority + id, workspace_id, folder_id, name, sort_priority ) VALUES (?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET @@ -696,20 +994,26 @@ pub async fn upsert_folder(pool: &Pool, r: Folder) -> Result Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } -pub async fn duplicate_request(id: &str, pool: &Pool) -> Result { - let mut request = get_request(id, pool).await?.clone(); +pub async fn duplicate_http_request( + app_handle: &AppHandle, + id: &str, +) -> Result { + let mut request = get_http_request(app_handle, id).await?.clone(); request.id = "".to_string(); - upsert_request(pool, request).await + upsert_http_request(app_handle, request).await } -pub async fn upsert_request( - pool: &Pool, +pub async fn upsert_http_request( + app_handle: &AppHandle, r: HttpRequest, ) -> Result { let id = match r.id.as_str() { @@ -720,22 +1024,13 @@ pub async fn upsert_request( let auth_json = Json(r.authentication); let trimmed_name = r.name.trim(); + let db = get_db(app_handle).await; + sqlx::query!( r#" INSERT INTO http_requests ( - id, - workspace_id, - folder_id, - name, - url, - url_parameters, - method, - body, - body_type, - authentication, - authentication_type, - headers, - sort_priority + id, workspace_id, folder_id, name, url, url_parameters, method, body, body_type, + authentication, authentication_type, headers, sort_priority ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET @@ -766,81 +1061,74 @@ pub async fn upsert_request( headers_json, r.sort_priority, ) - .execute(pool) + .execute(&db) .await?; - get_request(&id, pool).await + match get_http_request(app_handle, &id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } -pub async fn find_requests( +pub async fn list_requests( + app_handle: &AppHandle, workspace_id: &str, - pool: &Pool, ) -> Result, sqlx::Error> { + let db = get_db(app_handle).await; sqlx::query_as!( HttpRequest, r#" SELECT - id, - model, - workspace_id, - folder_id, - created_at, - updated_at, - name, - url, + id, model, workspace_id, folder_id, created_at, updated_at, name, url, url_parameters AS "url_parameters!: sqlx::types::Json>", - method, + method, body_type, authentication_type, sort_priority, body AS "body!: Json>", - body_type, authentication AS "authentication!: Json>", - authentication_type, - sort_priority, headers AS "headers!: sqlx::types::Json>" FROM http_requests WHERE workspace_id = ? "#, workspace_id, ) - .fetch_all(pool) + .fetch_all(&db) .await } -pub async fn get_request(id: &str, pool: &Pool) -> Result { +pub async fn get_http_request( + app_handle: &AppHandle, + id: &str, +) -> Result { + let db = get_db(app_handle).await; + sqlx::query_as!( HttpRequest, r#" SELECT - id, - model, - workspace_id, - folder_id, - created_at, - updated_at, - name, - url, + id, model, workspace_id, folder_id, created_at, updated_at, name, url, method, + body_type, authentication_type, sort_priority, url_parameters AS "url_parameters!: sqlx::types::Json>", - method, body AS "body!: Json>", - body_type, authentication AS "authentication!: Json>", - authentication_type, - sort_priority, headers AS "headers!: sqlx::types::Json>" FROM http_requests WHERE id = ? "#, id, ) - .fetch_one(pool) + .fetch_one(&db) .await } -pub async fn delete_request(id: &str, pool: &Pool) -> Result { - let req = get_request(id, pool).await?; +pub async fn delete_http_request( + app_handle: &AppHandle, + id: &str, +) -> Result { + let req = get_http_request(app_handle, id).await?; // DB deletes will cascade but this will delete the files - delete_all_responses(id, pool).await?; + delete_all_http_responses(app_handle, id).await?; + let db = get_db(app_handle).await; let _ = sqlx::query!( r#" DELETE FROM http_requests @@ -848,14 +1136,15 @@ pub async fn delete_request(id: &str, pool: &Pool) -> Result, version: Option<&str>, remote_addr: Option<&str>, - pool: &Pool, ) -> Result { - let req = get_request(request_id, pool).await?; + let req = get_http_request(app_handle, request_id).await?; let id = generate_id(Some("rp")); let headers_json = Json(headers); + let db = get_db(app_handle).await; sqlx::query!( r#" INSERT INTO http_responses ( - id, - request_id, - workspace_id, - elapsed, - elapsed_headers, - url, - status, - status_reason, - content_length, - body_path, - headers, - version, - remote_addr + id, request_id, workspace_id, elapsed, elapsed_headers, url, status, status_reason, + content_length, body_path, headers, version, remote_addr ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); "#, @@ -905,13 +1183,28 @@ pub async fn create_response( version, remote_addr, ) - .execute(pool) + .execute(&db) .await?; - get_response(&id, pool).await + get_http_response(app_handle, &id).await } -pub async fn cancel_pending_responses(pool: &Pool) -> Result<(), sqlx::Error> { +pub async fn cancel_pending_grpc_connections(app_handle: &AppHandle) -> Result<(), sqlx::Error> { + let db = get_db(app_handle).await; + sqlx::query!( + r#" + UPDATE grpc_connections + SET (elapsed) = (-1) + WHERE elapsed = 0; + "#, + ) + .execute(&db) + .await?; + Ok(()) +} + +pub async fn cancel_pending_responses(app_handle: &AppHandle) -> Result<(), sqlx::Error> { + let db = get_db(app_handle).await; sqlx::query!( r#" UPDATE http_responses @@ -919,24 +1212,24 @@ pub async fn cancel_pending_responses(pool: &Pool) -> Result<(), sqlx::E WHERE elapsed = 0; "#, ) - .execute(pool) + .execute(&db) .await?; Ok(()) } pub async fn update_response_if_id( + app_handle: &AppHandle, response: &HttpResponse, - pool: &Pool, ) -> Result { if response.id.is_empty() { Ok(response.clone()) } else { - update_response(response, pool).await + update_response(app_handle, response).await } } pub async fn upsert_workspace( - pool: &Pool, + app_handle: &AppHandle, workspace: Workspace, ) -> Result { let id = match workspace.id.as_str() { @@ -944,16 +1237,13 @@ pub async fn upsert_workspace( _ => workspace.id.to_string(), }; let trimmed_name = workspace.name.trim(); + + let db = get_db(app_handle).await; sqlx::query!( r#" INSERT INTO workspaces ( - id, - name, - description, - variables, - setting_request_timeout, - setting_follow_redirects, - setting_validate_certificates + id, name, description, variables, setting_request_timeout, + setting_follow_redirects, setting_validate_certificates ) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET @@ -973,32 +1263,26 @@ pub async fn upsert_workspace( workspace.setting_follow_redirects, workspace.setting_validate_certificates, ) - .execute(pool) + .execute(&db) .await?; - get_workspace(&id, pool).await + match get_workspace(app_handle, &id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } pub async fn update_response( + app_handle: &AppHandle, response: &HttpResponse, - pool: &Pool, ) -> Result { let headers_json = Json(&response.headers); + let db = get_db(app_handle).await; sqlx::query!( r#" UPDATE http_responses SET ( - elapsed, - elapsed_headers, - url, - status, - status_reason, - content_length, - body_path, - error, - headers, - version, - remote_addr, - updated_at + elapsed, elapsed_headers, url, status, status_reason, content_length, body_path, + error, headers, version, remote_addr, updated_at ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?; "#, response.elapsed, @@ -1014,12 +1298,20 @@ pub async fn update_response( response.remote_addr, response.id, ) - .execute(pool) + .execute(&db) .await?; - get_response(&response.id, pool).await + + match get_http_response(app_handle, &response.id).await { + Ok(m) => Ok(emit_upserted_model(app_handle, m)), + Err(e) => Err(e), + } } -pub async fn get_response(id: &str, pool: &Pool) -> Result { +pub async fn get_http_response( + app_handle: &AppHandle, + id: &str, +) -> Result { + let db = get_db(app_handle).await; sqlx::query_as!( HttpResponse, r#" @@ -1033,16 +1325,17 @@ pub async fn get_response(id: &str, pool: &Pool) -> Result, - pool: &Pool, ) -> Result, sqlx::Error> { let limit_unwrapped = limit.unwrap_or_else(|| i64::MAX); + let db = get_db(app_handle).await; sqlx::query_as!( HttpResponse, r#" @@ -1059,14 +1352,15 @@ pub async fn find_responses( request_id, limit_unwrapped, ) - .fetch_all(pool) + .fetch_all(&db) .await } -pub async fn find_responses_by_workspace_id( +pub async fn list_responses_by_workspace_id( + app_handle: &AppHandle, workspace_id: &str, - pool: &Pool, ) -> Result, sqlx::Error> { + let db = get_db(app_handle).await; sqlx::query_as!( HttpResponse, r#" @@ -1081,12 +1375,55 @@ pub async fn find_responses_by_workspace_id( "#, workspace_id, ) - .fetch_all(pool) + .fetch_all(&db) .await } -pub async fn delete_response(id: &str, pool: &Pool) -> Result { - let resp = get_response(id, pool).await?; +pub async fn delete_grpc_request( + app_handle: &AppHandle, + id: &str, +) -> Result { + let req = get_grpc_request(app_handle, id).await?; + + let db = get_db(app_handle).await; + let _ = sqlx::query!( + r#" + DELETE FROM grpc_requests + WHERE id = ? + "#, + id, + ) + .execute(&db) + .await; + + emit_deleted_model(app_handle, req) +} + +pub async fn delete_grpc_connection( + app_handle: &AppHandle, + id: &str, +) -> Result { + let resp = get_grpc_connection(app_handle, id).await?; + + let db = get_db(app_handle).await; + let _ = sqlx::query!( + r#" + DELETE FROM grpc_connections + WHERE id = ? + "#, + id, + ) + .execute(&db) + .await; + + emit_deleted_model(app_handle, resp) +} + +pub async fn delete_http_response( + app_handle: &AppHandle, + id: &str, +) -> Result { + let resp = get_http_response(app_handle, id).await?; // Delete the body file if it exists if let Some(p) = resp.body_path.clone() { @@ -1095,6 +1432,7 @@ pub async fn delete_response(id: &str, pool: &Pool) -> Result) -> Result, ) -> Result<(), sqlx::Error> { - for r in find_responses(request_id, None, pool).await? { - delete_response(&r.id, pool).await?; + for r in list_grpc_connections(app_handle, request_id).await? { + delete_grpc_connection(app_handle, &r.id).await?; + } + Ok(()) +} + +pub async fn delete_all_http_responses( + app_handle: &AppHandle, + request_id: &str, +) -> Result<(), sqlx::Error> { + for r in list_responses(app_handle, request_id, None).await? { + delete_http_response(app_handle, &r.id).await?; } Ok(()) } @@ -1146,10 +1494,9 @@ pub struct WorkspaceExportResources { pub async fn get_workspace_export_resources( app_handle: &AppHandle, - pool: &Pool, workspace_id: &str, ) -> WorkspaceExport { - let workspace = get_workspace(workspace_id, pool) + let workspace = get_workspace(app_handle, workspace_id) .await .expect("Failed to get workspace"); return WorkspaceExport { @@ -1158,15 +1505,33 @@ pub async fn get_workspace_export_resources( timestamp: chrono::Utc::now().naive_utc(), resources: WorkspaceExportResources { workspaces: vec![workspace], - environments: find_environments(workspace_id, pool) + environments: list_environments(app_handle, workspace_id) .await .expect("Failed to get environments"), - folders: find_folders(workspace_id, pool) + folders: list_folders(app_handle, workspace_id) .await .expect("Failed to get folders"), - requests: find_requests(workspace_id, pool) + requests: list_requests(app_handle, workspace_id) .await .expect("Failed to get requests"), }, }; } + +fn emit_upserted_model(app_handle: &AppHandle, model: S) -> S { + app_handle + .emit_all("upserted_model", model.clone()) + .unwrap(); + model +} + +fn emit_deleted_model(app_handle: &AppHandle, model: S) -> Result { + app_handle.emit_all("deleted_model", model.clone()).unwrap(); + Ok(model) +} + +async fn get_db(app_handle: &AppHandle) -> Pool { + let db_state = app_handle.state::>>(); + let db = &*db_state.lock().await; + db.clone() +} diff --git a/src-tauri/src/updates.rs b/src-tauri/src/updates.rs index 2e65321f..074d973b 100644 --- a/src-tauri/src/updates.rs +++ b/src-tauri/src/updates.rs @@ -1,8 +1,8 @@ use std::time::SystemTime; use log::info; -use tauri::{AppHandle, updater, Window, Wry}; use tauri::api::dialog; +use tauri::{updater, AppHandle, Window}; use crate::is_dev; @@ -27,14 +27,17 @@ impl YaakUpdater { } pub async fn force_check( &mut self, - app_handle: &AppHandle, + app_handle: &AppHandle, mode: UpdateMode, ) -> Result { self.last_update_check = SystemTime::now(); let update_mode = get_update_mode_str(mode); let enabled = !is_dev(); - info!("Checking for updates mode={} enabled={}", update_mode, enabled); + info!( + "Checking for updates mode={} enabled={}", + update_mode, enabled + ); if !enabled { return Ok(false); @@ -89,10 +92,11 @@ impl YaakUpdater { } pub async fn check( &mut self, - app_handle: &AppHandle, + app_handle: &AppHandle, mode: UpdateMode, ) -> Result { - let ignore_check = self.last_update_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS; + let ignore_check = + self.last_update_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS; if ignore_check { return Ok(false); } diff --git a/src-web/components/AppRouter.tsx b/src-web/components/AppRouter.tsx index f3d61137..c3e3725c 100644 --- a/src-web/components/AppRouter.tsx +++ b/src-web/components/AppRouter.tsx @@ -1,7 +1,7 @@ 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 { useHttpRequests } from '../hooks/useHttpRequests'; import { GlobalHooks } from './GlobalHooks'; import Workspace from './Workspace'; import Workspaces from './Workspaces'; @@ -49,7 +49,7 @@ export function AppRouter() { function WorkspaceOrRedirect() { const recentRequests = useRecentRequests(); const activeEnvironmentId = useActiveEnvironmentId(); - const requests = useRequests(); + const requests = useHttpRequests(); const request = requests.find((r) => r.id === recentRequests[0]); const routes = useAppRoutes(); diff --git a/src-web/components/BasicAuth.tsx b/src-web/components/BasicAuth.tsx index ed1bd61c..459a7b97 100644 --- a/src-web/components/BasicAuth.tsx +++ b/src-web/components/BasicAuth.tsx @@ -1,4 +1,4 @@ -import { useUpdateRequest } from '../hooks/useUpdateRequest'; +import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest'; import type { HttpRequest } from '../lib/models'; import { Input } from './core/Input'; import { VStack } from './core/Stacks'; @@ -9,7 +9,7 @@ interface Props { } export function BasicAuth({ requestId, authentication }: Props) { - const updateRequest = useUpdateRequest(requestId); + const updateRequest = useUpdateHttpRequest(requestId); return ( diff --git a/src-web/components/BearerAuth.tsx b/src-web/components/BearerAuth.tsx index 1a63f049..db149670 100644 --- a/src-web/components/BearerAuth.tsx +++ b/src-web/components/BearerAuth.tsx @@ -1,4 +1,4 @@ -import { useUpdateRequest } from '../hooks/useUpdateRequest'; +import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest'; import type { HttpRequest } from '../lib/models'; import { Input } from './core/Input'; import { VStack } from './core/Stacks'; @@ -9,7 +9,7 @@ interface Props { } export function BearerAuth({ requestId, authentication }: Props) { - const updateRequest = useUpdateRequest(requestId); + const updateRequest = useUpdateHttpRequest(requestId); return ( diff --git a/src-web/components/GlobalHooks.tsx b/src-web/components/GlobalHooks.tsx index aa60a419..6b061a6a 100644 --- a/src-web/components/GlobalHooks.tsx +++ b/src-web/components/GlobalHooks.tsx @@ -3,20 +3,23 @@ import { appWindow } from '@tauri-apps/api/window'; import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { cookieJarsQueryKey } from '../hooks/useCookieJars'; +import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections'; +import { grpcMessagesQueryKey } from '../hooks/useGrpcMessages'; +import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests'; +import { httpRequestsQueryKey } from '../hooks/useHttpRequests'; +import { httpResponsesQueryKey } from '../hooks/useHttpResponses'; 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 { settingsQueryKey } from '../hooks/useSettings'; -import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle'; import { useSyncAppearance } from '../hooks/useSyncAppearance'; +import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle'; import { workspacesQueryKey } from '../hooks/useWorkspaces'; import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore'; -import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models'; +import type { Model } from '../lib/models'; import { modelsEq } from '../lib/models'; import { setPathname } from '../lib/persistPathname'; @@ -42,43 +45,20 @@ export function GlobalHooks() { setPathname(location.pathname).catch(console.error); }, [location.pathname]); - useListenToTauriEvent('created_model', ({ payload, windowLabel }) => { + useListenToTauriEvent('upserted_model', ({ payload, windowLabel }) => { if (shouldIgnoreEvent(payload, windowLabel)) return; const queryKey = payload.model === 'http_request' - ? requestsQueryKey(payload) + ? httpRequestsQueryKey(payload) : payload.model === 'http_response' - ? responsesQueryKey(payload) - : payload.model === 'workspace' - ? workspacesQueryKey(payload) - : payload.model === 'key_value' - ? keyValueQueryKey(payload) - : payload.model === 'settings' - ? settingsQueryKey() - : payload.model === 'cookie_jar' - ? cookieJarsQueryKey(payload) - : null; - - if (queryKey === null) { - console.log('Unrecognized created model:', payload); - return; - } - - if (!shouldIgnoreModel(payload)) { - // Order newest first - queryClient.setQueryData(queryKey, (values) => [payload, ...(values ?? [])]); - } - }); - - useListenToTauriEvent('updated_model', ({ payload, windowLabel }) => { - if (shouldIgnoreEvent(payload, windowLabel)) return; - - const queryKey = - payload.model === 'http_request' - ? requestsQueryKey(payload) - : payload.model === 'http_response' - ? responsesQueryKey(payload) + ? httpResponsesQueryKey(payload) + : payload.model === 'grpc_connection' + ? grpcConnectionsQueryKey(payload) + : payload.model === 'grpc_message' + ? grpcMessagesQueryKey(payload) + : payload.model === 'grpc_request' + ? grpcRequestsQueryKey(payload) : payload.model === 'workspace' ? workspacesQueryKey(payload) : payload.model === 'key_value' @@ -98,12 +78,19 @@ export function GlobalHooks() { wasUpdatedExternally(payload.id); } + const pushToFront = (['http_response', 'grpc_connection'] as Model['model'][]).includes( + payload.model, + ); + if (!shouldIgnoreModel(payload)) { - console.time('set query date'); - queryClient.setQueryData(queryKey, (values) => - values?.map((v) => (modelsEq(v, payload) ? payload : v)), - ); - console.timeEnd('set query date'); + queryClient.setQueryData(queryKey, (values = []) => { + const index = values.findIndex((v) => modelsEq(v, payload)) ?? -1; + if (index >= 0) { + return [...values.slice(0, index), payload, ...values.slice(index + 1)]; + } else { + return pushToFront ? [payload, ...(values ?? [])] : [...(values ?? []), payload]; + } + }); } }); @@ -113,11 +100,17 @@ export function GlobalHooks() { if (shouldIgnoreModel(payload)) return; if (payload.model === 'workspace') { - queryClient.setQueryData(workspacesQueryKey(), removeById(payload)); + queryClient.setQueryData(workspacesQueryKey(), removeById(payload)); } else if (payload.model === 'http_request') { - queryClient.setQueryData(requestsQueryKey(payload), removeById(payload)); + queryClient.setQueryData(httpRequestsQueryKey(payload), removeById(payload)); } else if (payload.model === 'http_response') { - queryClient.setQueryData(responsesQueryKey(payload), removeById(payload)); + queryClient.setQueryData(httpResponsesQueryKey(payload), removeById(payload)); + } else if (payload.model === 'grpc_request') { + queryClient.setQueryData(grpcRequestsQueryKey(payload), removeById(payload)); + } else if (payload.model === 'grpc_connection') { + queryClient.setQueryData(grpcConnectionsQueryKey(payload), removeById(payload)); + } else if (payload.model === 'grpc_message') { + queryClient.setQueryData(grpcMessagesQueryKey(payload), removeById(payload)); } else if (payload.model === 'key_value') { queryClient.setQueryData(keyValueQueryKey(payload), undefined); } else if (payload.model === 'cookie_jar') { diff --git a/src-web/components/GraphQLEditor.tsx b/src-web/components/GraphQLEditor.tsx index 31e89bb8..2b08f6cd 100644 --- a/src-web/components/GraphQLEditor.tsx +++ b/src-web/components/GraphQLEditor.tsx @@ -72,7 +72,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi const dialog = useDialog(); return ( -
+
- -

Variables

- +
+ + Variables + + +
); } diff --git a/src-web/components/GrpcConnectionLayout.tsx b/src-web/components/GrpcConnectionLayout.tsx new file mode 100644 index 00000000..7d0aeaf6 --- /dev/null +++ b/src-web/components/GrpcConnectionLayout.tsx @@ -0,0 +1,119 @@ +import classNames from 'classnames'; +import type { CSSProperties } from 'react'; +import React, { useEffect, useMemo } from 'react'; +import { useActiveRequest } from '../hooks/useActiveRequest'; +import { useGrpc } from '../hooks/useGrpc'; +import { useGrpcConnections } from '../hooks/useGrpcConnections'; +import { useGrpcMessages } from '../hooks/useGrpcMessages'; +import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; +import { Banner } from './core/Banner'; +import { HotKeyList } from './core/HotKeyList'; +import { SplitLayout } from './core/SplitLayout'; +import { GrpcConnectionMessagesPane } from './GrpcConnectionMessagesPane'; +import { GrpcConnectionSetupPane } from './GrpcConnectionSetupPane'; + +interface Props { + style: CSSProperties; +} + +export function GrpcConnectionLayout({ style }: Props) { + const activeRequest = useActiveRequest('grpc_request'); + const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null); + const connections = useGrpcConnections(activeRequest?.id ?? null); + const activeConnection = connections[0] ?? null; + const messages = useGrpcMessages(activeConnection?.id ?? null); + const grpc = useGrpc(activeRequest, activeConnection); + + const services = grpc.reflect.data ?? null; + useEffect(() => { + if (services == null || activeRequest == null) return; + const s = services.find((s) => s.name === activeRequest.service); + if (s == null) { + updateRequest.mutate({ + service: services[0]?.name ?? null, + method: services[0]?.methods[0]?.name ?? null, + }); + return; + } + + const m = s.methods.find((m) => m.name === activeRequest.method); + if (m == null) { + updateRequest.mutate({ method: s.methods[0]?.name ?? null }); + return; + } + }, [activeRequest, services, updateRequest]); + + const activeMethod = useMemo(() => { + if (services == null || activeRequest == null) return null; + + const s = services.find((s) => s.name === activeRequest.service); + if (s == null) return null; + return s.methods.find((m) => m.name === activeRequest.method); + }, [activeRequest, services]); + + const methodType: + | 'unary' + | 'server_streaming' + | 'client_streaming' + | 'streaming' + | 'no-schema' + | 'no-method' = useMemo(() => { + if (services == null) return 'no-schema'; + if (activeMethod == null) return 'no-method'; + if (activeMethod.clientStreaming && activeMethod.serverStreaming) return 'streaming'; + if (activeMethod.clientStreaming) return 'client_streaming'; + if (activeMethod.serverStreaming) return 'server_streaming'; + return 'unary'; + }, [activeMethod, services]); + + if (activeRequest == null) { + return null; + } + + return ( + ( + + )} + secondSlot={({ style }) => + !grpc.unary.isLoading && ( +
+ {grpc.unary.error ? ( + + {grpc.unary.error} + + ) : messages.length >= 0 ? ( + + ) : ( + + )} +
+ ) + } + /> + ); +} diff --git a/src-web/components/GrpcConnectionMessagesPane.tsx b/src-web/components/GrpcConnectionMessagesPane.tsx new file mode 100644 index 00000000..3efc19a0 --- /dev/null +++ b/src-web/components/GrpcConnectionMessagesPane.tsx @@ -0,0 +1,127 @@ +import classNames from 'classnames'; +import { format } from 'date-fns'; +import type { CSSProperties } from 'react'; +import React, { useMemo, useState } from 'react'; +import { useGrpcConnections } from '../hooks/useGrpcConnections'; +import { useGrpcMessages } from '../hooks/useGrpcMessages'; +import type { GrpcRequest } from '../lib/models'; +import { Icon } from './core/Icon'; +import { JsonAttributeTree } from './core/JsonAttributeTree'; +import { Separator } from './core/Separator'; +import { SplitLayout } from './core/SplitLayout'; +import { HStack } from './core/Stacks'; +import { RecentConnectionsDropdown } from './RecentConnectionsDropdown'; + +interface Props { + style?: CSSProperties; + className?: string; + activeRequest: GrpcRequest; + methodType: + | 'unary' + | 'client_streaming' + | 'server_streaming' + | 'streaming' + | 'no-schema' + | 'no-method'; +} + +export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }: Props) { + const [activeMessageId, setActiveMessageId] = useState(null); + const connections = useGrpcConnections(activeRequest.id ?? null); + const activeConnection = connections[0] ?? null; + const messages = useGrpcMessages(activeConnection?.id ?? null); + + const activeMessage = useMemo( + () => messages.find((m) => m.id === activeMessageId) ?? null, + [activeMessageId, messages], + ); + + return ( + ( +
+ + + {messages.filter((m) => !m.isInfo).length} messages + {activeConnection?.elapsed === 0 && ( + + )} + + {activeConnection && ( + { + // todo + }} + /> + )} + +
+ {...messages.map((m) => ( + { + if (m.id === activeMessageId) setActiveMessageId(null); + else setActiveMessageId(m.id); + }} + alignItems="center" + className={classNames( + 'px-2 py-1 font-mono cursor-default group', + m === activeMessage && '!bg-highlight', + )} + > + +
+ {m.message} +
+
+ {format(m.createdAt, 'HH:mm:ss')} +
+
+ ))} +
+
+ )} + secondSlot={ + activeMessage && + (() => ( +
+
+ +
+
+ {activeMessage.isInfo ? ( + {activeMessage.message} + ) : ( + + )} +
+
+ )) + } + /> + ); +} diff --git a/src-web/components/GrpcConnectionSetupPane.tsx b/src-web/components/GrpcConnectionSetupPane.tsx new file mode 100644 index 00000000..82eeb19b --- /dev/null +++ b/src-web/components/GrpcConnectionSetupPane.tsx @@ -0,0 +1,235 @@ +import useResizeObserver from '@react-hook/resize-observer'; +import classNames from 'classnames'; +import type { CSSProperties, FormEvent } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import type { ReflectResponseService } from '../hooks/useGrpc'; +import { useGrpcConnections } from '../hooks/useGrpcConnections'; +import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; +import type { GrpcRequest } from '../lib/models'; +import { Button } from './core/Button'; +import { Icon } from './core/Icon'; +import { IconButton } from './core/IconButton'; +import { RadioDropdown } from './core/RadioDropdown'; +import { HStack, VStack } from './core/Stacks'; +import { GrpcEditor } from './GrpcEditor'; +import { UrlBar } from './UrlBar'; + +interface Props { + style?: CSSProperties; + className?: string; + activeRequest: GrpcRequest; + reflectionError?: string; + reflectionLoading?: boolean; + methodType: + | 'unary' + | 'client_streaming' + | 'server_streaming' + | 'streaming' + | 'no-schema' + | 'no-method'; + onUnary: () => void; + onCommit: () => void; + onCancel: () => void; + onSend: (v: { message: string }) => void; + onClientStreaming: () => void; + onServerStreaming: () => void; + onStreaming: () => void; + services: ReflectResponseService[] | null; +} + +export function GrpcConnectionSetupPane({ + style, + services, + methodType, + activeRequest, + reflectionError, + reflectionLoading, + onStreaming, + onClientStreaming, + onServerStreaming, + onCommit, + onCancel, + onSend, + onUnary, +}: Props) { + const connections = useGrpcConnections(activeRequest.id ?? null); + const updateRequest = useUpdateGrpcRequest(activeRequest?.id ?? null); + const activeConnection = connections[0] ?? null; + const isStreaming = activeConnection?.elapsed === 0; + + const [paneSize, setPaneSize] = useState(99999); + const urlContainerEl = useRef(null); + useResizeObserver(urlContainerEl.current, (entry) => { + setPaneSize(entry.contentRect.width); + }); + + const handleChangeUrl = useCallback( + (url: string) => updateRequest.mutateAsync({ url }), + [updateRequest], + ); + + const handleChangeMessage = useCallback( + (message: string) => updateRequest.mutateAsync({ message }), + [updateRequest], + ); + + const select = useMemo(() => { + const options = + services?.flatMap((s) => + s.methods.map((m) => ({ + label: `${s.name.split('.', 2).pop() ?? s.name}/${m.name}`, + value: `${s.name}/${m.name}`, + })), + ) ?? []; + const value = `${activeRequest?.service ?? ''}/${activeRequest?.method ?? ''}`; + return { value, options }; + }, [activeRequest?.method, activeRequest?.service, services]); + + const handleChangeService = useCallback( + async (v: string) => { + const [serviceName, methodName] = v.split('/', 2); + if (serviceName == null || methodName == null) throw new Error('Should never happen'); + await updateRequest.mutateAsync({ + service: serviceName, + method: methodName, + }); + }, + [updateRequest], + ); + + const handleConnect = useCallback( + async (e: FormEvent) => { + e.preventDefault(); + if (activeRequest == null) return; + + if (activeRequest.service == null || activeRequest.method == null) { + alert({ + id: 'grpc-invalid-service-method', + title: 'Error', + body: 'Service or method not selected', + }); + } + if (methodType === 'streaming') { + onStreaming(); + } else if (methodType === 'server_streaming') { + onServerStreaming(); + } else if (methodType === 'client_streaming') { + onClientStreaming(); + } else { + onUnary(); + } + }, + [activeRequest, methodType, onStreaming, onServerStreaming, onClientStreaming, onUnary], + ); + + return ( + +
+ + + ({ + label: o.label, + value: o.value, + type: 'default', + shortLabel: o.label, + }))} + extraItems={[ + { type: 'separator' }, + { + label: 'Refresh', + type: 'default', + key: 'custom', + leftSlot: , + }, + ]} + > + + + {!isStreaming && ( + + )} + {isStreaming && ( + + )} + {methodType === 'client_streaming' && isStreaming && ( + + )} + {(methodType === 'client_streaming' || methodType === 'streaming') && isStreaming && ( + onSend({ message: activeRequest.message ?? '' })} + icon="sendHorizontal" + /> + )} + +
+ +
+ ); +} diff --git a/src-web/components/GrpcEditor.tsx b/src-web/components/GrpcEditor.tsx new file mode 100644 index 00000000..12964bf5 --- /dev/null +++ b/src-web/components/GrpcEditor.tsx @@ -0,0 +1,153 @@ +import classNames from 'classnames'; +import type { EditorView } from 'codemirror'; +import { updateSchema } from 'codemirror-json-schema'; +import { useEffect, useRef } from 'react'; +import { useAlert } from '../hooks/useAlert'; +import type { ReflectResponseService } from '../hooks/useGrpc'; +import { tryFormatJson } from '../lib/formatters'; +import type { GrpcRequest } from '../lib/models'; +import { count } from '../lib/pluralize'; +import { Button } from './core/Button'; +import type { EditorProps } from './core/Editor'; +import { Editor } from './core/Editor'; +import { FormattedError } from './core/FormattedError'; +import { InlineCode } from './core/InlineCode'; +import { VStack } from './core/Stacks'; +import { useDialog } from './DialogContext'; +import { GrpcProtoSelection } from './GrpcProtoSelection'; + +type Props = Pick & { + services: ReflectResponseService[] | null; + reflectionError?: string; + reflectionLoading?: boolean; + request: GrpcRequest; +}; + +export function GrpcEditor({ + services, + reflectionError, + reflectionLoading, + request, + ...extraEditorProps +}: Props) { + const editorViewRef = useRef(null); + const alert = useAlert(); + const dialog = useDialog(); + + // Find the schema for the selected service and method and update the editor + useEffect(() => { + if (editorViewRef.current == null || services === null) return; + + const s = services.find((s) => s.name === request.service); + if (request.service != null && s == null) { + alert({ + id: 'grpc-find-service-error', + title: "Couldn't Find Service", + body: ( + <> + Failed to find service {request.service} in schema + + ), + }); + return; + } + + const schema = s?.methods.find((m) => m.name === request.method)?.schema; + if (request.method != null && schema == null) { + alert({ + id: 'grpc-find-schema-error', + title: "Couldn't Find Method", + body: ( + <> + Failed to find method {request.method} for{' '} + {request.service} in schema + + ), + }); + return; + } + + if (schema == null) { + return; + } + + try { + updateSchema(editorViewRef.current, JSON.parse(schema)); + } catch (err) { + alert({ + id: 'grpc-parse-schema-error', + title: 'Failed to Parse Schema', + body: ( + +

+ For service {request.service} and method{' '} + {request.method} +

+ {String(err)} +
+ ), + }); + } + }, [alert, services, request.method, request.service]); + + const reflectionUnavailable = reflectionError?.match(/unimplemented/i); + reflectionError = reflectionUnavailable ? undefined : reflectionError; + + return ( +
+ + +
, + ]} + {...extraEditorProps} + /> +
+ ); +} diff --git a/src-web/components/GrpcProtoSelection.tsx b/src-web/components/GrpcProtoSelection.tsx new file mode 100644 index 00000000..e6c8dba9 --- /dev/null +++ b/src-web/components/GrpcProtoSelection.tsx @@ -0,0 +1,148 @@ +import { open } from '@tauri-apps/api/dialog'; +import { useGrpc } from '../hooks/useGrpc'; +import { useGrpcRequest } from '../hooks/useGrpcRequest'; +import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest'; +import { count } from '../lib/pluralize'; +import { Banner } from './core/Banner'; +import { Button } from './core/Button'; +import { FormattedError } from './core/FormattedError'; +import { IconButton } from './core/IconButton'; +import { InlineCode } from './core/InlineCode'; +import { Link } from './core/Link'; +import { HStack, VStack } from './core/Stacks'; + +interface Props { + requestId: string; + onDone: () => void; +} + +export function GrpcProtoSelection({ requestId }: Props) { + const request = useGrpcRequest(requestId); + const grpc = useGrpc(request, null); + const updateRequest = useUpdateGrpcRequest(request?.id ?? null); + const services = grpc.reflect.data; + const serverReflection = request?.protoFiles.length === 0 && services != null; + let reflectError = grpc.reflect.error ?? null; + const reflectionUnimplemented = `${reflectError}`.match(/unimplemented/i); + + if (reflectionUnimplemented) { + reflectError = null; + } + + if (request == null) { + return null; + } + + return ( + + {/* Buttons on top so they get focus first */} + + + + + + {!serverReflection && services != null && services.length > 0 && ( + +

+ Found services + {services?.slice(0, 5).map((s, i) => { + return ( + + {s.name} + {i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '} + + ); + })} + {services?.length > 5 && count('other', services?.length - 5)} +

+
+ )} + {serverReflection && services != null && services.length > 0 && ( + +

+ Server reflection found services + {services?.map((s, i) => { + return ( + + {s.name} + {i === services.length - 1 ? '' : i === services.length - 2 ? ' and ' : ', '} + + ); + })} + . You can override this schema by manually selecting *.proto{' '} + files. +

+
+ )} + + {request.protoFiles.length > 0 && ( + + + + + + + + + {request.protoFiles.map((f, i) => ( + + + + + ))} + +
+ *.proto Files +
{f.split('/').pop()} + { + await updateRequest.mutateAsync({ + protoFiles: request.protoFiles.filter((p) => p !== f), + }); + grpc.reflect.remove(); + }} + /> +
+ )} + {reflectError && {reflectError}} + {reflectionUnimplemented && request.protoFiles.length === 0 && ( + + {request.url} doesn't implement{' '} + + Server Reflection + {' '} + . Please manually add the .proto file to get started. + + )} +
+
+ ); +} diff --git a/src-web/components/HttpRequestLayout.tsx b/src-web/components/HttpRequestLayout.tsx new file mode 100644 index 00000000..50e68ad3 --- /dev/null +++ b/src-web/components/HttpRequestLayout.tsx @@ -0,0 +1,29 @@ +import type { CSSProperties } from 'react'; +import React from 'react'; +import type { HttpRequest } from '../lib/models'; +import { SplitLayout } from './core/SplitLayout'; +import { RequestPane } from './RequestPane'; +import { ResponsePane } from './ResponsePane'; + +interface Props { + activeRequest: HttpRequest; + style: CSSProperties; +} + +export function HttpRequestLayout({ activeRequest, style }: Props) { + return ( + ( + + )} + secondSlot={({ style }) => } + /> + ); +} diff --git a/src-web/components/RecentConnectionsDropdown.tsx b/src-web/components/RecentConnectionsDropdown.tsx new file mode 100644 index 00000000..0c4f8f5f --- /dev/null +++ b/src-web/components/RecentConnectionsDropdown.tsx @@ -0,0 +1,60 @@ +import { formatDistanceToNowStrict } from 'date-fns'; +import { useDeleteGrpcConnection } from '../hooks/useDeleteGrpcConnection'; +import { useDeleteGrpcConnections } from '../hooks/useDeleteGrpcConnections'; +import type { GrpcConnection } from '../lib/models'; +import { count, pluralize } from '../lib/pluralize'; +import { Dropdown } from './core/Dropdown'; +import { Icon } from './core/Icon'; +import { IconButton } from './core/IconButton'; +import { HStack } from './core/Stacks'; + +interface Props { + connections: GrpcConnection[]; + activeConnection: GrpcConnection; + onPinned: (r: GrpcConnection) => void; +} + +export function RecentConnectionsDropdown({ activeConnection, connections, onPinned }: Props) { + const deleteConnection = useDeleteGrpcConnection(activeConnection?.id ?? null); + const deleteAllConnections = useDeleteGrpcConnections(activeConnection?.requestId); + + return ( + ({ + key: c.id, + label: ( + + {formatDistanceToNowStrict(c.createdAt + 'Z')} ago •{' '} + {c.elapsed}ms + + ), + leftSlot: activeConnection?.id === c.id ? : , + onSelect: () => onPinned(c), + })), + ]} + > + + + ); +} diff --git a/src-web/components/RecentRequestsDropdown.tsx b/src-web/components/RecentRequestsDropdown.tsx index ec509949..9800a819 100644 --- a/src-web/components/RecentRequestsDropdown.tsx +++ b/src-web/components/RecentRequestsDropdown.tsx @@ -7,7 +7,7 @@ import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId'; import { useAppRoutes } from '../hooks/useAppRoutes'; import { useHotKey } from '../hooks/useHotKey'; import { useRecentRequests } from '../hooks/useRecentRequests'; -import { useRequests } from '../hooks/useRequests'; +import { useHttpRequests } from '../hooks/useHttpRequests'; import { fallbackRequestName } from '../lib/fallbackRequestName'; import type { ButtonProps } from './core/Button'; import { Button } from './core/Button'; @@ -19,7 +19,7 @@ export function RecentRequestsDropdown({ className }: Pick allRecentRequestIds.slice(1), [allRecentRequestIds]); diff --git a/src-web/components/RecentResponsesDropdown.tsx b/src-web/components/RecentResponsesDropdown.tsx index e2b2210c..f28340b8 100644 --- a/src-web/components/RecentResponsesDropdown.tsx +++ b/src-web/components/RecentResponsesDropdown.tsx @@ -1,5 +1,5 @@ -import { useDeleteResponse } from '../hooks/useDeleteResponse'; -import { useDeleteResponses } from '../hooks/useDeleteResponses'; +import { useDeleteHttpResponse } from '../hooks/useDeleteHttpResponse'; +import { useDeleteHttpResponses } from '../hooks/useDeleteHttpResponses'; import type { HttpResponse } from '../lib/models'; import { Dropdown } from './core/Dropdown'; import { pluralize } from '../lib/pluralize'; @@ -19,8 +19,8 @@ export const RecentResponsesDropdown = function ResponsePane({ responses, onPinnedResponse, }: Props) { - const deleteResponse = useDeleteResponse(activeResponse?.id ?? null); - const deleteAllResponses = useDeleteResponses(activeResponse?.requestId); + const deleteResponse = useDeleteHttpResponse(activeResponse?.id ?? null); + const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId); return ( ('body'); -export const RequestPane = memo(function RequestPane({ style, fullHeight, className }: Props) { - const activeRequest = useActiveRequest(); - const activeRequestId = activeRequest?.id ?? null; - const updateRequest = useUpdateRequest(activeRequestId); +export const RequestPane = memo(function RequestPane({ + style, + fullHeight, + className, + activeRequest, +}: Props) { + const activeRequestId = activeRequest.id; + const updateRequest = useUpdateHttpRequest(activeRequestId); const [activeTab, setActiveTab] = useActiveTab(); const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState(0); - const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest?.id ?? null); + const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null); const tabs: TabItem[] = useMemo( - () => - activeRequest === null - ? [] - : [ - { - value: 'body', - options: { - value: activeRequest.bodyType, - items: [ - { type: 'separator', label: 'Form Data' }, - { label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED }, - { label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART }, - { type: 'separator', label: 'Text Content' }, - { label: 'JSON', value: BODY_TYPE_JSON }, - { label: 'XML', value: BODY_TYPE_XML }, - { label: 'GraphQL', value: BODY_TYPE_GRAPHQL }, - { type: 'separator', label: 'Other' }, - { label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE }, - ], - onChange: async (bodyType) => { - const patch: Partial = { bodyType }; - if (bodyType === BODY_TYPE_NONE) { - patch.headers = activeRequest?.headers.filter( - (h) => h.name.toLowerCase() !== 'content-type', - ); - } else if ( - bodyType === BODY_TYPE_FORM_URLENCODED || - bodyType === BODY_TYPE_FORM_MULTIPART || - bodyType === BODY_TYPE_JSON || - bodyType === BODY_TYPE_XML - ) { - patch.method = 'POST'; - patch.headers = [ - ...(activeRequest?.headers.filter( - (h) => h.name.toLowerCase() !== 'content-type', - ) ?? []), - { - name: 'Content-Type', - value: bodyType, - enabled: true, - }, - ]; - } else if (bodyType == BODY_TYPE_GRAPHQL) { - patch.method = 'POST'; - patch.headers = [ - ...(activeRequest?.headers.filter( - (h) => h.name.toLowerCase() !== 'content-type', - ) ?? []), - { - name: 'Content-Type', - value: 'application/json', - enabled: true, - }, - ]; - } - - // Force update header editor so any changed headers are reflected - setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100); - - updateRequest.mutate(patch); - }, - }, - }, - { - value: 'params', - label: ( -
- Params - p.name).length} /> -
- ), - }, - { - value: 'headers', - label: ( -
- Headers - h.name).length} /> -
- ), - }, - { - value: 'auth', - label: 'Auth', - options: { - value: activeRequest.authenticationType, - items: [ - { label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC }, - { label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER }, - { type: 'separator' }, - { label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE }, - ], - onChange: async (authenticationType) => { - let authentication: HttpRequest['authentication'] = activeRequest?.authentication; - if (authenticationType === AUTH_TYPE_BASIC) { - authentication = { - username: authentication.username ?? '', - password: authentication.password ?? '', - }; - } else if (authenticationType === AUTH_TYPE_BEARER) { - authentication = { - token: authentication.token ?? '', - }; - } - updateRequest.mutate({ authenticationType, authentication }); - }, - }, - }, + () => [ + { + value: 'body', + options: { + value: activeRequest.bodyType, + items: [ + { type: 'separator', label: 'Form Data' }, + { label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED }, + { label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART }, + { type: 'separator', label: 'Text Content' }, + { label: 'JSON', value: BODY_TYPE_JSON }, + { label: 'XML', value: BODY_TYPE_XML }, + { label: 'GraphQL', value: BODY_TYPE_GRAPHQL }, + { type: 'separator', label: 'Other' }, + { label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE }, ], + onChange: async (bodyType) => { + const patch: Partial = { bodyType }; + if (bodyType === BODY_TYPE_NONE) { + patch.headers = activeRequest.headers.filter( + (h) => h.name.toLowerCase() !== 'content-type', + ); + } else if ( + bodyType === BODY_TYPE_FORM_URLENCODED || + bodyType === BODY_TYPE_FORM_MULTIPART || + bodyType === BODY_TYPE_JSON || + bodyType === BODY_TYPE_XML + ) { + patch.method = 'POST'; + patch.headers = [ + ...(activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ?? + []), + { + name: 'Content-Type', + value: bodyType, + enabled: true, + }, + ]; + } else if (bodyType == BODY_TYPE_GRAPHQL) { + patch.method = 'POST'; + patch.headers = [ + ...(activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ?? + []), + { + name: 'Content-Type', + value: 'application/json', + enabled: true, + }, + ]; + } + + // Force update header editor so any changed headers are reflected + setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100); + + updateRequest.mutate(patch); + }, + }, + }, + { + value: 'params', + label: ( +
+ Params + p.name).length} /> +
+ ), + }, + { + value: 'headers', + label: ( +
+ Headers + h.name).length} /> +
+ ), + }, + { + value: 'auth', + label: 'Auth', + options: { + value: activeRequest.authenticationType, + items: [ + { label: 'Basic Auth', shortLabel: 'Basic', value: AUTH_TYPE_BASIC }, + { label: 'Bearer Token', shortLabel: 'Bearer', value: AUTH_TYPE_BEARER }, + { type: 'separator' }, + { label: 'No Authentication', shortLabel: 'Auth', value: AUTH_TYPE_NONE }, + ], + onChange: async (authenticationType) => { + let authentication: HttpRequest['authentication'] = activeRequest.authentication; + if (authenticationType === AUTH_TYPE_BASIC) { + authentication = { + username: authentication.username ?? '', + password: authentication.password ?? '', + }; + } else if (authenticationType === AUTH_TYPE_BEARER) { + authentication = { + token: authentication.token ?? '', + }; + } + updateRequest.mutate({ authenticationType, authentication }); + }, + }, + }, + ], [activeRequest, updateRequest], ); @@ -178,6 +179,27 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN [updateRequest], ); + const sendRequest = useSendRequest(activeRequest.id ?? null); + const handleSend = useCallback( + async (e: FormEvent) => { + e.preventDefault(); + await sendRequest.mutateAsync(); + }, + [sendRequest], + ); + + const handleMethodChange = useCallback( + (method: string) => updateRequest.mutate({ method }), + [updateRequest], + ); + const handleUrlChange = useCallback( + (url: string) => updateRequest.mutate({ url }), + [updateRequest], + ); + + const isLoading = useIsResponseLoading(activeRequestId ?? null); + const { updateKey } = useRequestUpdateKey(activeRequestId ?? null); + return (
@@ -262,7 +288,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN forceUpdateKey={forceUpdateKey} baseRequest={activeRequest} className="!bg-gray-50" - defaultValue={`${activeRequest?.body?.text ?? ''}`} + defaultValue={`${activeRequest.body?.text ?? ''}`} onChange={handleBodyTextChange} /> ) : activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ? ( diff --git a/src-web/components/RequestResponse.tsx b/src-web/components/RequestResponse.tsx deleted file mode 100644 index 0d961fbe..00000000 --- a/src-web/components/RequestResponse.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import useResizeObserver from '@react-hook/resize-observer'; -import classNames from 'classnames'; -import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; -import React, { memo, useCallback, useMemo, useRef, useState } from 'react'; -import { useLocalStorage } from 'react-use'; -import { useActiveRequest } from '../hooks/useActiveRequest'; -import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId'; -import { clamp } from '../lib/clamp'; -import { HotKeyList } from './core/HotKeyList'; -import { RequestPane } from './RequestPane'; -import { ResizeHandle } from './ResizeHandle'; -import { ResponsePane } from './ResponsePane'; - -interface Props { - style: CSSProperties; -} - -const rqst = { gridArea: 'rqst' }; -const resp = { gridArea: 'resp' }; -const drag = { gridArea: 'drag' }; - -const DEFAULT = 0.5; -const MIN_WIDTH_PX = 10; -const MIN_HEIGHT_PX = 30; -const STACK_VERTICAL_WIDTH = 700; - -export const RequestResponse = memo(function RequestResponse({ style }: Props) { - const containerRef = useRef(null); - const activeRequest = useActiveRequest(); - const [vertical, setVertical] = useState(false); - const [widthRaw, setWidth] = useLocalStorage(`body_width::${useActiveWorkspaceId()}`); - const [heightRaw, setHeight] = useLocalStorage(`body_height::${useActiveWorkspaceId()}`); - const width = widthRaw ?? DEFAULT; - const height = heightRaw ?? DEFAULT; - const [isResizing, setIsResizing] = useState(false); - const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>( - null, - ); - - useResizeObserver(containerRef.current, ({ contentRect }) => { - setVertical(contentRect.width < STACK_VERTICAL_WIDTH); - }); - - const styles = useMemo( - () => ({ - ...style, - gridTemplate: vertical - ? ` - ' ${rqst.gridArea}' minmax(0,${1 - height}fr) - ' ${drag.gridArea}' 0 - ' ${resp.gridArea}' minmax(0,${height}fr) - / 1fr - ` - : ` - ' ${rqst.gridArea} ${drag.gridArea} ${resp.gridArea}' minmax(0,1fr) - / ${1 - width}fr 0 ${width}fr - `, - }), - [vertical, width, height, style], - ); - - const unsub = () => { - if (moveState.current !== null) { - document.documentElement.removeEventListener('mousemove', moveState.current.move); - document.documentElement.removeEventListener('mouseup', moveState.current.up); - } - }; - - const handleReset = useCallback( - () => (vertical ? setHeight(DEFAULT) : setWidth(DEFAULT)), - [setHeight, vertical, setWidth], - ); - - const handleResizeStart = useCallback( - (e: ReactMouseEvent) => { - if (containerRef.current === null) return; - unsub(); - - const containerRect = containerRef.current.getBoundingClientRect(); - - const mouseStartX = e.clientX; - const mouseStartY = e.clientY; - const startWidth = containerRect.width * width; - const startHeight = containerRect.height * height; - - moveState.current = { - move: (e: MouseEvent) => { - e.preventDefault(); // Prevent text selection and things - if (vertical) { - const maxHeightPx = containerRect.height - MIN_HEIGHT_PX; - const newHeightPx = clamp( - startHeight - (e.clientY - mouseStartY), - MIN_HEIGHT_PX, - maxHeightPx, - ); - setHeight(newHeightPx / containerRect.height); - } else { - const maxWidthPx = containerRect.width - MIN_WIDTH_PX; - const newWidthPx = clamp( - startWidth - (e.clientX - mouseStartX), - MIN_WIDTH_PX, - maxWidthPx, - ); - setWidth(newWidthPx / containerRect.width); - } - }, - up: (e: MouseEvent) => { - e.preventDefault(); - unsub(); - setIsResizing(false); - }, - }; - document.documentElement.addEventListener('mousemove', moveState.current.move); - document.documentElement.addEventListener('mouseup', moveState.current.up); - setIsResizing(true); - }, - [width, height, vertical, setHeight, setWidth], - ); - - if (activeRequest === null) { - return ; - } - - return ( -
- - - -
- ); -}); diff --git a/src-web/components/ResizeHandle.tsx b/src-web/components/ResizeHandle.tsx index 7af97979..52cd6448 100644 --- a/src-web/components/ResizeHandle.tsx +++ b/src-web/components/ResizeHandle.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames'; import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; import React from 'react'; +import { Separator } from './core/Separator'; interface ResizeBarProps { style?: CSSProperties; @@ -17,6 +18,7 @@ export function ResizeHandle({ style, justify, className, + barClassName, onResizeStart, onReset, isResizing, @@ -28,6 +30,8 @@ export function ResizeHandle({ aria-hidden draggable style={style} + onDragStart={onResizeStart} + onDoubleClick={onReset} className={classNames( className, 'group z-10 flex', @@ -39,8 +43,6 @@ export function ResizeHandle({ side === 'left' && 'left-0', side === 'top' && 'top-0', )} - onDragStart={onResizeStart} - onDoubleClick={onReset} > {/* Show global overlay with cursor style to ensure cursor remains the same when moving quickly */} {isResizing && ( diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index 1b435db7..23840e63 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -2,12 +2,11 @@ import classNames from 'classnames'; import type { CSSProperties } from 'react'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { createGlobalState } from 'react-use'; -import { useActiveRequest } from '../hooks/useActiveRequest'; -import { useLatestResponse } from '../hooks/useLatestResponse'; +import { useHttpResponses } from '../hooks/useHttpResponses'; +import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse'; import { useResponseContentType } from '../hooks/useResponseContentType'; -import { useResponses } from '../hooks/useResponses'; import { useResponseViewMode } from '../hooks/useResponseViewMode'; -import type { HttpResponse } from '../lib/models'; +import type { HttpRequest, HttpResponse } from '../lib/models'; import { isResponseLoading } from '../lib/models'; import { Banner } from './core/Banner'; import { CountBadge } from './core/CountBadge'; @@ -29,15 +28,15 @@ import { WebPageViewer } from './responseViewers/WebPageViewer'; interface Props { style?: CSSProperties; className?: string; + activeRequest: HttpRequest; } const useActiveTab = createGlobalState('body'); -export const ResponsePane = memo(function ResponsePane({ style, className }: Props) { +export const ResponsePane = memo(function ResponsePane({ style, className, activeRequest }: Props) { const [pinnedResponseId, setPinnedResponseId] = useState(null); - const activeRequest = useActiveRequest(); - const latestResponse = useLatestResponse(activeRequest?.id ?? null); - const responses = useResponses(activeRequest?.id ?? null); + const latestResponse = useLatestHttpResponse(activeRequest.id); + const responses = useHttpResponses(activeRequest.id); const activeResponse: HttpResponse | null = pinnedResponseId ? responses.find((r) => r.id === pinnedResponseId) ?? null : latestResponse ?? null; @@ -85,10 +84,6 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro [activeResponse?.headers, contentType, setViewMode, viewMode], ); - if (activeRequest === null) { - return null; - } - return (
)} @@ -179,6 +174,8 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro ) : contentType?.match(/csv|tab-separated/) ? ( ) : ( + // ) : contentType?.startsWith('application/json') ? ( + // )} diff --git a/src-web/components/SettingsDialog.tsx b/src-web/components/SettingsDialog.tsx index 602dabf5..2419f44e 100644 --- a/src-web/components/SettingsDialog.tsx +++ b/src-web/components/SettingsDialog.tsx @@ -29,11 +29,20 @@ export const SettingsDialog = () => { size="sm" value={settings.appearance} onChange={(appearance) => updateSettings.mutateAsync({ ...settings, appearance })} - options={{ - system: 'System', - light: 'Light', - dark: 'Dark', - }} + options={[ + { + label: 'System', + value: 'system', + }, + { + label: 'Light', + value: 'light', + }, + { + label: 'Dark', + value: 'dark', + }, + ]} /> setIsFocused(true)} onBlur={() => setIsFocused(false)} containerClassName="shadow shadow-gray-100 dark:shadow-gray-50" - onChange={handleUrlChange} + onChange={onUrlChange} defaultValue={url} - placeholder="https://example.com" + placeholder={placeholder} leftSlot={ - + method != null && + onMethodChange != null && ( + + ) } rightSlot={ - + submitIcon !== null && ( + + ) } /> diff --git a/src-web/components/Workspace.tsx b/src-web/components/Workspace.tsx index 6b6a0e6a..27ef7b60 100644 --- a/src-web/components/Workspace.tsx +++ b/src-web/components/Workspace.tsx @@ -8,14 +8,17 @@ import type { } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useWindowSize } from 'react-use'; +import { useActiveRequest } from '../hooks/useActiveRequest'; import { useIsFullscreen } from '../hooks/useIsFullscreen'; import { useOsInfo } from '../hooks/useOsInfo'; import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarWidth } from '../hooks/useSidebarWidth'; import { Button } from './core/Button'; +import { HotKeyList } from './core/HotKeyList'; import { HStack } from './core/Stacks'; +import { GrpcConnectionLayout } from './GrpcConnectionLayout'; +import { HttpRequestLayout } from './HttpRequestLayout'; import { Overlay } from './Overlay'; -import { RequestResponse } from './RequestResponse'; import { ResizeHandle } from './ResizeHandle'; import { Sidebar } from './Sidebar'; import { SidebarActions } from './SidebarActions'; @@ -31,7 +34,7 @@ const WINDOW_FLOATING_SIDEBAR_WIDTH = 600; export default function Workspace() { const { setWidth, width, resetWidth } = useSidebarWidth(); const { hide, show, hidden } = useSidebarHidden(); - + const activeRequest = useActiveRequest(); const windowSize = useWindowSize(); const [floating, setFloating] = useState(false); const [isResizing, setIsResizing] = useState(false); @@ -44,7 +47,7 @@ export default function Workspace() { const shouldHide = windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH; if (shouldHide && !floating) { setFloating(true); - hide(); + hide().catch(console.error); } else if (!shouldHide && floating) { setFloating(false); } @@ -69,10 +72,10 @@ export default function Workspace() { e.preventDefault(); // Prevent text selection and things const newWidth = startWidth + (e.clientX - mouseStartX); if (newWidth < 100) { - hide(); + await hide(); resetWidth(); } else { - show(); + await show(); setWidth(newWidth); } }, @@ -163,7 +166,13 @@ export default function Workspace() { > - + {activeRequest == null ? ( + + ) : activeRequest.model === 'grpc_request' ? ( + + ) : ( + + )}
); } diff --git a/src-web/components/WorkspaceActionsDropdown.tsx b/src-web/components/WorkspaceActionsDropdown.tsx index 2f168440..707c15f5 100644 --- a/src-web/components/WorkspaceActionsDropdown.tsx +++ b/src-web/components/WorkspaceActionsDropdown.tsx @@ -51,23 +51,13 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ ), render: ({ hide }) => { return ( - + - + ); }, diff --git a/src-web/components/core/Button.tsx b/src-web/components/core/Button.tsx index 7c38d200..cc55226d 100644 --- a/src-web/components/core/Button.tsx +++ b/src-web/components/core/Button.tsx @@ -8,6 +8,7 @@ import { Icon } from './Icon'; export type ButtonProps = Omit, 'color'> & { innerClassName?: string; color?: 'custom' | 'default' | 'gray' | 'primary' | 'secondary' | 'warning' | 'danger'; + variant?: 'border' | 'solid'; isLoading?: boolean; size?: 'sm' | 'md' | 'xs'; justify?: 'start' | 'center'; @@ -27,10 +28,11 @@ export const Button = forwardRef(function Button innerClassName, children, forDropdown, - color, + color = 'default', type = 'button', justify = 'center', size = 'md', + variant = 'solid', leftSlot, rightSlot, disabled, @@ -53,24 +55,45 @@ export const Button = forwardRef(function Button 'flex-shrink-0 flex items-center', 'focus-visible-or-class:ring rounded-md', disabled ? 'pointer-events-none opacity-disabled' : 'pointer-events-auto', - color === 'custom' && 'ring-blue-500/50', - color === 'default' && - 'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-1000 ring-blue-500/50', - color === 'gray' && - 'text-gray-800 bg-highlight enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-1000 ring-blue-500/50', - color === 'primary' && 'bg-blue-400 text-white enabled:hocus:bg-blue-500 ring-blue-500/50', - color === 'secondary' && - 'bg-violet-400 text-white enabled:hocus:bg-violet-500 ring-violet-500/50', - color === 'warning' && - 'bg-orange-400 text-white enabled:hocus:bg-orange-500 ring-orange-500/50', - color === 'danger' && 'bg-red-400 text-white enabled:hocus:bg-red-500 ring-red-500/50', justify === 'start' && 'justify-start', justify === 'center' && 'justify-center', size === 'md' && 'h-md px-3', size === 'sm' && 'h-sm px-2.5 text-sm', size === 'xs' && 'h-xs px-2 text-sm', + // Solids + variant === 'solid' && color === 'custom' && 'ring-blue-500/50', + variant === 'solid' && + color === 'default' && + 'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-800 ring-blue-500/50', + variant === 'solid' && + color === 'gray' && + 'text-gray-800 bg-highlight enabled:hocus:text-gray-1000 ring-gray-400', + variant === 'solid' && color === 'primary' && 'bg-blue-400 text-white ring-blue-700', + variant === 'solid' && color === 'secondary' && 'bg-violet-400 text-white ring-violet-700', + variant === 'solid' && color === 'warning' && 'bg-orange-400 text-white ring-orange-700', + variant === 'solid' && color === 'danger' && 'bg-red-400 text-white ring-red-700', + // Borders + variant === 'border' && 'border', + variant === 'border' && + color === 'default' && + 'border-highlight text-gray-700 enabled:hocus:border-focus enabled:hocus:text-gray-800 ring-blue-500/50', + variant === 'border' && + color === 'gray' && + 'border-gray-500/70 text-gray-700 enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-800 ring-blue-500/50', + variant === 'border' && + color === 'primary' && + 'border-blue-500/70 text-blue-700 enabled:hocus:border-blue-500 ring-blue-500/50', + variant === 'border' && + color === 'secondary' && + 'border-violet-500/70 text-violet-700 enabled:hocus:border-violet-500 ring-violet-500/50', + variant === 'border' && + color === 'warning' && + 'border-orange-500/70 text-orange-700 enabled:hocus:border-orange-500 ring-orange-500/50', + variant === 'border' && + color === 'danger' && + 'border-red-500/70 text-red-700 enabled:hocus:border-red-500 ring-red-500/50', ), - [className, disabled, color, justify, size], + [className, disabled, justify, size, variant, color], ); const buttonRef = useRef(null); @@ -100,7 +123,7 @@ export const Button = forwardRef(function Button ) : null}
{count} diff --git a/src-web/components/core/Dropdown.tsx b/src-web/components/core/Dropdown.tsx index d0be47ab..4a802793 100644 --- a/src-web/components/core/Dropdown.tsx +++ b/src-web/components/core/Dropdown.tsx @@ -399,7 +399,7 @@ const Menu = forwardRef, MenuPro {items.map((item, i) => { if (item.type === 'separator') { return ( - + {item.label} ); @@ -473,7 +473,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men className={classNames( className, 'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap', - 'focus:bg-highlight focus:text-gray-900 rounded', + 'focus:bg-highlight focus:text-gray-800 rounded', item.variant === 'danger' && 'text-red-600', item.variant === 'notify' && 'text-pink-600', )} diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index f3867c53..7f13640c 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -38,7 +38,7 @@ export interface EditorProps { className?: string; heightMode?: 'auto' | 'full'; contentType?: string | null; - forceUpdateKey?: string; + forceUpdateKey?: string | number; autoFocus?: boolean; autoSelect?: boolean; defaultValue?: string | null; diff --git a/src-web/components/core/Editor/extensions.ts b/src-web/components/core/Editor/extensions.ts index f540bf55..e67052ac 100644 --- a/src-web/components/core/Editor/extensions.ts +++ b/src-web/components/core/Editor/extensions.ts @@ -10,7 +10,6 @@ import { json } from '@codemirror/lang-json'; import { xml } from '@codemirror/lang-xml'; import type { LanguageSupport } from '@codemirror/language'; import { - bracketMatching, foldGutter, foldKeymap, HighlightStyle, @@ -32,6 +31,7 @@ import { } from '@codemirror/view'; import { tags as t } from '@lezer/highlight'; import { graphql, graphqlLanguageSupport } from 'cm6-graphql'; +import { jsonSchema } from 'codemirror-json-schema'; import type { Environment, Workspace } from '../../../lib/models'; import type { EditorProps } from './index'; import { text } from './text/extension'; @@ -83,6 +83,7 @@ export const myHighlightStyle = HighlightStyle.define([ // ]); const syntaxExtensions: Record = { + 'application/grpc': jsonSchema() as any, // TODO: Fix this 'application/graphql': graphqlLanguageSupport(), 'application/json': json(), 'application/javascript': javascript(), @@ -119,7 +120,6 @@ export const baseExtensions = [ history(), dropCursor(), drawSelection(), - bracketMatching(), // TODO: Figure out how to debounce showing of autocomplete in a good way // debouncedAutocompletionDisplay({ millis: 1000 }), // autocompletion({ closeOnBlur: true, interactionDelay: 200, activateOnTyping: false }), diff --git a/src-web/components/core/FormattedError.tsx b/src-web/components/core/FormattedError.tsx index 97823fb3..27b2da83 100644 --- a/src-web/components/core/FormattedError.tsx +++ b/src-web/components/core/FormattedError.tsx @@ -1,11 +1,11 @@ import classNames from 'classnames'; +import type { ReactNode } from 'react'; interface Props { - children: string; + children: ReactNode; } export function FormattedError({ children }: Props) { - console.log('ERROR', children); return (
diff --git a/src-web/components/core/JsonAttributeTree.tsx b/src-web/components/core/JsonAttributeTree.tsx
new file mode 100644
index 00000000..08ef46c8
--- /dev/null
+++ b/src-web/components/core/JsonAttributeTree.tsx
@@ -0,0 +1,122 @@
+import classNames from 'classnames';
+import type { ReactNode } from 'react';
+import { useMemo, useState } from 'react';
+import { Icon } from './Icon';
+
+interface Props {
+  depth?: number;
+  attrValue: any;
+  attrKey?: string | number;
+  attrKeyJsonPath?: string;
+}
+
+export const JsonAttributeTree = ({ depth = 0, attrKey, attrValue, attrKeyJsonPath }: Props) => {
+  attrKeyJsonPath = attrKeyJsonPath ?? `${attrKey}`;
+
+  const [isExpanded, setIsExpanded] = useState(true);
+  const toggleExpanded = () => setIsExpanded((v) => !v);
+
+  const { isExpandable, children, label, labelClassName } = useMemo<{
+    isExpandable: boolean;
+    children: ReactNode;
+    label?: string;
+    labelClassName?: string;
+  }>(() => {
+    const jsonType = Object.prototype.toString.call(attrValue);
+    if (jsonType === '[object Object]') {
+      return {
+        children: isExpanded
+          ? Object.keys(attrValue)
+              .sort((a, b) => a.localeCompare(b))
+              .flatMap((k) => (
+                
+              ))
+          : null,
+        isExpandable: true,
+        label: isExpanded ? '{ }' : `{⋯}`,
+        labelClassName: 'text-gray-600',
+      };
+    } else if (jsonType === '[object Array]') {
+      return {
+        children: isExpanded
+          ? attrValue.flatMap((v: any, i: number) => (
+              
+            ))
+          : null,
+        isExpandable: true,
+        label: isExpanded ? '[ ]' : `[⋯]`,
+        labelClassName: 'text-gray-600',
+      };
+    } else {
+      return {
+        children: null,
+        isExpandable: false,
+        label: jsonType === '[object String]' ? `"${attrValue}"` : `${attrValue}`,
+        labelClassName: classNames(
+          jsonType === '[object Boolean]' && 'text-pink-600',
+          jsonType === '[object Number]' && 'text-blue-600',
+          jsonType === '[object String]' && 'text-yellow-600',
+          jsonType === '[object Null]' && 'text-red-600',
+        ),
+      };
+    }
+  }, [attrValue, attrKeyJsonPath, isExpanded, depth]);
+
+  const labelEl = (
+    
+      {label}
+    
+  );
+  return (
+    
+
+ {isExpandable ? ( + + ) : ( + <> + + {attrKey}: + + {labelEl} + + )} +
+ {children &&
{children}
} +
+ ); +}; + +function joinObjectKey(baseKey: string | undefined, key: string): string { + const quotedKey = key.match(/^[a-z0-9_]+$/i) ? key : `\`${key}\``; + + if (baseKey == null) return quotedKey; + else return `${baseKey}.${quotedKey}`; +} + +function joinArrayKey(baseKey: string | undefined, index: number): string { + return `${baseKey ?? ''}[${index}]`; +} diff --git a/src-web/components/core/Link.tsx b/src-web/components/core/Link.tsx new file mode 100644 index 00000000..ada25c3e --- /dev/null +++ b/src-web/components/core/Link.tsx @@ -0,0 +1,35 @@ +import classNames from 'classnames'; +import type { HTMLAttributes } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { Icon } from './Icon'; + +interface Props extends HTMLAttributes { + href: string; +} + +export function Link({ href, children, className, ...other }: Props) { + const isExternal = href.match(/^https?:\/\//); + + className = classNames(className, 'relative underline hover:text-violet-600'); + + if (isExternal) { + return ( + + {children} + + + ); + } + + return ( + + {children} + + ); +} diff --git a/src-web/components/core/Select.tsx b/src-web/components/core/Select.tsx index 4ef42883..8c733063 100644 --- a/src-web/components/core/Select.tsx +++ b/src-web/components/core/Select.tsx @@ -6,10 +6,11 @@ interface Props { labelPosition?: 'top' | 'left'; labelClassName?: string; hideLabel?: boolean; - value: string; - options: Record; + value: T; + options: { label: string; value: T }[]; onChange: (value: T) => void; size?: 'xs' | 'sm' | 'md' | 'lg'; + className?: string; } export function Select({ @@ -21,12 +22,14 @@ export function Select({ value, options, onChange, + className, size = 'md', }: Props) { const id = `input-${name}`; return (
({ style={selectBackgroundStyles} onChange={(e) => onChange(e.target.value as T)} className={classNames( - 'font-mono text-xs border w-full px-2 outline-none bg-transparent', + 'font-mono text-xs border w-full outline-none bg-transparent pl-2 pr-7', 'border-highlight focus:border-focus', size === 'xs' && 'h-xs', size === 'sm' && 'h-sm', @@ -56,8 +59,8 @@ export function Select({ size === 'lg' && 'h-lg', )} > - {Object.entries(options).map(([value, label]) => ( - ))} @@ -68,7 +71,7 @@ export function Select({ const selectBackgroundStyles = { backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`, - backgroundPosition: 'right 0.5rem center', + backgroundPosition: 'right 0.3rem center', backgroundRepeat: 'no-repeat', backgroundSize: '1.5em 1.5em', }; diff --git a/src-web/components/core/Separator.tsx b/src-web/components/core/Separator.tsx index e474157d..0a36d05e 100644 --- a/src-web/components/core/Separator.tsx +++ b/src-web/components/core/Separator.tsx @@ -1,10 +1,11 @@ import classNames from 'classnames'; +import type { ReactNode } from 'react'; interface Props { orientation?: 'horizontal' | 'vertical'; variant?: 'primary' | 'secondary'; className?: string; - children?: string; + children?: ReactNode; } export function Separator({ diff --git a/src-web/components/core/SplitLayout.tsx b/src-web/components/core/SplitLayout.tsx new file mode 100644 index 00000000..85ab5ff0 --- /dev/null +++ b/src-web/components/core/SplitLayout.tsx @@ -0,0 +1,169 @@ +import useResizeObserver from '@react-hook/resize-observer'; +import classNames from 'classnames'; +import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { useLocalStorage } from 'react-use'; +import { useActiveRequestId } from '../../hooks/useActiveRequestId'; +import { useActiveWorkspaceId } from '../../hooks/useActiveWorkspaceId'; +import { clamp } from '../../lib/clamp'; +import { ResizeHandle } from '../ResizeHandle'; +import { HotKeyList } from './HotKeyList'; + +interface SlotProps { + orientation: 'horizontal' | 'vertical'; + style: CSSProperties; +} + +interface Props { + name: string; + firstSlot: (props: SlotProps) => ReactNode; + secondSlot: null | ((props: SlotProps) => ReactNode); + style?: CSSProperties; + className?: string; + defaultRatio?: number; + minHeightPx?: number; + minWidthPx?: number; + forceVertical?: boolean; +} + +const areaL = { gridArea: 'left' }; +const areaR = { gridArea: 'right' }; +const areaD = { gridArea: 'drag' }; + +const STACK_VERTICAL_WIDTH = 700; + +export function SplitLayout({ + style, + firstSlot, + secondSlot, + className, + name, + forceVertical, + defaultRatio = 0.5, + minHeightPx = 10, + minWidthPx = 10, +}: Props) { + const containerRef = useRef(null); + const [vertical, setVertical] = useState(false); + const [widthRaw, setWidth] = useLocalStorage(`${name}_width::${useActiveWorkspaceId()}`); + const [heightRaw, setHeight] = useLocalStorage( + `${name}_height::${useActiveWorkspaceId()}`, + ); + const width = widthRaw ?? defaultRatio; + let height = heightRaw ?? defaultRatio; + const [isResizing, setIsResizing] = useState(false); + const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>( + null, + ); + + if (!secondSlot) { + height = 0; + minHeightPx = 0; + } + + useResizeObserver(containerRef.current, ({ contentRect }) => { + setVertical(contentRect.width < STACK_VERTICAL_WIDTH); + }); + + const styles = useMemo(() => { + return { + ...style, + gridTemplate: + forceVertical || vertical + ? ` + ' ${areaL.gridArea}' minmax(0,${1 - height}fr) + ' ${areaD.gridArea}' 0 + ' ${areaR.gridArea}' minmax(${minHeightPx}px,${height}fr) + / 1fr + ` + : ` + ' ${areaL.gridArea} ${areaD.gridArea} ${areaR.gridArea}' minmax(0,1fr) + / ${1 - width}fr 0 ${width}fr + `, + }; + }, [style, vertical, height, minHeightPx, width]); + + const unsub = () => { + if (moveState.current !== null) { + document.documentElement.removeEventListener('mousemove', moveState.current.move); + document.documentElement.removeEventListener('mouseup', moveState.current.up); + } + }; + + const handleReset = useCallback( + () => (vertical ? setHeight(defaultRatio) : setWidth(defaultRatio)), + [vertical, setHeight, defaultRatio, setWidth], + ); + + const handleResizeStart = useCallback( + (e: ReactMouseEvent) => { + if (containerRef.current === null) return; + unsub(); + + const containerRect = containerRef.current.getBoundingClientRect(); + + const mouseStartX = e.clientX; + const mouseStartY = e.clientY; + const startWidth = containerRect.width * width; + const startHeight = containerRect.height * height; + + moveState.current = { + move: (e: MouseEvent) => { + e.preventDefault(); // Prevent text selection and things + if (vertical) { + const maxHeightPx = containerRect.height - minHeightPx; + const newHeightPx = clamp( + startHeight - (e.clientY - mouseStartY), + minHeightPx, + maxHeightPx, + ); + setHeight(newHeightPx / containerRect.height); + } else { + const maxWidthPx = containerRect.width - minWidthPx; + const newWidthPx = clamp( + startWidth - (e.clientX - mouseStartX), + minWidthPx, + maxWidthPx, + ); + setWidth(newWidthPx / containerRect.width); + } + }, + up: (e: MouseEvent) => { + e.preventDefault(); + unsub(); + setIsResizing(false); + }, + }; + document.documentElement.addEventListener('mousemove', moveState.current.move); + document.documentElement.addEventListener('mouseup', moveState.current.up); + setIsResizing(true); + }, + [width, height, vertical, minHeightPx, setHeight, minWidthPx, setWidth], + ); + + const activeRequestId = useActiveRequestId(); + if (activeRequestId === null) { + return ; + } + + return ( +
+ {firstSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })} + {secondSlot && ( + <> + + {secondSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })} + + )} +
+ ); +} diff --git a/src-web/components/core/Stacks.tsx b/src-web/components/core/Stacks.tsx index e9eef566..00b3cb14 100644 --- a/src-web/components/core/Stacks.tsx +++ b/src-web/components/core/Stacks.tsx @@ -6,6 +6,7 @@ const gapClasses = { 0: 'gap-0', 0.5: 'gap-0.5', 1: 'gap-1', + 1.5: 'gap-1.5', 2: 'gap-2', 3: 'gap-3', 4: 'gap-4', @@ -56,7 +57,7 @@ export const VStack = forwardRef(function VStack( type BaseStackProps = HTMLAttributes & { as?: ComponentType | 'ul' | 'label' | 'form'; space?: keyof typeof gapClasses; - alignItems?: 'start' | 'center' | 'stretch'; + alignItems?: 'start' | 'center' | 'stretch' | 'end'; justifyContent?: 'start' | 'center' | 'end' | 'between'; }; @@ -75,6 +76,7 @@ const BaseStack = forwardRef(function BaseStack( alignItems === 'center' && 'items-center', alignItems === 'start' && 'items-start', alignItems === 'stretch' && 'items-stretch', + alignItems === 'end' && 'items-end', justifyContent === 'start' && 'justify-start', justifyContent === 'center' && 'justify-center', justifyContent === 'end' && 'justify-end', diff --git a/src-web/components/responseViewers/JsonViewer.tsx b/src-web/components/responseViewers/JsonViewer.tsx new file mode 100644 index 00000000..62949b3a --- /dev/null +++ b/src-web/components/responseViewers/JsonViewer.tsx @@ -0,0 +1,25 @@ +import classNames from 'classnames'; +import { useResponseBodyText } from '../../hooks/useResponseBodyText'; +import type { HttpResponse } from '../../lib/models'; +import { JsonAttributeTree } from '../core/JsonAttributeTree'; + +interface Props { + response: HttpResponse; + className?: string; +} + +export function JsonViewer({ response, className }: Props) { + const rawBody = useResponseBodyText(response) ?? ''; + let parsed = {}; + try { + parsed = JSON.parse(rawBody); + } catch (e) { + // foo + } + + return ( +
+ +
+ ); +} diff --git a/src-web/hooks/Confirm.tsx b/src-web/hooks/Confirm.tsx index 127ff9c0..fdf2c594 100644 --- a/src-web/hooks/Confirm.tsx +++ b/src-web/hooks/Confirm.tsx @@ -30,13 +30,13 @@ export function Confirm({ onHide, onResult, variant = 'confirm' }: ConfirmProps) }; return ( - + + - ); } diff --git a/src-web/hooks/useActiveRequest.ts b/src-web/hooks/useActiveRequest.ts index 0c8e96df..1af9b2bf 100644 --- a/src-web/hooks/useActiveRequest.ts +++ b/src-web/hooks/useActiveRequest.ts @@ -1,9 +1,28 @@ -import type { HttpRequest } from '../lib/models'; +import { r } from 'vitest/dist/types-94cfe4b4'; +import type { GrpcRequest, HttpRequest } from '../lib/models'; import { useActiveRequestId } from './useActiveRequestId'; -import { useRequests } from './useRequests'; +import { useGrpcRequests } from './useGrpcRequests'; +import { useHttpRequests } from './useHttpRequests'; -export function useActiveRequest(): HttpRequest | null { - const requestId = useActiveRequestId(); - const requests = useRequests(); - return requests.find((r) => r.id === requestId) ?? null; +interface TypeMap { + http_request: HttpRequest; + grpc_request: GrpcRequest; +} + +export function useActiveRequest( + model?: T | undefined, +): TypeMap[T] | null { + const requestId = useActiveRequestId(); + const httpRequests = useHttpRequests(); + const grpcRequests = useGrpcRequests(); + + if (model === 'http_request') { + return (httpRequests.find((r) => r.id === requestId) ?? null) as TypeMap[T] | null; + } else if (model === 'grpc_request') { + return (grpcRequests.find((r) => r.id === requestId) ?? null) as TypeMap[T] | null; + } else { + return (grpcRequests.find((r) => r.id === requestId) ?? + httpRequests.find((r) => r.id === requestId) ?? + null) as TypeMap[T] | null; + } } diff --git a/src-web/hooks/useAlert.ts b/src-web/hooks/useAlert.ts index 0dbfac2a..2c3b84e0 100644 --- a/src-web/hooks/useAlert.ts +++ b/src-web/hooks/useAlert.ts @@ -1,3 +1,4 @@ +import { useCallback } from 'react'; import type { DialogProps } from '../components/core/Dialog'; import { useDialog } from '../components/DialogContext'; import type { AlertProps } from './Alert'; @@ -5,20 +6,16 @@ import { Alert } from './Alert'; export function useAlert() { const dialog = useDialog(); - return ({ - id, - title, - body, - }: { - id: string; - title: DialogProps['title']; - body: AlertProps['body']; - }) => - dialog.show({ - id, - title, - hideX: true, - size: 'sm', - render: ({ hide }) => Alert({ onHide: hide, body }), - }); + return useCallback( + ({ id, title, body }: { id: string; title: DialogProps['title']; body: AlertProps['body'] }) => + dialog.show({ + id, + title, + hideX: true, + size: 'sm', + render: ({ hide }) => Alert({ onHide: hide, body }), + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); } diff --git a/src-web/hooks/useCookieJars.ts b/src-web/hooks/useCookieJars.ts index d87fec2d..ec4481c7 100644 --- a/src-web/hooks/useCookieJars.ts +++ b/src-web/hooks/useCookieJars.ts @@ -15,7 +15,7 @@ export function useCookieJars() { queryKey: cookieJarsQueryKey({ workspaceId: workspaceId ?? 'n/a' }), queryFn: async () => { if (workspaceId == null) return []; - return (await invoke('list_cookie_jars', { workspaceId })) as CookieJar[]; + return (await invoke('cmd_list_cookie_jars', { workspaceId })) as CookieJar[]; }, }).data ?? [] ); diff --git a/src-web/hooks/useCreateCookieJar.ts b/src-web/hooks/useCreateCookieJar.ts index 9f2c70be..0e1bf02d 100644 --- a/src-web/hooks/useCreateCookieJar.ts +++ b/src-web/hooks/useCreateCookieJar.ts @@ -1,17 +1,17 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api'; import { trackEvent } from '../lib/analytics'; -import type { HttpRequest } from '../lib/models'; +import type { CookieJar } from '../lib/models'; import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { cookieJarsQueryKey } from './useCookieJars'; import { usePrompt } from './usePrompt'; -import { requestsQueryKey } from './useRequests'; export function useCreateCookieJar() { const workspaceId = useActiveWorkspaceId(); const queryClient = useQueryClient(); const prompt = usePrompt(); - return useMutation({ + return useMutation({ mutationFn: async () => { if (workspaceId === null) { throw new Error("Cannot create cookie jar when there's no active workspace"); @@ -23,13 +23,13 @@ export function useCreateCookieJar() { label: 'Name', defaultValue: 'My Jar', }); - return invoke('create_cookie_jar', { workspaceId, name }); + return invoke('cmd_create_cookie_jar', { workspaceId, name }); }, onSettled: () => trackEvent('CookieJar', 'Create'), - onSuccess: async (request) => { - queryClient.setQueryData( - requestsQueryKey({ workspaceId: request.workspaceId }), - (requests) => [...(requests ?? []), request], + onSuccess: async (cookieJar) => { + queryClient.setQueryData( + cookieJarsQueryKey({ workspaceId: cookieJar.workspaceId }), + (items) => [...(items ?? []), cookieJar], ); }, }); diff --git a/src-web/hooks/useCreateEnvironment.ts b/src-web/hooks/useCreateEnvironment.ts index 23f0d92f..871be224 100644 --- a/src-web/hooks/useCreateEnvironment.ts +++ b/src-web/hooks/useCreateEnvironment.ts @@ -22,7 +22,7 @@ export function useCreateEnvironment() { label: 'Name', defaultValue: 'My Environment', }); - return invoke('create_environment', { name, variables: [], workspaceId }); + return invoke('cmd_create_environment', { name, variables: [], workspaceId }); }, onSettled: () => trackEvent('Environment', 'Create'), onSuccess: async (environment) => { diff --git a/src-web/hooks/useCreateFolder.ts b/src-web/hooks/useCreateFolder.ts index 844c77d8..abe6eabb 100644 --- a/src-web/hooks/useCreateFolder.ts +++ b/src-web/hooks/useCreateFolder.ts @@ -16,7 +16,7 @@ export function useCreateFolder() { } patch.name = patch.name || 'New Folder'; patch.sortPriority = patch.sortPriority || -Date.now(); - return invoke('create_folder', { workspaceId, ...patch }); + return invoke('cmd_create_folder', { workspaceId, ...patch }); }, onSettled: () => trackEvent('Folder', 'Create'), onSuccess: async (request) => { diff --git a/src-web/hooks/useCreateGrpcRequest.ts b/src-web/hooks/useCreateGrpcRequest.ts new file mode 100644 index 00000000..adaa7020 --- /dev/null +++ b/src-web/hooks/useCreateGrpcRequest.ts @@ -0,0 +1,55 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import { trackEvent } from '../lib/analytics'; +import type { GrpcRequest, HttpRequest } from '../lib/models'; +import { useActiveEnvironmentId } from './useActiveEnvironmentId'; +import { useActiveRequest } from './useActiveRequest'; +import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useAppRoutes } from './useAppRoutes'; +import { grpcRequestsQueryKey } from './useGrpcRequests'; +import { httpRequestsQueryKey } from './useHttpRequests'; + +export function useCreateGrpcRequest() { + const workspaceId = useActiveWorkspaceId(); + const activeEnvironmentId = useActiveEnvironmentId(); + // const activeRequest = useActiveRequest(); + const activeRequest = null; + const routes = useAppRoutes(); + const queryClient = useQueryClient(); + + return useMutation< + GrpcRequest, + unknown, + Partial> + >({ + mutationFn: (patch) => { + if (workspaceId === null) { + throw new Error("Cannot create grpc request when there's no active workspace"); + } + if (patch.sortPriority === undefined) { + if (activeRequest != null) { + // Place above currently-active request + // patch.sortPriority = activeRequest.sortPriority + 0.0001; + } else { + // Place at the very top + patch.sortPriority = -Date.now(); + } + } + // patch.folderId = patch.folderId; // TODO: || activeRequest?.folderId; + return invoke('cmd_create_grpc_request', { workspaceId, name: '', ...patch }); + }, + onSettled: () => trackEvent('GrpcRequest', 'Create'), + onSuccess: async (request) => { + queryClient.setQueryData( + grpcRequestsQueryKey({ workspaceId: request.workspaceId }), + (requests) => [...(requests ?? []), request], + ); + // TODO: This should navigate to the new request + routes.navigate('request', { + workspaceId: request.workspaceId, + requestId: request.id, + environmentId: activeEnvironmentId ?? undefined, + }); + }, + }); +} diff --git a/src-web/hooks/useCreateRequest.ts b/src-web/hooks/useCreateHttpRequest.ts similarity index 87% rename from src-web/hooks/useCreateRequest.ts rename to src-web/hooks/useCreateHttpRequest.ts index ba5b2f10..19daee46 100644 --- a/src-web/hooks/useCreateRequest.ts +++ b/src-web/hooks/useCreateHttpRequest.ts @@ -6,9 +6,9 @@ import { useActiveEnvironmentId } from './useActiveEnvironmentId'; import { useActiveRequest } from './useActiveRequest'; import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { useAppRoutes } from './useAppRoutes'; -import { requestsQueryKey } from './useRequests'; +import { httpRequestsQueryKey } from './useHttpRequests'; -export function useCreateRequest() { +export function useCreateHttpRequest() { const workspaceId = useActiveWorkspaceId(); const activeEnvironmentId = useActiveEnvironmentId(); const activeRequest = useActiveRequest(); @@ -34,12 +34,12 @@ export function useCreateRequest() { } } patch.folderId = patch.folderId || activeRequest?.folderId; - return invoke('create_request', { workspaceId, name: '', ...patch }); + return invoke('cmd_create_http_request', { workspaceId, name: '', ...patch }); }, onSettled: () => trackEvent('HttpRequest', 'Create'), onSuccess: async (request) => { queryClient.setQueryData( - requestsQueryKey({ workspaceId: request.workspaceId }), + httpRequestsQueryKey({ workspaceId: request.workspaceId }), (requests) => [...(requests ?? []), request], ); routes.navigate('request', { diff --git a/src-web/hooks/useCreateWorkspace.ts b/src-web/hooks/useCreateWorkspace.ts index 39c510c3..58422cfa 100644 --- a/src-web/hooks/useCreateWorkspace.ts +++ b/src-web/hooks/useCreateWorkspace.ts @@ -10,7 +10,7 @@ export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean } const queryClient = useQueryClient(); return useMutation>({ mutationFn: (patch) => { - return invoke('create_workspace', patch); + return invoke('cmd_create_workspace', patch); }, onSettled: () => trackEvent('Workspace', 'Create'), onSuccess: async (workspace) => { diff --git a/src-web/hooks/useDeleteAnyGrpcRequest.tsx b/src-web/hooks/useDeleteAnyGrpcRequest.tsx new file mode 100644 index 00000000..d824605c --- /dev/null +++ b/src-web/hooks/useDeleteAnyGrpcRequest.tsx @@ -0,0 +1,43 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import { InlineCode } from '../components/core/InlineCode'; +import { trackEvent } from '../lib/analytics'; +import { fallbackRequestName } from '../lib/fallbackRequestName'; +import type { GrpcRequest } from '../lib/models'; +import { getGrpcRequest } from '../lib/store'; +import { useConfirm } from './useConfirm'; +import { grpcRequestsQueryKey } from './useGrpcRequests'; + +export function useDeleteAnyGrpcRequest() { + const queryClient = useQueryClient(); + const confirm = useConfirm(); + + return useMutation({ + mutationFn: async (id) => { + const request = await getGrpcRequest(id); + if (request == null) return null; + + const confirmed = await confirm({ + id: 'delete-grpc-request', + title: 'Delete Request', + variant: 'delete', + description: ( + <> + Permanently delete {fallbackRequestName(request)}? + + ), + }); + if (!confirmed) return null; + return invoke('cmd_delete_grpc_request', { requestId: id }); + }, + onSettled: () => trackEvent('GrpcRequest', 'Delete'), + onSuccess: async (request) => { + if (request === null) return; + + const { workspaceId, id: requestId } = request; + queryClient.setQueryData(grpcRequestsQueryKey({ workspaceId }), (requests) => + (requests ?? []).filter((r) => r.id !== requestId), + ); + }, + }); +} diff --git a/src-web/hooks/useDeleteAnyRequest.tsx b/src-web/hooks/useDeleteAnyHttpRequest.tsx similarity index 67% rename from src-web/hooks/useDeleteAnyRequest.tsx rename to src-web/hooks/useDeleteAnyHttpRequest.tsx index eb1f76bd..de3af626 100644 --- a/src-web/hooks/useDeleteAnyRequest.tsx +++ b/src-web/hooks/useDeleteAnyHttpRequest.tsx @@ -4,18 +4,20 @@ import { InlineCode } from '../components/core/InlineCode'; import { trackEvent } from '../lib/analytics'; import { fallbackRequestName } from '../lib/fallbackRequestName'; import type { HttpRequest } from '../lib/models'; -import { getRequest } from '../lib/store'; +import { getHttpRequest } from '../lib/store'; import { useConfirm } from './useConfirm'; -import { requestsQueryKey } from './useRequests'; -import { responsesQueryKey } from './useResponses'; +import { httpRequestsQueryKey } from './useHttpRequests'; +import { httpResponsesQueryKey } from './useHttpResponses'; -export function useDeleteAnyRequest() { +export function useDeleteAnyHttpRequest() { const queryClient = useQueryClient(); const confirm = useConfirm(); return useMutation({ mutationFn: async (id) => { - const request = await getRequest(id); + const request = await getHttpRequest(id); + if (request == null) return null; + const confirmed = await confirm({ id: 'delete-request', title: 'Delete Request', @@ -27,7 +29,7 @@ export function useDeleteAnyRequest() { ), }); if (!confirmed) return null; - return invoke('delete_request', { requestId: id }); + return invoke('cmd_delete_http_request', { requestId: id }); }, onSettled: () => trackEvent('HttpRequest', 'Delete'), onSuccess: async (request) => { @@ -35,8 +37,8 @@ export function useDeleteAnyRequest() { if (request === null) return; const { workspaceId, id: requestId } = request; - queryClient.setQueryData(responsesQueryKey({ requestId }), []); // Responses were deleted - queryClient.setQueryData(requestsQueryKey({ workspaceId }), (requests) => + queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []); // Responses were deleted + queryClient.setQueryData(httpRequestsQueryKey({ workspaceId }), (requests) => (requests ?? []).filter((r) => r.id !== requestId), ); }, diff --git a/src-web/hooks/useDeleteCookieJar.tsx b/src-web/hooks/useDeleteCookieJar.tsx index 0bcff2db..fc3bed00 100644 --- a/src-web/hooks/useDeleteCookieJar.tsx +++ b/src-web/hooks/useDeleteCookieJar.tsx @@ -23,7 +23,7 @@ export function useDeleteCookieJar(cookieJar: CookieJar | null) { ), }); if (!confirmed) return null; - return invoke('delete_cookie_jar', { cookieJarId: cookieJar?.id }); + return invoke('cmd_delete_cookie_jar', { cookieJarId: cookieJar?.id }); }, onSettled: () => trackEvent('CookieJar', 'Delete'), onSuccess: async (cookieJar) => { diff --git a/src-web/hooks/useDeleteEnvironment.tsx b/src-web/hooks/useDeleteEnvironment.tsx index b36f4910..982ed54b 100644 --- a/src-web/hooks/useDeleteEnvironment.tsx +++ b/src-web/hooks/useDeleteEnvironment.tsx @@ -23,7 +23,7 @@ export function useDeleteEnvironment(environment: Environment | null) { ), }); if (!confirmed) return null; - return invoke('delete_environment', { environmentId: environment?.id }); + return invoke('cmd_delete_environment', { environmentId: environment?.id }); }, onSettled: () => trackEvent('Environment', 'Delete'), onSuccess: async (environment) => { diff --git a/src-web/hooks/useDeleteFolder.tsx b/src-web/hooks/useDeleteFolder.tsx index 57219c34..377c9baa 100644 --- a/src-web/hooks/useDeleteFolder.tsx +++ b/src-web/hooks/useDeleteFolder.tsx @@ -6,7 +6,7 @@ import type { Folder } from '../lib/models'; import { getFolder } from '../lib/store'; import { useConfirm } from './useConfirm'; import { foldersQueryKey } from './useFolders'; -import { requestsQueryKey } from './useRequests'; +import { httpRequestsQueryKey } from './useHttpRequests'; export function useDeleteFolder(id: string | null) { const queryClient = useQueryClient(); @@ -26,7 +26,7 @@ export function useDeleteFolder(id: string | null) { ), }); if (!confirmed) return null; - return invoke('delete_folder', { folderId: id }); + return invoke('cmd_delete_folder', { folderId: id }); }, onSettled: () => trackEvent('Folder', 'Delete'), onSuccess: async (folder) => { @@ -36,7 +36,7 @@ export function useDeleteFolder(id: string | null) { const { workspaceId } = folder; // Nesting makes it hard to clean things up, so just clear everything that could have been deleted - await queryClient.invalidateQueries(requestsQueryKey({ workspaceId })); + await queryClient.invalidateQueries(httpRequestsQueryKey({ workspaceId })); await queryClient.invalidateQueries(foldersQueryKey({ workspaceId })); }, }); diff --git a/src-web/hooks/useDeleteGrpcConnection.ts b/src-web/hooks/useDeleteGrpcConnection.ts new file mode 100644 index 00000000..e01d8f23 --- /dev/null +++ b/src-web/hooks/useDeleteGrpcConnection.ts @@ -0,0 +1,21 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import { trackEvent } from '../lib/analytics'; +import type { GrpcConnection } from '../lib/models'; +import { grpcConnectionsQueryKey } from './useGrpcConnections'; + +export function useDeleteGrpcConnection(id: string | null) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async () => { + return await invoke('cmd_delete_grpc_connection', { id: id }); + }, + onSettled: () => trackEvent('GrpcConnection', 'Delete'), + onSuccess: ({ requestId, id: connectionId }) => { + queryClient.setQueryData( + grpcConnectionsQueryKey({ requestId }), + (connections) => (connections ?? []).filter((c) => c.id !== connectionId), + ); + }, + }); +} diff --git a/src-web/hooks/useDeleteGrpcConnections.ts b/src-web/hooks/useDeleteGrpcConnections.ts new file mode 100644 index 00000000..5440ba2b --- /dev/null +++ b/src-web/hooks/useDeleteGrpcConnections.ts @@ -0,0 +1,19 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import { trackEvent } from '../lib/analytics'; +import { grpcConnectionsQueryKey } from './useGrpcConnections'; + +export function useDeleteGrpcConnections(requestId?: string) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async () => { + if (requestId === undefined) return; + await invoke('cmd_delete_all_grpc_connections', { requestId }); + }, + onSettled: () => trackEvent('GrpcConnection', 'DeleteMany'), + onSuccess: async () => { + if (requestId === undefined) return; + queryClient.setQueryData(grpcConnectionsQueryKey({ requestId }), []); + }, + }); +} diff --git a/src-web/hooks/useDeleteResponse.ts b/src-web/hooks/useDeleteHttpResponse.ts similarity index 65% rename from src-web/hooks/useDeleteResponse.ts rename to src-web/hooks/useDeleteHttpResponse.ts index e3aa6572..3ee0bfee 100644 --- a/src-web/hooks/useDeleteResponse.ts +++ b/src-web/hooks/useDeleteHttpResponse.ts @@ -2,17 +2,17 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api'; import { trackEvent } from '../lib/analytics'; import type { HttpResponse } from '../lib/models'; -import { responsesQueryKey } from './useResponses'; +import { httpResponsesQueryKey } from './useHttpResponses'; -export function useDeleteResponse(id: string | null) { +export function useDeleteHttpResponse(id: string | null) { const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => { - return await invoke('delete_response', { id: id }); + return await invoke('cmd_delete_http_response', { id: id }); }, onSettled: () => trackEvent('HttpResponse', 'Delete'), onSuccess: ({ requestId, id: responseId }) => { - queryClient.setQueryData(responsesQueryKey({ requestId }), (responses) => + queryClient.setQueryData(httpResponsesQueryKey({ requestId }), (responses) => (responses ?? []).filter((response) => response.id !== responseId), ); }, diff --git a/src-web/hooks/useDeleteResponses.ts b/src-web/hooks/useDeleteHttpResponses.ts similarity index 63% rename from src-web/hooks/useDeleteResponses.ts rename to src-web/hooks/useDeleteHttpResponses.ts index 933cf1b3..264a87dd 100644 --- a/src-web/hooks/useDeleteResponses.ts +++ b/src-web/hooks/useDeleteHttpResponses.ts @@ -1,19 +1,19 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api'; import { trackEvent } from '../lib/analytics'; -import { responsesQueryKey } from './useResponses'; +import { httpResponsesQueryKey } from './useHttpResponses'; -export function useDeleteResponses(requestId?: string) { +export function useDeleteHttpResponses(requestId?: string) { const queryClient = useQueryClient(); return useMutation({ mutationFn: async () => { if (requestId === undefined) return; - await invoke('delete_all_responses', { requestId }); + await invoke('cmd_delete_all_http_responses', { requestId }); }, onSettled: () => trackEvent('HttpResponse', 'DeleteMany'), onSuccess: async () => { if (requestId === undefined) return; - queryClient.setQueryData(responsesQueryKey({ requestId }), []); + queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []); }, }); } diff --git a/src-web/hooks/useDeleteRequest.tsx b/src-web/hooks/useDeleteRequest.tsx index 5412d1f2..4f0240f3 100644 --- a/src-web/hooks/useDeleteRequest.tsx +++ b/src-web/hooks/useDeleteRequest.tsx @@ -1,9 +1,9 @@ import { useMutation } from '@tanstack/react-query'; import type { HttpRequest } from '../lib/models'; -import { useDeleteAnyRequest } from './useDeleteAnyRequest'; +import { useDeleteAnyHttpRequest } from './useDeleteAnyHttpRequest'; export function useDeleteRequest(id: string | null) { - const deleteAnyRequest = useDeleteAnyRequest(); + const deleteAnyRequest = useDeleteAnyHttpRequest(); return useMutation({ mutationFn: () => deleteAnyRequest.mutateAsync(id ?? 'n/a'), diff --git a/src-web/hooks/useDeleteWorkspace.tsx b/src-web/hooks/useDeleteWorkspace.tsx index 03e0a24d..36615228 100644 --- a/src-web/hooks/useDeleteWorkspace.tsx +++ b/src-web/hooks/useDeleteWorkspace.tsx @@ -6,7 +6,7 @@ import type { Workspace } from '../lib/models'; import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { useAppRoutes } from './useAppRoutes'; import { useConfirm } from './useConfirm'; -import { requestsQueryKey } from './useRequests'; +import { httpRequestsQueryKey } from './useHttpRequests'; import { workspacesQueryKey } from './useWorkspaces'; export function useDeleteWorkspace(workspace: Workspace | null) { @@ -28,7 +28,7 @@ export function useDeleteWorkspace(workspace: Workspace | null) { ), }); if (!confirmed) return null; - return invoke('delete_workspace', { workspaceId: workspace?.id }); + return invoke('cmd_delete_workspace', { workspaceId: workspace?.id }); }, onSettled: () => trackEvent('Workspace', 'Delete'), onSuccess: async (workspace) => { @@ -43,8 +43,8 @@ export function useDeleteWorkspace(workspace: Workspace | null) { } // Also clean up other things that may have been deleted - queryClient.setQueryData(requestsQueryKey({ workspaceId }), []); - await queryClient.invalidateQueries(requestsQueryKey({ workspaceId })); + queryClient.setQueryData(httpRequestsQueryKey({ workspaceId }), []); + await queryClient.invalidateQueries(httpRequestsQueryKey({ workspaceId })); }, }); } diff --git a/src-web/hooks/useDuplicateGrpcRequest.ts b/src-web/hooks/useDuplicateGrpcRequest.ts new file mode 100644 index 00000000..14efd4bb --- /dev/null +++ b/src-web/hooks/useDuplicateGrpcRequest.ts @@ -0,0 +1,41 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import { trackEvent } from '../lib/analytics'; +import type { GrpcRequest } from '../lib/models'; +import { useActiveEnvironmentId } from './useActiveEnvironmentId'; +import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useAppRoutes } from './useAppRoutes'; +import { grpcRequestsQueryKey } from './useGrpcRequests'; + +export function useDuplicateGrpcRequest({ + id, + navigateAfter, +}: { + id: string | null; + navigateAfter: boolean; +}) { + const activeWorkspaceId = useActiveWorkspaceId(); + const activeEnvironmentId = useActiveEnvironmentId(); + const routes = useAppRoutes(); + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async () => { + if (id === null) throw new Error("Can't duplicate a null grpc request"); + return invoke('cmd_duplicate_grpc_request', { id }); + }, + onSettled: () => trackEvent('GrpcRequest', 'Duplicate'), + onSuccess: async (request) => { + queryClient.setQueryData( + grpcRequestsQueryKey({ workspaceId: request.workspaceId }), + (requests) => [...(requests ?? []), request], + ); + if (navigateAfter && activeWorkspaceId !== null) { + routes.navigate('request', { + workspaceId: activeWorkspaceId, + requestId: request.id, + environmentId: activeEnvironmentId ?? undefined, + }); + } + }, + }); +} diff --git a/src-web/hooks/useDuplicateRequest.ts b/src-web/hooks/useDuplicateHttpRequest.ts similarity index 84% rename from src-web/hooks/useDuplicateRequest.ts rename to src-web/hooks/useDuplicateHttpRequest.ts index 16c32731..6e1f33f9 100644 --- a/src-web/hooks/useDuplicateRequest.ts +++ b/src-web/hooks/useDuplicateHttpRequest.ts @@ -5,9 +5,9 @@ import type { HttpRequest } from '../lib/models'; import { useActiveEnvironmentId } from './useActiveEnvironmentId'; import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { useAppRoutes } from './useAppRoutes'; -import { requestsQueryKey } from './useRequests'; +import { httpRequestsQueryKey } from './useHttpRequests'; -export function useDuplicateRequest({ +export function useDuplicateHttpRequest({ id, navigateAfter, }: { @@ -21,12 +21,12 @@ export function useDuplicateRequest({ return useMutation({ mutationFn: async () => { if (id === null) throw new Error("Can't duplicate a null request"); - return invoke('duplicate_request', { id }); + return invoke('cmd_duplicate_http_request', { id }); }, onSettled: () => trackEvent('HttpRequest', 'Duplicate'), onSuccess: async (request) => { queryClient.setQueryData( - requestsQueryKey({ workspaceId: request.workspaceId }), + httpRequestsQueryKey({ workspaceId: request.workspaceId }), (requests) => [...(requests ?? []), request], ); if (navigateAfter && activeWorkspaceId !== null) { diff --git a/src-web/hooks/useEnvironments.ts b/src-web/hooks/useEnvironments.ts index c3dba4c1..a7e709f9 100644 --- a/src-web/hooks/useEnvironments.ts +++ b/src-web/hooks/useEnvironments.ts @@ -15,7 +15,7 @@ export function useEnvironments() { queryKey: environmentsQueryKey({ workspaceId: workspaceId ?? 'n/a' }), queryFn: async () => { if (workspaceId == null) return []; - return (await invoke('list_environments', { workspaceId })) as Environment[]; + return (await invoke('cmd_list_environments', { workspaceId })) as Environment[]; }, }).data ?? [] ); diff --git a/src-web/hooks/useExportData.tsx b/src-web/hooks/useExportData.tsx index b6b7bb77..7dde2ab8 100644 --- a/src-web/hooks/useExportData.tsx +++ b/src-web/hooks/useExportData.tsx @@ -25,7 +25,7 @@ export function useExportData() { return; } - await invoke('export_data', { workspaceId: workspace.id, exportPath }); + await invoke('cmd_export_data', { workspaceId: workspace.id, exportPath }); }, }); } diff --git a/src-web/hooks/useFilterResponse.ts b/src-web/hooks/useFilterResponse.ts index 4ed9bf19..f9b1845b 100644 --- a/src-web/hooks/useFilterResponse.ts +++ b/src-web/hooks/useFilterResponse.ts @@ -16,7 +16,7 @@ export function useFilterResponse({ return null; } - return (await invoke('filter_response', { responseId, filter })) as string | null; + return (await invoke('cmd_filter_response', { responseId, filter })) as string | null; }, }).data ?? null ); diff --git a/src-web/hooks/useFolders.ts b/src-web/hooks/useFolders.ts index a1ace628..8bfb97c3 100644 --- a/src-web/hooks/useFolders.ts +++ b/src-web/hooks/useFolders.ts @@ -15,7 +15,7 @@ export function useFolders() { queryKey: foldersQueryKey({ workspaceId: workspaceId ?? 'n/a' }), queryFn: async () => { if (workspaceId == null) return []; - return (await invoke('list_folders', { workspaceId })) as Folder[]; + return (await invoke('cmd_list_folders', { workspaceId })) as Folder[]; }, }).data ?? [] ); diff --git a/src-web/hooks/useGrpc.ts b/src-web/hooks/useGrpc.ts new file mode 100644 index 00000000..de4ab684 --- /dev/null +++ b/src-web/hooks/useGrpc.ts @@ -0,0 +1,79 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import { emit } from '@tauri-apps/api/event'; +import { minPromiseMillis } from '../lib/minPromiseMillis'; +import type { GrpcConnection, GrpcMessage, GrpcRequest } from '../lib/models'; +import { useDebouncedValue } from './useDebouncedValue'; + +export interface ReflectResponseService { + name: string; + methods: { name: string; schema: string; serverStreaming: boolean; clientStreaming: boolean }[]; +} + +export function useGrpc(req: GrpcRequest | null, conn: GrpcConnection | null) { + const requestId = req?.id ?? 'n/a'; + + const unary = useMutation({ + mutationKey: ['grpc_unary', conn?.id ?? 'n/a'], + mutationFn: async () => + (await invoke('cmd_grpc_call_unary', { + requestId, + })) as GrpcMessage, + }); + + const clientStreaming = useMutation({ + mutationKey: ['grpc_client_streaming', conn?.id ?? 'n/a'], + mutationFn: async () => await invoke('cmd_grpc_client_streaming', { requestId }), + }); + + const serverStreaming = useMutation({ + mutationKey: ['grpc_server_streaming', conn?.id ?? 'n/a'], + mutationFn: async () => await invoke('cmd_grpc_server_streaming', { requestId }), + }); + + const streaming = useMutation({ + mutationKey: ['grpc_streaming', conn?.id ?? 'n/a'], + mutationFn: async () => await invoke('cmd_grpc_streaming', { requestId }), + }); + + const send = useMutation({ + mutationFn: async ({ message }: { message: string }) => + await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, { Message: message }), + }); + + const cancel = useMutation({ + mutationKey: ['grpc_cancel', conn?.id ?? 'n/a'], + mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Cancel'), + }); + + const commit = useMutation({ + mutationKey: ['grpc_commit', conn?.id ?? 'n/a'], + mutationFn: async () => await emit(`grpc_client_msg_${conn?.id ?? 'none'}`, 'Commit'), + }); + + const debouncedUrl = useDebouncedValue(req?.url ?? 'n/a', 1000); + const reflect = useQuery({ + enabled: req != null, + queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl], + refetchOnWindowFocus: false, + queryFn: async () => { + console.log('useGrpc.reflect', { requestId }); + return (await minPromiseMillis( + invoke('cmd_grpc_reflect', { requestId }), + 300, + )) as ReflectResponseService[]; + }, + }); + + return { + unary, + clientStreaming, + serverStreaming, + streaming, + reflect, + cancel, + commit, + isStreaming: conn?.elapsed === 0, + send, + }; +} diff --git a/src-web/hooks/useGrpcConnections.ts b/src-web/hooks/useGrpcConnections.ts new file mode 100644 index 00000000..85ce2770 --- /dev/null +++ b/src-web/hooks/useGrpcConnections.ts @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import type { GrpcConnection } from '../lib/models'; + +export function grpcConnectionsQueryKey({ requestId }: { requestId: string }) { + return ['grpc_connections', { requestId }]; +} + +export function useGrpcConnections(requestId: string | null) { + return ( + useQuery({ + enabled: requestId !== null, + initialData: [], + queryKey: grpcConnectionsQueryKey({ requestId: requestId ?? 'n/a' }), + queryFn: async () => { + return (await invoke('cmd_list_grpc_connections', { + requestId, + limit: 200, + })) as GrpcConnection[]; + }, + }).data ?? [] + ); +} diff --git a/src-web/hooks/useGrpcMessages.ts b/src-web/hooks/useGrpcMessages.ts new file mode 100644 index 00000000..d7c8f100 --- /dev/null +++ b/src-web/hooks/useGrpcMessages.ts @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import type { GrpcMessage } from '../lib/models'; + +export function grpcMessagesQueryKey({ connectionId }: { connectionId: string }) { + return ['grpc_messages', { connectionId }]; +} + +export function useGrpcMessages(connectionId: string | null) { + return ( + useQuery({ + enabled: connectionId !== null, + initialData: [], + queryKey: grpcMessagesQueryKey({ connectionId: connectionId ?? 'n/a' }), + queryFn: async () => { + return (await invoke('cmd_list_grpc_messages', { + connectionId, + limit: 200, + })) as GrpcMessage[]; + }, + }).data ?? [] + ); +} diff --git a/src-web/hooks/useGrpcRequest.ts b/src-web/hooks/useGrpcRequest.ts new file mode 100644 index 00000000..be0789c5 --- /dev/null +++ b/src-web/hooks/useGrpcRequest.ts @@ -0,0 +1,7 @@ +import type { GrpcRequest } from '../lib/models'; +import { useGrpcRequests } from './useGrpcRequests'; + +export function useGrpcRequest(id: string | null): GrpcRequest | null { + const requests = useGrpcRequests(); + return requests.find((r) => r.id === id) ?? null; +} diff --git a/src-web/hooks/useGrpcRequests.ts b/src-web/hooks/useGrpcRequests.ts new file mode 100644 index 00000000..0a699b67 --- /dev/null +++ b/src-web/hooks/useGrpcRequests.ts @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import type { GrpcRequest } from '../lib/models'; +import { useActiveWorkspaceId } from './useActiveWorkspaceId'; + +export function grpcRequestsQueryKey({ workspaceId }: { workspaceId: string }) { + return ['grpc_requests', { workspaceId }]; +} + +export function useGrpcRequests() { + const workspaceId = useActiveWorkspaceId(); + return ( + useQuery({ + enabled: workspaceId != null, + queryKey: grpcRequestsQueryKey({ workspaceId: workspaceId ?? 'n/a' }), + queryFn: async () => { + if (workspaceId == null) return []; + return (await invoke('cmd_list_grpc_requests', { workspaceId })) as GrpcRequest[]; + }, + }).data ?? [] + ); +} diff --git a/src-web/hooks/useHotKey.ts b/src-web/hooks/useHotKey.ts index 0796729d..b38aa303 100644 --- a/src-web/hooks/useHotKey.ts +++ b/src-web/hooks/useHotKey.ts @@ -5,44 +5,47 @@ import { debounce } from '../lib/debounce'; import { useOsInfo } from './useOsInfo'; export type HotkeyAction = - | 'request.send' - | 'request.create' - | 'request.duplicate' - | 'sidebar.toggle' - | 'sidebar.focus' - | 'urlBar.focus' | 'environmentEditor.toggle' | 'hotkeys.showHelp' - | 'requestSwitcher.prev' + | 'grpc_request.send' + | 'http_request.create' + | 'http_request.duplicate' + | 'http_request.send' | 'requestSwitcher.next' - | 'settings.show'; + | 'requestSwitcher.prev' + | 'settings.show' + | 'sidebar.focus' + | 'sidebar.toggle' + | 'urlBar.focus'; const hotkeys: Record = { - 'request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'], - 'request.create': ['CmdCtrl+n'], - 'request.duplicate': ['CmdCtrl+d'], - 'sidebar.toggle': ['CmdCtrl+b'], - 'sidebar.focus': ['CmdCtrl+1'], - 'urlBar.focus': ['CmdCtrl+l'], 'environmentEditor.toggle': ['CmdCtrl+Shift+e'], + 'grpc_request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'], 'hotkeys.showHelp': ['CmdCtrl+Shift+/'], - 'settings.show': ['CmdCtrl+,'], - 'requestSwitcher.prev': ['Control+Tab'], + 'http_request.create': ['CmdCtrl+n'], + 'http_request.duplicate': ['CmdCtrl+d'], + 'http_request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'], 'requestSwitcher.next': ['Control+Shift+Tab'], + 'requestSwitcher.prev': ['Control+Tab'], + 'settings.show': ['CmdCtrl+,'], + 'sidebar.focus': ['CmdCtrl+1'], + 'sidebar.toggle': ['CmdCtrl+b'], + 'urlBar.focus': ['CmdCtrl+l'], }; const hotkeyLabels: Record = { - 'request.send': 'Send Request', - 'request.create': 'New Request', - 'request.duplicate': 'Duplicate Request', - 'sidebar.toggle': 'Toggle Sidebar', - 'sidebar.focus': 'Focus Sidebar', - 'urlBar.focus': 'Focus URL', 'environmentEditor.toggle': 'Edit Environments', + 'grpc_request.send': 'Send Message', 'hotkeys.showHelp': 'Show Keyboard Shortcuts', - 'requestSwitcher.prev': 'Go To Next Request', + 'http_request.create': 'New Request', + 'http_request.duplicate': 'Duplicate Request', + 'http_request.send': 'Send Request', 'requestSwitcher.next': 'Go To Previous Request', + 'requestSwitcher.prev': 'Go To Next Request', 'settings.show': 'Open Settings', + 'sidebar.focus': 'Focus Sidebar', + 'sidebar.toggle': 'Toggle Sidebar', + 'urlBar.focus': 'Focus URL', }; export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[]; @@ -109,11 +112,11 @@ export function useAnyHotkey( } currentKeys.current.delete(normalizeKey(e.key, os)); }; - window.addEventListener('keydown', down); - window.addEventListener('keyup', up); + document.addEventListener('keydown', down, { capture: true }); + document.addEventListener('keyup', up, { capture: true }); return () => { - window.removeEventListener('keydown', down); - window.removeEventListener('keyup', up); + document.removeEventListener('keydown', down, { capture: true }); + document.removeEventListener('keyup', up, { capture: true }); }; }, [options.enable, os]); } diff --git a/src-web/hooks/useHttpRequest.ts b/src-web/hooks/useHttpRequest.ts new file mode 100644 index 00000000..c0b072ea --- /dev/null +++ b/src-web/hooks/useHttpRequest.ts @@ -0,0 +1,7 @@ +import type { HttpRequest } from '../lib/models'; +import { useHttpRequests } from './useHttpRequests'; + +export function useHttpRequest(id: string | null): HttpRequest | null { + const requests = useHttpRequests(); + return requests.find((r) => r.id === id) ?? null; +} diff --git a/src-web/hooks/useRequests.ts b/src-web/hooks/useHttpRequests.ts similarity index 62% rename from src-web/hooks/useRequests.ts rename to src-web/hooks/useHttpRequests.ts index c5344ee0..a5896dfa 100644 --- a/src-web/hooks/useRequests.ts +++ b/src-web/hooks/useHttpRequests.ts @@ -3,19 +3,19 @@ import { invoke } from '@tauri-apps/api'; import type { HttpRequest } from '../lib/models'; import { useActiveWorkspaceId } from './useActiveWorkspaceId'; -export function requestsQueryKey({ workspaceId }: { workspaceId: string }) { +export function httpRequestsQueryKey({ workspaceId }: { workspaceId: string }) { return ['http_requests', { workspaceId }]; } -export function useRequests() { +export function useHttpRequests() { const workspaceId = useActiveWorkspaceId(); return ( useQuery({ enabled: workspaceId != null, - queryKey: requestsQueryKey({ workspaceId: workspaceId ?? 'n/a' }), + queryKey: httpRequestsQueryKey({ workspaceId: workspaceId ?? 'n/a' }), queryFn: async () => { if (workspaceId == null) return []; - return (await invoke('list_requests', { workspaceId })) as HttpRequest[]; + return (await invoke('cmd_list_http_requests', { workspaceId })) as HttpRequest[]; }, }).data ?? [] ); diff --git a/src-web/hooks/useResponses.ts b/src-web/hooks/useHttpResponses.ts similarity index 52% rename from src-web/hooks/useResponses.ts rename to src-web/hooks/useHttpResponses.ts index cd914147..8d8b9173 100644 --- a/src-web/hooks/useResponses.ts +++ b/src-web/hooks/useHttpResponses.ts @@ -2,18 +2,18 @@ import { useQuery } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api'; import type { HttpResponse } from '../lib/models'; -export function responsesQueryKey({ requestId }: { requestId: string }) { +export function httpResponsesQueryKey({ requestId }: { requestId: string }) { return ['http_responses', { requestId }]; } -export function useResponses(requestId: string | null) { +export function useHttpResponses(requestId: string | null) { return ( useQuery({ enabled: requestId !== null, initialData: [], - queryKey: responsesQueryKey({ requestId: requestId ?? 'n/a' }), + queryKey: httpResponsesQueryKey({ requestId: requestId ?? 'n/a' }), queryFn: async () => { - return (await invoke('list_responses', { requestId, limit: 200 })) as HttpResponse[]; + return (await invoke('cmd_list_http_responses', { requestId, limit: 200 })) as HttpResponse[]; }, }).data ?? [] ); diff --git a/src-web/hooks/useImportData.tsx b/src-web/hooks/useImportData.tsx index 24b5397a..ce29afcd 100644 --- a/src-web/hooks/useImportData.tsx +++ b/src-web/hooks/useImportData.tsx @@ -35,7 +35,7 @@ export function useImportData() { environments: Environment[]; folders: Folder[]; requests: HttpRequest[]; - } = await invoke('import_data', { + } = await invoke('cmd_import_data', { filePaths: Array.isArray(selected) ? selected : [selected], }); const importedWorkspace = imported.workspaces[0]; diff --git a/src-web/hooks/useIsResponseLoading.ts b/src-web/hooks/useIsResponseLoading.ts index 56d8b33d..4fd86abf 100644 --- a/src-web/hooks/useIsResponseLoading.ts +++ b/src-web/hooks/useIsResponseLoading.ts @@ -1,8 +1,8 @@ import { isResponseLoading } from '../lib/models'; -import { useLatestResponse } from './useLatestResponse'; +import { useLatestHttpResponse } from './useLatestHttpResponse'; export function useIsResponseLoading(requestId: string | null): boolean { - const response = useLatestResponse(requestId); + const response = useLatestHttpResponse(requestId); if (response === null) return false; return isResponseLoading(response); } diff --git a/src-web/hooks/useKeyValue.ts b/src-web/hooks/useKeyValue.ts index 3a2b0d56..bdec4cab 100644 --- a/src-web/hooks/useKeyValue.ts +++ b/src-web/hooks/useKeyValue.ts @@ -28,6 +28,7 @@ export function useKeyValue({ const query = useQuery({ queryKey: keyValueQueryKey({ namespace, key }), queryFn: async () => getKeyValue({ namespace, key, fallback: defaultValue }), + refetchOnWindowFocus: false, }); const mutate = useMutation({ @@ -37,19 +38,21 @@ export function useKeyValue({ }); const set = useCallback( - (value: ((v: T) => T) | T) => { + async (value: ((v: T) => T) | T) => { if (typeof value === 'function') { - getKeyValue({ namespace, key, fallback: defaultValue }).then((kv) => { - mutate.mutate(value(kv)); + await getKeyValue({ namespace, key, fallback: defaultValue }).then((kv) => { + const newV = value(kv); + if (newV === kv) return; + return mutate.mutateAsync(newV); }); - } else { - mutate.mutate(value); + } else if (value !== query.data) { + await mutate.mutateAsync(value); } }, - [defaultValue, key, mutate, namespace], + [defaultValue, key, mutate, namespace, query.data], ); - const reset = useCallback(() => mutate.mutate(defaultValue), [mutate, defaultValue]); + const reset = useCallback(async () => mutate.mutateAsync(defaultValue), [mutate, defaultValue]); return useMemo( () => ({ diff --git a/src-web/hooks/useLatestGrpcConnection.ts b/src-web/hooks/useLatestGrpcConnection.ts new file mode 100644 index 00000000..a139c71c --- /dev/null +++ b/src-web/hooks/useLatestGrpcConnection.ts @@ -0,0 +1,7 @@ +import type { GrpcConnection } from '../lib/models'; +import { useGrpcConnections } from './useGrpcConnections'; + +export function useLatestGrpcConnection(requestId: string | null): GrpcConnection | null { + const connections = useGrpcConnections(requestId); + return connections[0] ?? null; +} diff --git a/src-web/hooks/useLatestHttpResponse.ts b/src-web/hooks/useLatestHttpResponse.ts new file mode 100644 index 00000000..085aab80 --- /dev/null +++ b/src-web/hooks/useLatestHttpResponse.ts @@ -0,0 +1,7 @@ +import type { HttpResponse } from '../lib/models'; +import { useHttpResponses } from './useHttpResponses'; + +export function useLatestHttpResponse(requestId: string | null): HttpResponse | null { + const responses = useHttpResponses(requestId); + return responses[0] ?? null; +} diff --git a/src-web/hooks/useLatestResponse.ts b/src-web/hooks/useLatestResponse.ts deleted file mode 100644 index 9f802715..00000000 --- a/src-web/hooks/useLatestResponse.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { HttpResponse } from '../lib/models'; -import { useResponses } from './useResponses'; - -export function useLatestResponse(requestId: string | null): HttpResponse | null { - const responses = useResponses(requestId); - return responses[0] ?? null; -} diff --git a/src-web/hooks/useRequest.ts b/src-web/hooks/useRequest.ts deleted file mode 100644 index fe0f717a..00000000 --- a/src-web/hooks/useRequest.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { HttpRequest } from '../lib/models'; -import { useRequests } from './useRequests'; - -export function useRequest(id: string | null): HttpRequest | null { - const requests = useRequests(); - return requests.find((r) => r.id === id) ?? null; -} diff --git a/src-web/hooks/useSendAnyRequest.ts b/src-web/hooks/useSendAnyRequest.ts index 58230bee..67dd75f7 100644 --- a/src-web/hooks/useSendAnyRequest.ts +++ b/src-web/hooks/useSendAnyRequest.ts @@ -4,7 +4,7 @@ import { save } from '@tauri-apps/api/dialog'; import slugify from 'slugify'; import { trackEvent } from '../lib/analytics'; import type { HttpResponse } from '../lib/models'; -import { getRequest } from '../lib/store'; +import { getHttpRequest } from '../lib/store'; import { useActiveCookieJar } from './useActiveCookieJar'; import { useActiveEnvironmentId } from './useActiveEnvironmentId'; import { useAlert } from './useAlert'; @@ -15,7 +15,7 @@ export function useSendAnyRequest(options: { download?: boolean } = {}) { const { activeCookieJar } = useActiveCookieJar(); return useMutation({ mutationFn: async (id) => { - const request = await getRequest(id); + const request = await getHttpRequest(id); if (request == null) { return null; } @@ -31,7 +31,7 @@ export function useSendAnyRequest(options: { download?: boolean } = {}) { } } - return invoke('send_request', { + return invoke('cmd_send_request', { requestId: id, environmentId, downloadDir: downloadDir, diff --git a/src-web/hooks/useSettings.ts b/src-web/hooks/useSettings.ts index de58ad10..ca521694 100644 --- a/src-web/hooks/useSettings.ts +++ b/src-web/hooks/useSettings.ts @@ -11,7 +11,7 @@ export function useSettings() { useQuery({ queryKey: settingsQueryKey(), queryFn: async () => { - return (await invoke('get_settings')) as Settings; + return (await invoke('cmd_get_settings')) as Settings; }, }).data ?? undefined ); diff --git a/src-web/hooks/useUpdateAnyFolder.ts b/src-web/hooks/useUpdateAnyFolder.ts index b093f2b6..7aad7048 100644 --- a/src-web/hooks/useUpdateAnyFolder.ts +++ b/src-web/hooks/useUpdateAnyFolder.ts @@ -14,7 +14,7 @@ export function useUpdateAnyFolder() { throw new Error("Can't update a null folder"); } - await invoke('update_folder', { folder: update(folder) }); + await invoke('cmd_update_folder', { folder: update(folder) }); }, onMutate: async ({ id, update }) => { const folder = await getFolder(id); diff --git a/src-web/hooks/useUpdateAnyGrpcRequest.ts b/src-web/hooks/useUpdateAnyGrpcRequest.ts new file mode 100644 index 00000000..2c8caa85 --- /dev/null +++ b/src-web/hooks/useUpdateAnyGrpcRequest.ts @@ -0,0 +1,36 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { invoke } from '@tauri-apps/api'; +import type { GrpcRequest } from '../lib/models'; +import { sleep } from '../lib/sleep'; +import { getGrpcRequest } from '../lib/store'; +import { grpcRequestsQueryKey } from './useGrpcRequests'; + +export function useUpdateAnyGrpcRequest() { + const queryClient = useQueryClient(); + + return useMutation< + void, + unknown, + { id: string; update: Partial | ((r: GrpcRequest) => GrpcRequest) } + >({ + mutationFn: async ({ id, update }) => { + const request = await getGrpcRequest(id); + if (request === null) { + throw new Error("Can't update a null request"); + } + + const patchedRequest = + typeof update === 'function' ? update(request) : { ...request, ...update }; + await invoke('cmd_update_grpc_request', { request: patchedRequest }); + }, + onMutate: async ({ id, update }) => { + const request = await getGrpcRequest(id); + if (request === null) return; + const patchedRequest = + typeof update === 'function' ? update(request) : { ...request, ...update }; + queryClient.setQueryData(grpcRequestsQueryKey(request), (requests) => + (requests ?? []).map((r) => (r.id === patchedRequest.id ? patchedRequest : r)), + ); + }, + }); +} diff --git a/src-web/hooks/useUpdateAnyRequest.ts b/src-web/hooks/useUpdateAnyHttpRequest.ts similarity index 68% rename from src-web/hooks/useUpdateAnyRequest.ts rename to src-web/hooks/useUpdateAnyHttpRequest.ts index 7a4f447f..08425fa0 100644 --- a/src-web/hooks/useUpdateAnyRequest.ts +++ b/src-web/hooks/useUpdateAnyHttpRequest.ts @@ -1,10 +1,10 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api'; import type { HttpRequest } from '../lib/models'; -import { getRequest } from '../lib/store'; -import { requestsQueryKey } from './useRequests'; +import { getHttpRequest } from '../lib/store'; +import { httpRequestsQueryKey } from './useHttpRequests'; -export function useUpdateAnyRequest() { +export function useUpdateAnyHttpRequest() { const queryClient = useQueryClient(); return useMutation< @@ -13,21 +13,21 @@ export function useUpdateAnyRequest() { { id: string; update: Partial | ((r: HttpRequest) => HttpRequest) } >({ mutationFn: async ({ id, update }) => { - const request = await getRequest(id); + const request = await getHttpRequest(id); if (request === null) { throw new Error("Can't update a null request"); } const patchedRequest = typeof update === 'function' ? update(request) : { ...request, ...update }; - await invoke('update_request', { request: patchedRequest }); + await invoke('cmd_update_http_request', { request: patchedRequest }); }, onMutate: async ({ id, update }) => { - const request = await getRequest(id); + const request = await getHttpRequest(id); if (request === null) return; const patchedRequest = typeof update === 'function' ? update(request) : { ...request, ...update }; - queryClient.setQueryData(requestsQueryKey(request), (requests) => + queryClient.setQueryData(httpRequestsQueryKey(request), (requests) => (requests ?? []).map((r) => (r.id === patchedRequest.id ? patchedRequest : r)), ); }, diff --git a/src-web/hooks/useUpdateCookieJar.ts b/src-web/hooks/useUpdateCookieJar.ts index f5406ec8..0a899363 100644 --- a/src-web/hooks/useUpdateCookieJar.ts +++ b/src-web/hooks/useUpdateCookieJar.ts @@ -15,7 +15,7 @@ export function useUpdateCookieJar(id: string | null) { const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v }; console.log('NEW COOKIE JAR', newCookieJar.cookies.length); - await invoke('update_cookie_jar', { cookieJar: newCookieJar }); + await invoke('cmd_update_cookie_jar', { cookieJar: newCookieJar }); }, onMutate: async (v) => { const cookieJar = await getCookieJar(id); diff --git a/src-web/hooks/useUpdateEnvironment.ts b/src-web/hooks/useUpdateEnvironment.ts index 0c95a11a..6c984e83 100644 --- a/src-web/hooks/useUpdateEnvironment.ts +++ b/src-web/hooks/useUpdateEnvironment.ts @@ -14,7 +14,7 @@ export function useUpdateEnvironment(id: string | null) { } const newEnvironment = typeof v === 'function' ? v(environment) : { ...environment, ...v }; - await invoke('update_environment', { environment: newEnvironment }); + await invoke('cmd_update_environment', { environment: newEnvironment }); }, onMutate: async (v) => { const environment = await getEnvironment(id); diff --git a/src-web/hooks/useUpdateGrpcRequest.ts b/src-web/hooks/useUpdateGrpcRequest.ts new file mode 100644 index 00000000..4f05f14e --- /dev/null +++ b/src-web/hooks/useUpdateGrpcRequest.ts @@ -0,0 +1,12 @@ +import { useMutation } from '@tanstack/react-query'; +import type { GrpcRequest } from '../lib/models'; +import { useUpdateAnyGrpcRequest } from './useUpdateAnyGrpcRequest'; + +export function useUpdateGrpcRequest(id: string | null) { + const updateAnyGrpcRequest = useUpdateAnyGrpcRequest(); + return useMutation | ((r: GrpcRequest) => GrpcRequest)>({ + mutationFn: async (update) => { + return updateAnyGrpcRequest.mutateAsync({ id: id ?? 'n/a', update }); + }, + }); +} diff --git a/src-web/hooks/useUpdateRequest.ts b/src-web/hooks/useUpdateHttpRequest.ts similarity index 62% rename from src-web/hooks/useUpdateRequest.ts rename to src-web/hooks/useUpdateHttpRequest.ts index 6c6b82d6..3a4faee7 100644 --- a/src-web/hooks/useUpdateRequest.ts +++ b/src-web/hooks/useUpdateHttpRequest.ts @@ -1,9 +1,9 @@ import { useMutation } from '@tanstack/react-query'; import type { HttpRequest } from '../lib/models'; -import { useUpdateAnyRequest } from './useUpdateAnyRequest'; +import { useUpdateAnyHttpRequest } from './useUpdateAnyHttpRequest'; -export function useUpdateRequest(id: string | null) { - const updateAnyRequest = useUpdateAnyRequest(); +export function useUpdateHttpRequest(id: string | null) { + const updateAnyRequest = useUpdateAnyHttpRequest(); return useMutation | ((r: HttpRequest) => HttpRequest)>({ mutationFn: async (update) => updateAnyRequest.mutateAsync({ id: id ?? 'n/a', update }), }); diff --git a/src-web/hooks/useUpdateSettings.ts b/src-web/hooks/useUpdateSettings.ts index 8431a84a..a3ee7bd6 100644 --- a/src-web/hooks/useUpdateSettings.ts +++ b/src-web/hooks/useUpdateSettings.ts @@ -8,7 +8,7 @@ export function useUpdateSettings() { return useMutation({ mutationFn: async (settings) => { - await invoke('update_settings', { settings }); + await invoke('cmd_update_settings', { settings }); }, onMutate: async (settings) => { queryClient.setQueryData(settingsQueryKey(), settings); diff --git a/src-web/hooks/useUpdateWorkspace.ts b/src-web/hooks/useUpdateWorkspace.ts index 3b71ee00..92d86b22 100644 --- a/src-web/hooks/useUpdateWorkspace.ts +++ b/src-web/hooks/useUpdateWorkspace.ts @@ -14,7 +14,7 @@ export function useUpdateWorkspace(id: string | null) { } const newWorkspace = typeof v === 'function' ? v(workspace) : { ...workspace, ...v }; - await invoke('update_workspace', { workspace: newWorkspace }); + await invoke('cmd_update_workspace', { workspace: newWorkspace }); }, onMutate: async (v) => { const workspace = await getWorkspace(id); diff --git a/src-web/hooks/useVariables.ts b/src-web/hooks/useVariables.ts index 0a9cfaa8..4b0d8504 100644 --- a/src-web/hooks/useVariables.ts +++ b/src-web/hooks/useVariables.ts @@ -11,7 +11,7 @@ export function useVariables({ environmentId }: { environmentId: string }) { useQuery({ queryKey: variablesQueryKey({ environmentId }), queryFn: async () => { - return (await invoke('list_variables', { environmentId })) as EnvironmentVariable[]; + return (await invoke('cmd_list_variables', { environmentId })) as EnvironmentVariable[]; }, }).data ?? [] ); diff --git a/src-web/hooks/useWorkspaces.ts b/src-web/hooks/useWorkspaces.ts index 4e09cbac..e8d05df9 100644 --- a/src-web/hooks/useWorkspaces.ts +++ b/src-web/hooks/useWorkspaces.ts @@ -10,7 +10,7 @@ export function workspacesQueryKey(_?: {}) { export function useWorkspaces() { return ( useQuery(workspacesQueryKey(), async () => { - return (await invoke('list_workspaces')) as Workspace[]; + return (await invoke('cmd_list_workspaces')) as Workspace[]; }).data ?? [] ); } diff --git a/src-web/lib/analytics.ts b/src-web/lib/analytics.ts index 2c1504b6..0865a0cf 100644 --- a/src-web/lib/analytics.ts +++ b/src-web/lib/analytics.ts @@ -9,6 +9,9 @@ export function trackEvent( | 'Workspace' | 'Environment' | 'Folder' + | 'GrpcMessage' + | 'GrpcConnection' + | 'GrpcRequest' | 'HttpRequest' | 'HttpResponse' | 'KeyValue', @@ -25,7 +28,7 @@ export function trackEvent( | 'Duplicate', attributes: Record = {}, ) { - invoke('track_event', { + invoke('cmd_track_event', { resource: resource, action, attributes, diff --git a/src-web/lib/fallbackRequestName.ts b/src-web/lib/fallbackRequestName.ts index 6746377b..712df566 100644 --- a/src-web/lib/fallbackRequestName.ts +++ b/src-web/lib/fallbackRequestName.ts @@ -1,6 +1,6 @@ -import type { HttpRequest } from './models'; +import type { GrpcRequest, HttpRequest } from './models'; -export function fallbackRequestName(r: HttpRequest | null): string { +export function fallbackRequestName(r: HttpRequest | GrpcRequest | null): string { if (r == null) return ''; if (r.name) { @@ -9,17 +9,22 @@ export function fallbackRequestName(r: HttpRequest | null): string { const withoutVariables = r.url.replace(/\$\{\[[^\]]+]}/g, ''); if (withoutVariables.trim() === '') { - return 'New Request'; + return r.model === 'http_request' ? 'New HTTP Request' : 'new gRPC Request'; } const fixedUrl = r.url.match(/^https?:\/\//) ? r.url : 'http://' + r.url; - try { - const url = new URL(fixedUrl); - const pathname = url.pathname === '/' ? '' : url.pathname; - return `${url.host}${pathname}`; - } catch (_) { - // Nothing + if (r.model === 'grpc_request' && r.service != null && r.method != null) { + const shortService = r.service.split('.').pop(); + return `${shortService}/${r.method}`; + } else { + try { + const url = new URL(fixedUrl); + const pathname = url.pathname === '/' ? '' : url.pathname; + return `${url.host}${pathname}`; + } catch (_) { + // Nothing + } } return r.url; diff --git a/src-web/lib/formatters.ts b/src-web/lib/formatters.ts index d59ca206..18e8f2dd 100644 --- a/src-web/lib/formatters.ts +++ b/src-web/lib/formatters.ts @@ -1,6 +1,7 @@ -export function tryFormatJson(text: string): string { +export function tryFormatJson(text: string, pretty = true): string { try { - return JSON.stringify(JSON.parse(text), null, 2); + if (pretty) return JSON.stringify(JSON.parse(text), null, 2); + else return JSON.stringify(JSON.parse(text)); } catch (_) { return text; } diff --git a/src-web/lib/keyValueStore.ts b/src-web/lib/keyValueStore.ts index 69235d91..76269c62 100644 --- a/src-web/lib/keyValueStore.ts +++ b/src-web/lib/keyValueStore.ts @@ -14,7 +14,7 @@ export async function setKeyValue({ key: string | string[]; value: T; }): Promise { - await invoke('set_key_value', { + await invoke('cmd_set_key_value', { namespace, key: buildKeyValueKey(key), value: JSON.stringify(value), @@ -30,7 +30,7 @@ export async function getKeyValue({ key: string | string[]; fallback: T; }) { - const kv = (await invoke('get_key_value', { + const kv = (await invoke('cmd_get_key_value', { namespace, key: buildKeyValueKey(key), })) as KeyValue | null; diff --git a/src-web/lib/minPromiseMillis.ts b/src-web/lib/minPromiseMillis.ts index 8287a429..c9272a50 100644 --- a/src-web/lib/minPromiseMillis.ts +++ b/src-web/lib/minPromiseMillis.ts @@ -1,9 +1,19 @@ import { sleep } from './sleep'; +/** Ensures a promise takes at least a certain number of milliseconds to resolve */ export async function minPromiseMillis(promise: Promise, millis: number) { const start = Date.now(); - const result = await promise; + let result; + let err; + + try { + result = await promise; + } catch (e) { + err = e; + } + const delayFor = millis - (Date.now() - start); await sleep(delayFor); - return result; + if (err) throw err; + else return result; } diff --git a/src-web/lib/models.ts b/src-web/lib/models.ts index 42dab3ba..56acf0e7 100644 --- a/src-web/lib/models.ts +++ b/src-web/lib/models.ts @@ -12,6 +12,9 @@ export const AUTH_TYPE_BEARER = 'bearer'; export type Model = | Settings | Workspace + | GrpcConnection + | GrpcRequest + | GrpcMessage | HttpRequest | HttpResponse | KeyValue @@ -101,6 +104,38 @@ export interface HttpUrlParameter { enabled?: boolean; } +export interface GrpcRequest extends BaseModel { + readonly workspaceId: string; + readonly model: 'grpc_request'; + folderId: string | null; + sortPriority: number; + name: string; + url: string; + service: string | null; + method: string | null; + message: string; + protoFiles: string[]; +} + +export interface GrpcMessage extends BaseModel { + readonly workspaceId: string; + readonly requestId: string; + readonly connectionId: string; + readonly model: 'grpc_message'; + message: string; + isServer: boolean; + isInfo: boolean; +} + +export interface GrpcConnection extends BaseModel { + readonly workspaceId: string; + readonly requestId: string; + readonly model: 'grpc_connection'; + service: string; + method: string; + elapsed: number; +} + export interface HttpRequest extends BaseModel { readonly workspaceId: string; readonly model: 'http_request'; diff --git a/src-web/lib/sendEphemeralRequest.ts b/src-web/lib/sendEphemeralRequest.ts index daa4a909..878c6818 100644 --- a/src-web/lib/sendEphemeralRequest.ts +++ b/src-web/lib/sendEphemeralRequest.ts @@ -7,5 +7,5 @@ export async function sendEphemeralRequest( ): Promise { // Remove some things that we don't want to associate const newRequest = { ...request }; - return invoke('send_ephemeral_request', { request: newRequest, environmentId }); + return invoke('cmd_send_ephemeral_request', { request: newRequest, environmentId }); } diff --git a/src-web/lib/store.ts b/src-web/lib/store.ts index dd4f29df..a8967b6b 100644 --- a/src-web/lib/store.ts +++ b/src-web/lib/store.ts @@ -1,13 +1,30 @@ import { invoke } from '@tauri-apps/api'; -import type { CookieJar, Environment, Folder, HttpRequest, Settings, Workspace } from './models'; +import type { + CookieJar, + Environment, + Folder, + GrpcRequest, + HttpRequest, + Settings, + Workspace, +} from './models'; export async function getSettings(): Promise { - return invoke('get_settings', {}); + return invoke('cmd_get_settings', {}); } -export async function getRequest(id: string | null): Promise { +export async function getGrpcRequest(id: string | null): Promise { if (id === null) return null; - const request: HttpRequest = (await invoke('get_request', { id })) ?? null; + const request: GrpcRequest = (await invoke('cmd_get_grpc_request', { id })) ?? null; + if (request == null) { + return null; + } + return request; +} + +export async function getHttpRequest(id: string | null): Promise { + if (id === null) return null; + const request: HttpRequest = (await invoke('cmd_get_http_request', { id })) ?? null; if (request == null) { return null; } @@ -16,7 +33,7 @@ export async function getRequest(id: string | null): Promise export async function getEnvironment(id: string | null): Promise { if (id === null) return null; - const environment: Environment = (await invoke('get_environment', { id })) ?? null; + const environment: Environment = (await invoke('cmd_get_environment', { id })) ?? null; if (environment == null) { return null; } @@ -25,7 +42,7 @@ export async function getEnvironment(id: string | null): Promise { if (id === null) return null; - const folder: Folder = (await invoke('get_folder', { id })) ?? null; + const folder: Folder = (await invoke('cmd_get_folder', { id })) ?? null; if (folder == null) { return null; } @@ -34,7 +51,7 @@ export async function getFolder(id: string | null): Promise { export async function getWorkspace(id: string | null): Promise { if (id === null) return null; - const workspace: Workspace = (await invoke('get_workspace', { id })) ?? null; + const workspace: Workspace = (await invoke('cmd_get_workspace', { id })) ?? null; if (workspace == null) { return null; } @@ -43,7 +60,7 @@ export async function getWorkspace(id: string | null): Promise export async function getCookieJar(id: string | null): Promise { if (id === null) return null; - const cookieJar: CookieJar = (await invoke('get_cookie_jar', { id })) ?? null; + const cookieJar: CookieJar = (await invoke('cmd_get_cookie_jar', { id })) ?? null; if (cookieJar == null) { return null; } diff --git a/src-web/main.css b/src-web/main.css index 2eb0a0e2..cf0c5588 100644 --- a/src-web/main.css +++ b/src-web/main.css @@ -28,6 +28,15 @@ @apply select-none cursor-default; } + a, + a * { + @apply cursor-pointer !important; + } + + table th { + @apply text-left; + } + .hide-scrollbars { &::-webkit-scrollbar-corner, &::-webkit-scrollbar { diff --git a/src-web/main.tsx b/src-web/main.tsx index 0819069d..899910d7 100644 --- a/src-web/main.tsx +++ b/src-web/main.tsx @@ -1,14 +1,12 @@ +import { type } from '@tauri-apps/api/os'; +import { appWindow } from '@tauri-apps/api/window'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import { attachConsole } from 'tauri-plugin-log-api'; import { App } from './components/App'; -import { maybeRestorePathname } from './lib/persistPathname'; import './main.css'; import { getSettings } from './lib/store'; import type { Appearance } from './lib/theme/window'; import { setAppearanceOnDocument } from './lib/theme/window'; -import { appWindow } from '@tauri-apps/api/window'; -import { type } from '@tauri-apps/api/os'; // Hide decorations here because it doesn't work in Rust for some reason (bug?) const osType = await type(); @@ -16,8 +14,8 @@ if (osType !== 'Darwin') { await appWindow.setDecorations(false); } -await attachConsole(); -await maybeRestorePathname(); +// await attachConsole(); +// await maybeRestorePathname(); const settings = await getSettings(); setAppearanceOnDocument(settings.appearance as Appearance); diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 77ee621a..dbc74c03 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -46,8 +46,9 @@ module.exports = { ], }, fontSize: { - '3xs': '0.6rem', - '2xs': '0.7rem', + '4xs': '0.6rem', + '3xs': '0.675rem', + '2xs': '0.75rem', xs: '0.8rem', sm: '0.9rem', base: '1rem',