From c941cb69899feed851eb068601c18920eff9550a Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 13 Apr 2023 18:48:40 -0700 Subject: [PATCH] Support binary responses! --- package-lock.json | 81 ++++++ package.json | 1 + src-tauri/Cargo.toml | 2 +- .../20230413232435_response-body-blob.sql | 5 + src-tauri/sqlx-data.json | 260 ++++++++++-------- src-tauri/src/main.rs | 31 ++- src-tauri/src/models.rs | 48 +++- src-tauri/tauri.conf.json | 11 +- src-web/components/ImageView.tsx | 9 - src-web/components/ResponsePane.tsx | 88 ++---- src-web/components/RouteError.tsx | 9 +- src-web/components/Sidebar.tsx | 4 +- src-web/components/UrlBar.tsx | 1 - .../components/WorkspaceActionsDropdown.tsx | 4 +- src-web/components/core/Editor/Editor.tsx | 4 +- src-web/components/core/IconButton.tsx | 2 +- .../responseViewers/ImageViewer.tsx | 15 + .../components/responseViewers/TextViewer.tsx | 26 ++ .../WebPageViewer.tsx} | 14 +- src-web/hooks/useIntrospectGraphQL.ts | 4 + src-web/hooks/useResponseBodyBlob.ts | 20 ++ src-web/hooks/useResponseBodyText.ts | 21 ++ src-web/hooks/useResponseContentType.ts | 9 + src-web/hooks/useResponseViewMode.ts | 13 +- src-web/lib/models.ts | 4 +- 25 files changed, 455 insertions(+), 231 deletions(-) create mode 100644 src-tauri/migrations/20230413232435_response-body-blob.sql delete mode 100644 src-web/components/ImageView.tsx create mode 100644 src-web/components/responseViewers/ImageViewer.tsx create mode 100644 src-web/components/responseViewers/TextViewer.tsx rename src-web/components/{core/Webview.tsx => responseViewers/WebPageViewer.tsx} (63%) create mode 100644 src-web/hooks/useResponseBodyBlob.ts create mode 100644 src-web/hooks/useResponseBodyText.ts create mode 100644 src-web/hooks/useResponseContentType.ts diff --git a/package-lock.json b/package-lock.json index d8eea4a7..bd551272 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@tanstack/react-query-persist-client": "^4.28.0", "@tauri-apps/api": "^1.2.0", "@vitejs/plugin-react": "^3.1.0", + "buffer": "^6.0.3", "classnames": "^2.3.2", "cm6-graphql": "^0.0.4-canary-b30a2325.0", "codemirror": "^6.0.1", @@ -2820,6 +2821,25 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2876,6 +2896,29 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -4809,6 +4852,25 @@ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -9325,6 +9387,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -9359,6 +9426,15 @@ "update-browserslist-db": "^1.0.10" } }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "cac": { "version": "6.7.14", "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", @@ -10812,6 +10888,11 @@ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", diff --git a/package.json b/package.json index 0faa73e0..1f1aaca5 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@tanstack/react-query-persist-client": "^4.28.0", "@tauri-apps/api": "^1.2.0", "@vitejs/plugin-react": "^3.1.0", + "buffer": "^6.0.3", "classnames": "^2.3.2", "cm6-graphql": "^0.0.4-canary-b30a2325.0", "codemirror": "^6.0.1", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f09a6ab1..301e6660 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -20,7 +20,7 @@ cocoa = "0.24.1" [dependencies] serde_json = { version = "1.0", features = ["raw_value"] } serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.2", features = ["config-toml", "devtools", "shell-open", "system-tray", "updater", "window-start-dragging"] } +tauri = { version = "1.2", features = ["config-toml", "devtools", "fs-read-file", "protocol-asset", "shell-open", "system-tray", "updater", "window-start-dragging"] } http = "0.2.8" reqwest = { version = "0.11.14", features = ["json"] } tokio = { version = "1.25.0", features = ["sync"] } diff --git a/src-tauri/migrations/20230413232435_response-body-blob.sql b/src-tauri/migrations/20230413232435_response-body-blob.sql new file mode 100644 index 00000000..3296e696 --- /dev/null +++ b/src-tauri/migrations/20230413232435_response-body-blob.sql @@ -0,0 +1,5 @@ +DELETE FROM main.http_responses; +ALTER TABLE http_responses DROP COLUMN body; +ALTER TABLE http_responses ADD COLUMN body BLOB; +ALTER TABLE http_responses ADD COLUMN body_path TEXT; +ALTER TABLE http_responses ADD COLUMN content_length INTEGER; diff --git a/src-tauri/sqlx-data.json b/src-tauri/sqlx-data.json index 1b0deeac..1308143a 100644 --- a/src-tauri/sqlx-data.json +++ b/src-tauri/sqlx-data.json @@ -68,16 +68,6 @@ }, "query": "\n DELETE FROM http_responses\n WHERE request_id = ?\n " }, - "318ed5a1126fe00719393cf4e6c788ee5a265af88b7253f61a475f78c6774ef6": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Right": 9 - } - }, - "query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n url,\n status,\n status_reason,\n body,\n headers\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);\n " - }, "448a1d1f1866ab42c0f81fcf8eb2930bf21dfdd43ca4831bc1a198cf45ac3732": { "describe": { "columns": [], @@ -88,6 +78,16 @@ }, "query": "\n DELETE FROM http_requests\n WHERE id = ?\n " }, + "62475fd9483fb5eda01c937949da2ef66ac7005b4be06b87aa6210d462348aca": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Right": 10 + } + }, + "query": "\n UPDATE http_responses SET (\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body,\n body_path,\n error,\n headers,\n updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n " + }, "6f0cb5a6d1e8dbc8cdfcc3c7e7944b2c83c22cb795b9d6b98fe067dabec9680b": { "describe": { "columns": [ @@ -194,15 +194,15 @@ }, "query": "\n DELETE FROM workspaces\n WHERE id = ?\n " }, - "a83698dcf9a815b881097133edb31a34ba25e7c6c114d463c495342a85371639": { + "8947a2a90478277c42fe9b06bc1fa98197642a4d281a3dbc101be2c9c1fec36c": { "describe": { "columns": [], "nullable": [], "parameters": { - "Right": 8 + "Right": 11 } }, - "query": "\n UPDATE http_responses SET (elapsed, url, status, status_reason, body, error, headers, updated_at) =\n (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n " + "query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body,\n body_path,\n headers\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n " }, "b19c275180909a39342b13c3cdcf993781636913ae590967f5508c46a56dc961": { "describe": { @@ -214,6 +214,108 @@ }, "query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n name,\n url,\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 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 sort_priority = excluded.sort_priority\n " }, + "c23c61b05a4c9e04ab0c1fc2c579d6f2a82a37aeed8addf9861b4985f2a5422e": { + "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": "updated_at", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "created_at", + "ordinal": 5, + "type_info": "Datetime" + }, + { + "name": "url", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "status", + "ordinal": 7, + "type_info": "Int64" + }, + { + "name": "status_reason", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "content_length", + "ordinal": 9, + "type_info": "Int64" + }, + { + "name": "body", + "ordinal": 10, + "type_info": "Blob" + }, + { + "name": "body_path", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "elapsed", + "ordinal": 12, + "type_info": "Int64" + }, + { + "name": "error", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "headers!: sqlx::types::Json>", + "ordinal": 14, + "type_info": "Text" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + false, + true, + false + ], + "parameters": { + "Right": 1 + } + }, + "query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_responses\n WHERE id = ?\n " + }, "caf3f21bf291dfbd36446592066e96c1f83abe96f6ea9211a3e049eb9c58a8c8": { "describe": { "columns": [ @@ -406,96 +508,6 @@ }, "query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\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 " }, - "d5ad6d5f82fe837fa9215bd4619ec18a7c95b3088d4fbf9825f2d1d28069d1ce": { - "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": "updated_at", - "ordinal": 4, - "type_info": "Datetime" - }, - { - "name": "created_at", - "ordinal": 5, - "type_info": "Datetime" - }, - { - "name": "status", - "ordinal": 6, - "type_info": "Int64" - }, - { - "name": "status_reason", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "body", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "elapsed", - "ordinal": 9, - "type_info": "Int64" - }, - { - "name": "url", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "error", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "headers!: sqlx::types::Json>", - "ordinal": 12, - "type_info": "Text" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - true, - false - ], - "parameters": { - "Right": 1 - } - }, - "query": "\n SELECT id, model, workspace_id, request_id, updated_at,\n created_at, status, status_reason, body, elapsed, url, error,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at ASC\n " - }, "d80c09497771e3641022e73ec6c6a87e73a551f88a948a5445d754922b82b50b": { "describe": { "columns": [], @@ -516,7 +528,7 @@ }, "query": "\n UPDATE workspaces SET (name, updated_at) =\n (?, CURRENT_TIMESTAMP) WHERE id = ?;\n " }, - "e3ade0a69348d512e47e964bded9d7d890b92fdc1e01c6c22fa5e91f943639f2": { + "e7bba61f8d60fb021c027d829b070e7087041e0f30252754aaaa85223b614896": { "describe": { "columns": [ { @@ -550,38 +562,48 @@ "type_info": "Datetime" }, { - "name": "status", + "name": "url", "ordinal": 6, + "type_info": "Text" + }, + { + "name": "status", + "ordinal": 7, "type_info": "Int64" }, { "name": "status_reason", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "body", "ordinal": 8, "type_info": "Text" }, { - "name": "elapsed", + "name": "content_length", "ordinal": 9, "type_info": "Int64" }, { - "name": "url", + "name": "body", "ordinal": 10, - "type_info": "Text" + "type_info": "Blob" }, { - "name": "error", + "name": "body_path", "ordinal": 11, "type_info": "Text" }, { - "name": "headers!: sqlx::types::Json>", + "name": "elapsed", "ordinal": 12, + "type_info": "Int64" + }, + { + "name": "error", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "headers!: sqlx::types::Json>", + "ordinal": 14, "type_info": "Text" } ], @@ -593,9 +615,11 @@ false, false, false, + false, + true, + true, + true, true, - false, - false, false, true, false @@ -604,7 +628,7 @@ "Right": 1 } }, - "query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at,\n status, status_reason, body, elapsed, url, error,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_responses\n WHERE id = ?\n " + "query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at ASC\n " }, "f116d8cf9aad828135bb8c3a4c8b8e6b857ae13303989e9133a33b2d1cf20e96": { "describe": { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 94a29937..ecaefff4 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -9,7 +9,8 @@ extern crate objc; use std::collections::HashMap; use std::env::current_dir; -use std::fs::create_dir_all; +use std::fs::{create_dir_all, File}; +use std::io::Write; use base64::Engine; use http::header::{HeaderName, ACCEPT, USER_AGENT}; @@ -221,9 +222,29 @@ async fn actually_send_ephemeral_request( .collect(), ); response.url = v.url().to_string(); - response.body = v.text().await.expect("Failed to get body"); + let body_bytes = v.bytes().await.expect("Failed to get body").to_vec(); + response.content_length = Some(body_bytes.len() as i64); + if body_bytes.len() > 1000 { + let dir = app_handle.path_resolver().app_data_dir().unwrap(); + let body_path = dir.join(response.id.clone()); + let mut f = File::options() + .create(true) + .write(true) + .open(&body_path) + .expect("Failed to open file"); + f.write_all(body_bytes.as_slice()) + .expect("Failed to write to file"); + response.body_path = Some( + body_path + .to_str() + .expect("Failed to get body path") + .to_string(), + ); + } else { + response.body = Some(body_bytes); + } response.elapsed = start.elapsed().as_millis() as i64; - response = models::update_response_if_id(response, pool) + response = models::update_response_if_id(&response, pool) .await .expect("Failed to update response"); if request.id != "" { @@ -247,7 +268,7 @@ async fn send_request( .await .expect("Failed to get request"); - let response = models::create_response(&req.id, 0, "", 0, None, "", vec![], pool) + let response = models::create_response(&req.id, 0, "", 0, None, None, None, None, vec![], pool) .await .expect("Failed to create response"); @@ -271,7 +292,7 @@ async fn response_err( ) -> Result { let mut response = response.clone(); response.error = Some(error.clone()); - response = models::update_response_if_id(response, pool) + response = models::update_response_if_id(&response, pool) .await .expect("Failed to update response"); emit_side_effect(app_handle, "updated_model", &response); diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 791af81d..910ec826 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -63,10 +63,12 @@ pub struct HttpResponse { pub updated_at: NaiveDateTime, pub error: Option, pub url: String, + pub content_length: Option, pub elapsed: i64, pub status: i64, pub status_reason: Option, - pub body: String, + pub body: Option>, + pub body_path: Option, pub headers: Json>, } @@ -373,7 +375,9 @@ pub async fn create_response( url: &str, status: i64, status_reason: Option<&str>, - body: &str, + content_length: Option, + body: Option>, + body_path: Option<&str>, headers: Vec, pool: &Pool, ) -> Result { @@ -392,10 +396,12 @@ pub async fn create_response( url, status, status_reason, + content_length, body, + body_path, headers ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); "#, id, request_id, @@ -404,7 +410,9 @@ pub async fn create_response( url, status, status_reason, + content_length, body, + body_path, headers_json, ) .execute(pool) @@ -415,11 +423,11 @@ pub async fn create_response( } pub async fn update_response_if_id( - response: HttpResponse, + response: &HttpResponse, pool: &Pool, ) -> Result { if response.id == "" { - return Ok(response); + return Ok(response.clone()); } return update_response(response, pool).await; } @@ -444,20 +452,32 @@ pub async fn update_workspace( } pub async fn update_response( - response: HttpResponse, + response: &HttpResponse, pool: &Pool, ) -> Result { - let headers_json = Json(response.headers); + let headers_json = Json(&response.headers); sqlx::query!( r#" - UPDATE http_responses SET (elapsed, url, status, status_reason, body, error, headers, updated_at) = - (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?; + UPDATE http_responses SET ( + elapsed, + url, + status, + status_reason, + content_length, + body, + body_path, + error, + headers, + updated_at + ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?; "#, response.elapsed, response.url, response.status, response.status_reason, + response.content_length, response.body, + response.body_path, response.error, headers_json, response.id, @@ -469,11 +489,11 @@ pub async fn update_response( } pub async fn get_response(id: &str, pool: &Pool) -> Result { - sqlx::query_as_unchecked!( + sqlx::query_as!( HttpResponse, r#" - SELECT id, model, workspace_id, request_id, updated_at, created_at, - status, status_reason, body, elapsed, url, error, + SELECT id, model, workspace_id, request_id, updated_at, created_at, url, + status, status_reason, content_length, body, body_path, elapsed, error, headers AS "headers!: sqlx::types::Json>" FROM http_responses WHERE id = ? @@ -491,8 +511,8 @@ pub async fn find_responses( sqlx::query_as!( HttpResponse, r#" - SELECT id, model, workspace_id, request_id, updated_at, - created_at, status, status_reason, body, elapsed, url, error, + SELECT id, model, workspace_id, request_id, updated_at, created_at, url, + status, status_reason, content_length, body, body_path, elapsed, error, headers AS "headers!: sqlx::types::Json>" FROM http_responses WHERE request_id = ? diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 4394bd44..360f9ca4 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -14,9 +14,15 @@ "windows": [], "allowlist": { "all": false, + "protocol": { + "assetScope": ["$APPDATA/*"], + "asset": true + }, "fs": { + "readFile": true, "scope": [ - "$RESOURCE/*" + "$RESOURCE/*", + "$APPDATA/*" ] }, "shell": { @@ -60,7 +66,8 @@ "timestampUrl": "" } }, - "security": {}, + "security": { + }, "systemTray": { "iconAsTemplate": true, "iconPath": "icons/icon.png" diff --git a/src-web/components/ImageView.tsx b/src-web/components/ImageView.tsx deleted file mode 100644 index 4dede0a2..00000000 --- a/src-web/components/ImageView.tsx +++ /dev/null @@ -1,9 +0,0 @@ -interface Props { - data: string; -} - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export function ImageView({ data }: Props) { - // const dataUri = `data:image/png;base64,${window.btoa(data)}`; - return
Image preview not supported until binary response support is added
; -} diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index 09bd61ab..33bca803 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -5,26 +5,26 @@ import { createGlobalState } from 'react-use'; import { useActiveRequestId } from '../hooks/useActiveRequestId'; import { useDeleteResponse } from '../hooks/useDeleteResponse'; import { useDeleteResponses } from '../hooks/useDeleteResponses'; +import { useResponseContentType } from '../hooks/useResponseContentType'; import { useResponses } from '../hooks/useResponses'; import { useResponseViewMode } from '../hooks/useResponseViewMode'; -import { tryFormatJson } from '../lib/formatters'; import type { HttpResponse } from '../lib/models'; import { isResponseLoading } from '../lib/models'; import { pluralize } from '../lib/pluralize'; import { Banner } from './core/Banner'; import { CountBadge } from './core/CountBadge'; import { Dropdown } from './core/Dropdown'; -import { Editor } from './core/Editor'; import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; import { HStack } from './core/Stacks'; import { StatusTag } from './core/StatusTag'; import type { TabItem } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs'; -import { Webview } from './core/Webview'; import { EmptyStateText } from './EmptyStateText'; -import { ImageView } from './ImageView'; import { ResponseHeaders } from './ResponseHeaders'; +import { ImageViewer } from './responseViewers/ImageViewer'; +import { TextViewer } from './responseViewers/TextViewer'; +import { WebPageViewer } from './responseViewers/WebPageViewer'; interface Props { style?: CSSProperties; @@ -48,12 +48,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro // Unset pinned response when a new one comes in useEffect(() => setPinnedResponseId(null), [responses.length]); - const contentType = useMemo( - () => - activeResponse?.headers.find((h) => h.name.toLowerCase() === 'content-type')?.value ?? - 'text/plain', - [activeResponse], - ); + const contentType = useResponseContentType(activeResponse); const tabs: TabItem[] = useMemo( () => [ @@ -84,9 +79,6 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro [activeResponse?.headers, setViewMode, viewMode], ); - // Don't render until we know the view mode - if (viewMode === undefined) return null; - return (
{activeResponse.elapsed}ms )} - {activeResponse.body.length > 0 && ( + {activeResponse.contentLength && ( <> - {(activeResponse.body.length / 1000).toFixed(1)} KB + {(activeResponse.contentLength / 1000).toFixed(1)} KB )} @@ -169,49 +161,29 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro )} - { - - - - - - {!activeResponse.body ? ( - Empty Body - ) : viewMode === 'pretty' && contentType.includes('html') ? ( - - ) : viewMode === 'pretty' && contentType.includes('json') ? ( - - ) : contentType.startsWith('image') ? ( - - ) : activeResponse?.body ? ( - - ) : null} - - - } + + + + + + {!activeResponse.contentLength ? ( + Empty Body + ) : viewMode === 'pretty' && contentType?.includes('html') ? ( + + ) : contentType?.startsWith('image') ? ( + + ) : ( + + )} + + )}
diff --git a/src-web/components/RouteError.tsx b/src-web/components/RouteError.tsx index b3a1ddf7..2feb82b3 100644 --- a/src-web/components/RouteError.tsx +++ b/src-web/components/RouteError.tsx @@ -1,4 +1,5 @@ import { useRouteError } from 'react-router-dom'; +import { useAppRoutes } from '../hooks/useAppRoutes'; import { Button } from './core/Button'; import { Heading } from './core/Heading'; import { VStack } from './core/Stacks'; @@ -8,6 +9,7 @@ export default function RouteError() { const stringified = JSON.stringify(error); // eslint-disable-next-line @typescript-eslint/no-explicit-any const message = (error as any).message ?? stringified; + const routes = useAppRoutes(); return (
@@ -16,7 +18,12 @@ export default function RouteError() { {message} -