Remove response body and basic hotkeys

This commit is contained in:
Gregory Schier
2023-11-21 22:15:01 -08:00
parent 2c7bf29ec6
commit 7381dcec05
27 changed files with 1731 additions and 146 deletions

1550
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -86,6 +86,7 @@
"postcss": "^8.4.21", "postcss": "^8.4.21",
"postcss-nesting": "^11.2.1", "postcss-nesting": "^11.2.1",
"prettier": "^2.8.4", "prettier": "^2.8.4",
"react-devtools": "^4.28.5",
"tailwindcss": "^3.2.7", "tailwindcss": "^3.2.7",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"vite": "^4.0.0", "vite": "^4.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"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<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE workspace_id = ?\n ORDER BY created_at DESC\n ", "query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at DESC\n LIMIT ?\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -53,34 +53,29 @@
"ordinal": 9, "ordinal": 9,
"type_info": "Int64" "type_info": "Int64"
}, },
{
"name": "body",
"ordinal": 10,
"type_info": "Blob"
},
{ {
"name": "body_path", "name": "body_path",
"ordinal": 11, "ordinal": 10,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "elapsed", "name": "elapsed",
"ordinal": 12, "ordinal": 11,
"type_info": "Int64" "type_info": "Int64"
}, },
{ {
"name": "error", "name": "error",
"ordinal": 13, "ordinal": 12,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>", "name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
"ordinal": 14, "ordinal": 13,
"type_info": "Text" "type_info": "Text"
} }
], ],
"parameters": { "parameters": {
"Right": 1 "Right": 2
}, },
"nullable": [ "nullable": [
false, false,
@@ -94,11 +89,10 @@
true, true,
true, true,
true, true,
true,
false, false,
true, true,
false false
] ]
}, },
"hash": "26072725d536c3cfdffd9a681d17c0ee2f246ca98e0459630a2430236d3bbdd2" "hash": "07b0c398efd1d5f8f479652de658716a9e7faef6aba6583dd209a4f290c5edd1"
} }

View File

@@ -1,12 +1,12 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"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 ", "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_path,\n headers\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ",
"describe": { "describe": {
"columns": [], "columns": [],
"parameters": { "parameters": {
"Right": 11 "Right": 10
}, },
"nullable": [] "nullable": []
}, },
"hash": "8947a2a90478277c42fe9b06bc1fa98197642a4d281a3dbc101be2c9c1fec36c" "hash": "198bd086ccc87d2e6c24cb1c717f486d3ab58c0c958ede850c018fc266eade87"
} }

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE http_responses SET (\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n error,\n headers,\n updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 9
},
"nullable": []
},
"hash": "294cbe19f9ddd9519ace3558df4308948082ec0ce7096855aa7d8fba519b8b4f"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"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<Vec<HttpResponseHeader>>\"\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_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE workspace_id = ?\n ORDER BY created_at DESC\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -53,29 +53,24 @@
"ordinal": 9, "ordinal": 9,
"type_info": "Int64" "type_info": "Int64"
}, },
{
"name": "body",
"ordinal": 10,
"type_info": "Blob"
},
{ {
"name": "body_path", "name": "body_path",
"ordinal": 11, "ordinal": 10,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "elapsed", "name": "elapsed",
"ordinal": 12, "ordinal": 11,
"type_info": "Int64" "type_info": "Int64"
}, },
{ {
"name": "error", "name": "error",
"ordinal": 13, "ordinal": 12,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>", "name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
"ordinal": 14, "ordinal": 13,
"type_info": "Text" "type_info": "Text"
} }
], ],
@@ -94,11 +89,10 @@
true, true,
true, true,
true, true,
true,
false, false,
true, true,
false false
] ]
}, },
"hash": "c23c61b05a4c9e04ab0c1fc2c579d6f2a82a37aeed8addf9861b4985f2a5422e" "hash": "3d199d371be948211f4a50c869b307f5df60784293c52397d77a187633a406dd"
} }

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"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 ",
"describe": {
"columns": [],
"parameters": {
"Right": 10
},
"nullable": []
},
"hash": "62475fd9483fb5eda01c937949da2ef66ac7005b4be06b87aa6210d462348aca"
}

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "SQLite", "db_name": "SQLite",
"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<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at DESC\n ", "query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE id = ?\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@@ -53,29 +53,24 @@
"ordinal": 9, "ordinal": 9,
"type_info": "Int64" "type_info": "Int64"
}, },
{
"name": "body",
"ordinal": 10,
"type_info": "Blob"
},
{ {
"name": "body_path", "name": "body_path",
"ordinal": 11, "ordinal": 10,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "elapsed", "name": "elapsed",
"ordinal": 12, "ordinal": 11,
"type_info": "Int64" "type_info": "Int64"
}, },
{ {
"name": "error", "name": "error",
"ordinal": 13, "ordinal": 12,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>", "name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
"ordinal": 14, "ordinal": 13,
"type_info": "Text" "type_info": "Text"
} }
], ],
@@ -94,11 +89,10 @@
true, true,
true, true,
true, true,
true,
false, false,
true, true,
false false
] ]
}, },
"hash": "5aa070e61995f8b1724efaa94c5f0cef5a4be6efda5d70354ad449d7d4b5aee4" "hash": "679a519475adeb50abf046114d3c0d1e48e103f2bb11ef47637d7f0b00ed241f"
} }

View File

@@ -0,0 +1 @@
ALTER TABLE http_responses DROP COLUMN body;

View File

@@ -4,7 +4,7 @@
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "ESNext", "module": "ESNext",
"lib": [ "lib": [
"ESNext" "ESNext",
], ],
"skipLibCheck": true, "skipLibCheck": true,
"moduleResolution": "bundler", "moduleResolution": "bundler",
@@ -18,6 +18,6 @@
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true
}, },
"include": [ "include": [
"src" "./src"
] ]
} }

View File

@@ -16,20 +16,20 @@ use fern::colors::ColoredLevelConfig;
use log::{debug, info, warn}; use log::{debug, info, warn};
use rand::random; use rand::random;
use serde::Serialize; use serde::Serialize;
use sqlx::{Pool, Sqlite, SqlitePool};
use sqlx::migrate::Migrator; use sqlx::migrate::Migrator;
use sqlx::types::Json; use sqlx::types::Json;
use sqlx::{Pool, Sqlite, SqlitePool}; use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
use tauri::{Manager, WindowEvent};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::TitleBarStyle; use tauri::TitleBarStyle;
use tauri::{AppHandle, Menu, RunEvent, State, Submenu, Window, WindowUrl, Wry};
use tauri::{CustomMenuItem, Manager, WindowEvent};
use tauri_plugin_log::{fern, LogTarget}; use tauri_plugin_log::{fern, LogTarget};
use tauri_plugin_window_state::{StateFlags, WindowExt}; use tauri_plugin_window_state::{StateFlags, WindowExt};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use window_ext::TrafficLightWindowExt; use window_ext::TrafficLightWindowExt;
use crate::analytics::{track_event, AnalyticsAction, AnalyticsResource}; use crate::analytics::{AnalyticsAction, AnalyticsResource, track_event};
use crate::plugin::{ImportResources, ImportResult}; use crate::plugin::{ImportResources, ImportResult};
use crate::send::actually_send_request; use crate::send::actually_send_request;
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater}; use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
@@ -186,7 +186,7 @@ async fn send_request(
.await .await
.expect("Failed to get request"); .expect("Failed to get request");
let response = models::create_response(&req.id, 0, "", 0, None, None, None, None, vec![], pool) let response = models::create_response(&req.id, 0, "", 0, None, None, None, vec![], pool)
.await .await
.expect("Failed to create response"); .expect("Failed to create response");
@@ -551,10 +551,11 @@ async fn get_workspace(
#[tauri::command] #[tauri::command]
async fn list_responses( async fn list_responses(
request_id: &str, request_id: &str,
limit: Option<i64>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<Vec<models::HttpResponse>, String> { ) -> Result<Vec<models::HttpResponse>, String> {
let pool = &*db_instance.lock().await; let pool = &*db_instance.lock().await;
models::find_responses(request_id, pool) models::find_responses(request_id, limit, pool)
.await .await
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }

View File

@@ -124,7 +124,6 @@ pub struct HttpResponse {
pub elapsed: i64, pub elapsed: i64,
pub status: i64, pub status: i64,
pub status_reason: Option<String>, pub status_reason: Option<String>,
pub body: Option<Vec<u8>>,
pub body_path: Option<String>, pub body_path: Option<String>,
pub headers: Json<Vec<HttpResponseHeader>>, pub headers: Json<Vec<HttpResponseHeader>>,
} }
@@ -594,7 +593,6 @@ pub async fn create_response(
status: i64, status: i64,
status_reason: Option<&str>, status_reason: Option<&str>,
content_length: Option<i64>, content_length: Option<i64>,
body: Option<Vec<u8>>,
body_path: Option<&str>, body_path: Option<&str>,
headers: Vec<HttpResponseHeader>, headers: Vec<HttpResponseHeader>,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
@@ -613,11 +611,10 @@ pub async fn create_response(
status, status,
status_reason, status_reason,
content_length, content_length,
body,
body_path, body_path,
headers headers
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"#, "#,
id, id,
request_id, request_id,
@@ -627,7 +624,6 @@ pub async fn create_response(
status, status,
status_reason, status_reason,
content_length, content_length,
body,
body_path, body_path,
headers_json, headers_json,
) )
@@ -704,19 +700,17 @@ pub async fn update_response(
status, status,
status_reason, status_reason,
content_length, content_length,
body,
body_path, body_path,
error, error,
headers, headers,
updated_at updated_at
) = (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?; ) = (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;
"#, "#,
response.elapsed, response.elapsed,
response.url, response.url,
response.status, response.status,
response.status_reason, response.status_reason,
response.content_length, response.content_length,
response.body,
response.body_path, response.body_path,
response.error, response.error,
headers_json, headers_json,
@@ -732,7 +726,7 @@ pub async fn get_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse,
HttpResponse, HttpResponse,
r#" r#"
SELECT id, model, workspace_id, request_id, updated_at, created_at, url, SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
status, status_reason, content_length, body, body_path, elapsed, error, status, status_reason, content_length, body_path, elapsed, error,
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>" headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
FROM http_responses FROM http_responses
WHERE id = ? WHERE id = ?
@@ -745,19 +739,26 @@ pub async fn get_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse,
pub async fn find_responses( pub async fn find_responses(
request_id: &str, request_id: &str,
limit: Option<i64>,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<Vec<HttpResponse>, sqlx::Error> { ) -> Result<Vec<HttpResponse>, sqlx::Error> {
let limit_unwrapped = match limit {
Some(l) => l,
None => i64::MAX,
};
sqlx::query_as!( sqlx::query_as!(
HttpResponse, HttpResponse,
r#" r#"
SELECT id, model, workspace_id, request_id, updated_at, created_at, url, SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
status, status_reason, content_length, body, body_path, elapsed, error, status, status_reason, content_length, body_path, elapsed, error,
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>" headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
FROM http_responses FROM http_responses
WHERE request_id = ? WHERE request_id = ?
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT ?
"#, "#,
request_id, request_id,
limit_unwrapped,
) )
.fetch_all(pool) .fetch_all(pool)
.await .await
@@ -771,7 +772,7 @@ pub async fn find_responses_by_workspace_id(
HttpResponse, HttpResponse,
r#" r#"
SELECT id, model, workspace_id, request_id, updated_at, created_at, url, SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
status, status_reason, content_length, body, body_path, elapsed, error, status, status_reason, content_length, body_path, elapsed, error,
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>" headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
FROM http_responses FROM http_responses
WHERE workspace_id = ? WHERE workspace_id = ?
@@ -810,7 +811,7 @@ pub async fn delete_all_responses(
request_id: &str, request_id: &str,
pool: &Pool<Sqlite>, pool: &Pool<Sqlite>,
) -> Result<(), sqlx::Error> { ) -> Result<(), sqlx::Error> {
for r in find_responses(request_id, pool).await? { for r in find_responses(request_id, None, pool).await? {
delete_response(&r.id, pool).await?; delete_response(&r.id, pool).await?;
} }
Ok(()) Ok(())

View File

@@ -228,11 +228,6 @@ pub async fn actually_send_request(
); );
} }
// Also store body directly on the model, if small enough
if body_bytes.len() < 100_000 {
response.body = Some(body_bytes);
}
response.elapsed = start.elapsed().as_millis() as i64; response.elapsed = start.elapsed().as_millis() as i64;
response = models::update_response_if_id(&response, pool) response = models::update_response_if_id(&response, pool)
.await .await

View File

@@ -92,9 +92,11 @@ export function GlobalHooks() {
} }
if (!shouldIgnoreModel(payload)) { if (!shouldIgnoreModel(payload)) {
console.time('set query date');
queryClient.setQueryData<Model[]>(queryKey, (values) => queryClient.setQueryData<Model[]>(queryKey, (values) =>
values?.map((v) => (modelsEq(v, payload) ? payload : v)), values?.map((v) => (modelsEq(v, payload) ? payload : v)),
); );
console.timeEnd('set query date');
} }
}); });

View File

@@ -1,12 +1,8 @@
import { invoke } from '@tauri-apps/api';
import { appWindow } from '@tauri-apps/api/window';
import classNames from 'classnames'; import classNames from 'classnames';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { memo, useCallback, useMemo, useState } from 'react'; import { memo, useCallback, useMemo, useState } from 'react';
import { createGlobalState } from 'react-use'; import { createGlobalState } from 'react-use';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { tryFormatJson } from '../lib/formatters'; import { tryFormatJson } from '../lib/formatters';
@@ -47,7 +43,6 @@ const useActiveTab = createGlobalState<string>('body');
export const RequestPane = memo(function RequestPane({ style, fullHeight, className }: Props) { export const RequestPane = memo(function RequestPane({ style, fullHeight, className }: Props) {
const activeRequest = useActiveRequest(); const activeRequest = useActiveRequest();
const activeRequestId = activeRequest?.id ?? null; const activeRequestId = activeRequest?.id ?? null;
const activeEnvironmentId = useActiveEnvironmentId();
const updateRequest = useUpdateRequest(activeRequestId); const updateRequest = useUpdateRequest(activeRequestId);
const [activeTab, setActiveTab] = useActiveTab(); const [activeTab, setActiveTab] = useActiveTab();
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0); const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
@@ -183,18 +178,6 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
[updateRequest], [updateRequest],
); );
useListenToTauriEvent(
'send_request',
async ({ windowLabel }) => {
if (windowLabel !== appWindow.label) return;
await invoke('send_request', {
requestId: activeRequestId,
environmentId: activeEnvironmentId,
});
},
[activeRequestId, activeEnvironmentId],
);
return ( return (
<div <div
style={style} style={style}

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import { useCallback, memo, useEffect, useMemo, useState } from 'react'; import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { createGlobalState } from 'react-use'; import { createGlobalState } from 'react-use';
import { useActiveRequestId } from '../hooks/useActiveRequestId'; import { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useLatestResponse } from '../hooks/useLatestResponse'; import { useLatestResponse } from '../hooks/useLatestResponse';
@@ -18,12 +18,12 @@ import { StatusTag } from './core/StatusTag';
import type { TabItem } from './core/Tabs/Tabs'; import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText'; import { EmptyStateText } from './EmptyStateText';
import { RecentResponsesDropdown } from './RecentResponsesDropdown';
import { ResponseHeaders } from './ResponseHeaders'; import { ResponseHeaders } from './ResponseHeaders';
import { CsvViewer } from './responseViewers/CsvViewer'; import { CsvViewer } from './responseViewers/CsvViewer';
import { ImageViewer } from './responseViewers/ImageViewer'; import { ImageViewer } from './responseViewers/ImageViewer';
import { TextViewer } from './responseViewers/TextViewer'; import { TextViewer } from './responseViewers/TextViewer';
import { WebPageViewer } from './responseViewers/WebPageViewer'; import { WebPageViewer } from './responseViewers/WebPageViewer';
import { RecentResponsesDropdown } from './RecentResponsesDropdown';
interface Props { interface Props {
style?: CSSProperties; style?: CSSProperties;
@@ -48,11 +48,14 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
const contentType = useResponseContentType(activeResponse); const contentType = useResponseContentType(activeResponse);
const handlePinnedResponse = useCallback((r: HttpResponse) => { const handlePinnedResponse = useCallback(
setPinnedResponseId(r.id); (r: HttpResponse) => {
}, [setPinnedResponseId]) setPinnedResponseId(r.id);
},
[setPinnedResponseId],
);
const tabs: TabItem[] = useMemo( const tabs = useMemo<TabItem[]>(
() => [ () => [
{ {
value: 'body', value: 'body',
@@ -62,7 +65,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
onChange: setViewMode, onChange: setViewMode,
items: [ items: [
{ label: 'Pretty', value: 'pretty' }, { label: 'Pretty', value: 'pretty' },
{ label: 'Raw', value: 'raw' }, ...(contentType?.startsWith('image') ? [] : [{ label: 'Raw', value: 'raw' }]),
], ],
}, },
}, },
@@ -78,7 +81,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
value: 'headers', value: 'headers',
}, },
], ],
[activeResponse?.headers, setViewMode, viewMode], [activeResponse?.headers, contentType, setViewMode, viewMode],
); );
return ( return (
@@ -145,10 +148,14 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
<TabContent value="body"> <TabContent value="body">
{!activeResponse.contentLength ? ( {!activeResponse.contentLength ? (
<EmptyStateText>Empty Body</EmptyStateText> <EmptyStateText>Empty Body</EmptyStateText>
) : viewMode === 'pretty' && contentType?.includes('html') ? (
<WebPageViewer response={activeResponse} />
) : contentType?.startsWith('image') ? ( ) : contentType?.startsWith('image') ? (
<ImageViewer className="pb-2" response={activeResponse} /> <ImageViewer className="pb-2" response={activeResponse} />
) : activeResponse.contentLength > 2 * 1000 * 1000 ? (
<div className="text-sm italic text-gray-500">
Cannot preview text responses larger than 2MB
</div>
) : viewMode === 'pretty' && contentType?.includes('html') ? (
<WebPageViewer response={activeResponse} />
) : contentType?.match(/csv|tab-separated/) ? ( ) : contentType?.match(/csv|tab-separated/) ? (
<CsvViewer className="pb-2" response={activeResponse} /> <CsvViewer className="pb-2" response={activeResponse} />
) : ( ) : (

View File

@@ -14,9 +14,9 @@ import { useCreateRequest } from '../hooks/useCreateRequest';
import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest'; import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
import { useDeleteFolder } from '../hooks/useDeleteFolder'; import { useDeleteFolder } from '../hooks/useDeleteFolder';
import { useFolders } from '../hooks/useFolders'; import { useFolders } from '../hooks/useFolders';
import { useHotkey } from '../hooks/useHotkey';
import { useKeyValue } from '../hooks/useKeyValue'; import { useKeyValue } from '../hooks/useKeyValue';
import { useLatestResponse } from '../hooks/useLatestResponse'; import { useLatestResponse } from '../hooks/useLatestResponse';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { usePrompt } from '../hooks/usePrompt'; import { usePrompt } from '../hooks/usePrompt';
import { useRequests } from '../hooks/useRequests'; import { useRequests } from '../hooks/useRequests';
import { useSendManyRequests } from '../hooks/useSendFolder'; import { useSendManyRequests } from '../hooks/useSendFolder';
@@ -52,7 +52,6 @@ interface TreeNode {
export function Sidebar({ className }: Props) { export function Sidebar({ className }: Props) {
const { hidden } = useSidebarHidden(); const { hidden } = useSidebarHidden();
const createRequest = useCreateRequest();
const sidebarRef = useRef<HTMLLIElement>(null); const sidebarRef = useRef<HTMLLIElement>(null);
const activeRequestId = useActiveRequestId(); const activeRequestId = useActiveRequestId();
const activeEnvironmentId = useActiveEnvironmentId(); const activeEnvironmentId = useActiveEnvironmentId();
@@ -116,9 +115,6 @@ export function Sidebar({ className }: Props) {
return { tree, treeParentMap, selectableRequests }; return { tree, treeParentMap, selectableRequests };
}, [activeWorkspace, requests, folders]); }, [activeWorkspace, requests, folders]);
// TODO: Move these listeners to a central place
useListenToTauriEvent('new_request', async () => createRequest.mutate({}));
const focusActiveRequest = useCallback( const focusActiveRequest = useCallback(
(args: { forced?: { id: string; tree: TreeNode }; noFocusSidebar?: boolean } = {}) => { (args: { forced?: { id: string; tree: TreeNode }; noFocusSidebar?: boolean } = {}) => {
const { forced, noFocusSidebar } = args; const { forced, noFocusSidebar } = args;
@@ -193,19 +189,15 @@ export function Sidebar({ className }: Props) {
useKeyPressEvent('Backspace', handleDeleteKey); useKeyPressEvent('Backspace', handleDeleteKey);
useKeyPressEvent('Delete', handleDeleteKey); useKeyPressEvent('Delete', handleDeleteKey);
useListenToTauriEvent( useHotkey('sidebar.focus', () => {
'focus_sidebar', if (hidden || hasFocus) return;
() => { // Select 0 index on focus if none selected
if (hidden || hasFocus) return; focusActiveRequest(
// Select 0 index on focus if none selected selectedTree != null && selectedId != null
focusActiveRequest( ? { forced: { id: selectedId, tree: selectedTree } }
selectedTree != null && selectedId != null : undefined,
? { forced: { id: selectedId, tree: selectedTree } } );
: undefined, });
);
},
[focusActiveRequest, hidden, activeRequestId],
);
useKeyPressEvent('Enter', (e) => { useKeyPressEvent('Enter', (e) => {
if (!hasFocus) return; if (!hasFocus) return;

View File

@@ -1,6 +1,7 @@
import { memo } from 'react'; import { memo } from 'react';
import { useCreateFolder } from '../hooks/useCreateFolder'; import { useCreateFolder } from '../hooks/useCreateFolder';
import { useCreateRequest } from '../hooks/useCreateRequest'; import { useCreateRequest } from '../hooks/useCreateRequest';
import { useHotkey } from '../hooks/useHotkey';
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { Dropdown } from './core/Dropdown'; import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
@@ -12,6 +13,8 @@ export const SidebarActions = memo(function SidebarActions() {
const createFolder = useCreateFolder(); const createFolder = useCreateFolder();
const { hidden, toggle } = useSidebarHidden(); const { hidden, toggle } = useSidebarHidden();
useHotkey('request.create', () => createRequest.mutate({}));
return ( return (
<HStack> <HStack>
<IconButton <IconButton

View File

@@ -2,8 +2,8 @@ import classNames from 'classnames';
import type { EditorView } from 'codemirror'; import type { EditorView } from 'codemirror';
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import { memo, useCallback, useRef, useState } from 'react'; import { memo, useCallback, useRef, useState } from 'react';
import { useHotkey } from '../hooks/useHotkey';
import { useIsResponseLoading } from '../hooks/useIsResponseLoading'; import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useSendRequest } from '../hooks/useSendRequest'; import { useSendRequest } from '../hooks/useSendRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest';
@@ -40,9 +40,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
[sendRequest], [sendRequest],
); );
useListenToTauriEvent('focus_url', () => { useHotkey('url.focus', () => inputRef.current?.focus());
inputRef.current?.focus();
});
return ( return (
<form onSubmit={handleSubmit} className={classNames('url-bar', className)}> <form onSubmit={handleSubmit} className={classNames('url-bar', className)}>
@@ -79,6 +77,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
className="!h-auto w-8 mr-0.5 my-0.5" className="!h-auto w-8 mr-0.5 my-0.5"
icon={loading ? 'update' : 'paperPlane'} icon={loading ? 'update' : 'paperPlane'}
spin={loading} spin={loading}
hotkeyAction="request.send"
/> />
} }
/> />

View File

@@ -8,6 +8,7 @@ import type {
} from 'react'; } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useWindowSize } from 'react-use'; import { useWindowSize } from 'react-use';
import { useHotkey } from '../hooks/useHotkey';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useOsInfo } from '../hooks/useOsInfo'; import { useOsInfo } from '../hooks/useOsInfo';
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarHidden } from '../hooks/useSidebarHidden';
@@ -39,7 +40,7 @@ export default function Workspace() {
null, null,
); );
useListenToTauriEvent('toggle_sidebar', toggle); useHotkey('sidebar.toggle', toggle);
// float/un-float sidebar on window resize // float/un-float sidebar on window resize
useEffect(() => { useEffect(() => {

View File

@@ -1,6 +1,8 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { HTMLAttributes, ReactNode } from 'react'; import type { HTMLAttributes, ReactNode } from 'react';
import { forwardRef, memo, useMemo } from 'react'; import { forwardRef, memo, useImperativeHandle, useMemo, useRef } from 'react';
import type { HotkeyAction } from '../../hooks/useHotkey';
import { useHotkey } from '../../hooks/useHotkey';
import { Icon } from './Icon'; import { Icon } from './Icon';
const colorStyles = { const colorStyles = {
@@ -26,6 +28,7 @@ export type ButtonProps = HTMLAttributes<HTMLButtonElement> & {
title?: string; title?: string;
leftSlot?: ReactNode; leftSlot?: ReactNode;
rightSlot?: ReactNode; rightSlot?: ReactNode;
hotkeyAction?: HotkeyAction;
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -43,6 +46,8 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
leftSlot, leftSlot,
rightSlot, rightSlot,
disabled, disabled,
hotkeyAction,
onClick,
...props ...props
}: ButtonProps, }: ButtonProps,
ref, ref,
@@ -66,8 +71,25 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
[className, disabled, color, justify, size], [className, disabled, color, justify, size],
); );
const buttonRef = useRef<HTMLButtonElement>(null);
useImperativeHandle<HTMLButtonElement | null, HTMLButtonElement | null>(
ref,
() => buttonRef.current,
);
useHotkey(hotkeyAction ?? null, () => {
buttonRef.current?.click();
});
return ( return (
<button ref={ref} type={type} className={classes} disabled={disabled} {...props}> <button
ref={buttonRef}
type={type}
className={classes}
disabled={disabled}
onClick={onClick}
{...props}
>
{isLoading ? ( {isLoading ? (
<Icon icon="update" size={size} className="animate-spin mr-1" /> <Icon icon="update" size={size} className="animate-spin mr-1" />
) : leftSlot ? ( ) : leftSlot ? (

View File

@@ -5,12 +5,6 @@ export type { EditorProps } from './Editor';
// showing any content // showing any content
// const editor = await import('./Editor'); // const editor = await import('./Editor');
document.addEventListener('keydown', (e) => {
console.log('E', e.key);
e.preventDefault();
e.stopPropagation();
});
export const Editor = editor.Editor; export const Editor = editor.Editor;
export const graphql = editor.graphql; export const graphql = editor.graphql;
export const getIntrospectionQuery = editor.getIntrospectionQuery; export const getIntrospectionQuery = editor.getIntrospectionQuery;

View File

@@ -20,6 +20,7 @@ export function WebPageViewer({ response }: Props) {
return ( return (
<div className="h-full pb-3"> <div className="h-full pb-3">
<iframe <iframe
key={body ? 'has-body' : 'no-body'}
title="Response preview" title="Response preview"
srcDoc={contentForIframe} srcDoc={contentForIframe}
sandbox="allow-scripts allow-same-origin" sandbox="allow-scripts allow-same-origin"

View File

@@ -0,0 +1,60 @@
import { useEffect, useRef } from 'react';
export type HotkeyAction =
| 'request.send'
| 'request.create'
| 'request.duplicate'
| 'sidebar.toggle'
| 'sidebar.focus'
| 'url.focus';
const hotkeys: Record<HotkeyAction, string[]> = {
'request.send': ['Meta+Enter', 'Meta+r'],
'request.create': ['Meta+n'],
'request.duplicate': ['Meta+d'],
'sidebar.toggle': ['Meta+b'],
'sidebar.focus': ['Meta+1'],
'url.focus': ['Meta+l'],
};
export function useHotkey(action: HotkeyAction | null, callback: (e: KeyboardEvent) => void) {
const currentKeys = useRef<Set<string>>(new Set());
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
useEffect(() => {
const down = (e: KeyboardEvent) => {
console.log('KEY DOWN', e.key);
currentKeys.current.add(e.key);
for (const [hkAction, hkKeys] of Object.entries(hotkeys)) {
for (const hkKey of hkKeys) {
const keys = hkKey.split('+');
if (
keys.length === currentKeys.current.size &&
keys.every((key) => currentKeys.current.has(key)) &&
hkAction === action
) {
// Triggered hotkey!
console.log('TRIGGER!', action);
e.preventDefault();
e.stopPropagation();
callbackRef.current(e);
}
}
}
};
const up = (e: KeyboardEvent) => {
currentKeys.current.delete(e.key);
};
window.addEventListener('keydown', down);
window.addEventListener('keyup', up);
return () => {
window.removeEventListener('keydown', down);
window.removeEventListener('keyup', up);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [action, callback]);
}

View File

@@ -13,9 +13,7 @@ export function useResponses(requestId: string | null) {
initialData: [], initialData: [],
queryKey: responsesQueryKey({ requestId: requestId ?? 'n/a' }), queryKey: responsesQueryKey({ requestId: requestId ?? 'n/a' }),
queryFn: async () => { queryFn: async () => {
return (await invoke('list_responses', { return (await invoke('list_responses', { requestId, limit: 200 })) as HttpResponse[];
requestId,
})) as HttpResponse[];
}, },
}).data ?? [] }).data ?? []
); );

View File

@@ -84,7 +84,6 @@ export interface HttpResponse extends BaseModel {
readonly workspaceId: string; readonly workspaceId: string;
readonly model: 'http_response'; readonly model: 'http_response';
readonly requestId: string; readonly requestId: string;
readonly body: number[] | null;
readonly bodyPath: string | null; readonly bodyPath: string | null;
readonly contentLength: number | null; readonly contentLength: number | null;
readonly error: string; readonly error: string;

View File

@@ -2,10 +2,6 @@ import { readBinaryFile, readTextFile } from '@tauri-apps/api/fs';
import type { HttpResponse } from './models'; import type { HttpResponse } from './models';
export async function getResponseBodyText(response: HttpResponse): Promise<string | null> { export async function getResponseBodyText(response: HttpResponse): Promise<string | null> {
if (response.body) {
const uint8Array = Uint8Array.from(response.body);
return new TextDecoder().decode(uint8Array);
}
if (response.bodyPath) { if (response.bodyPath) {
return await readTextFile(response.bodyPath); return await readTextFile(response.bodyPath);
} }
@@ -13,9 +9,6 @@ export async function getResponseBodyText(response: HttpResponse): Promise<strin
} }
export async function getResponseBodyBlob(response: HttpResponse): Promise<Uint8Array | null> { export async function getResponseBodyBlob(response: HttpResponse): Promise<Uint8Array | null> {
if (response.body) {
return Uint8Array.from(response.body);
}
if (response.bodyPath) { if (response.bodyPath) {
return readBinaryFile(response.bodyPath); return readBinaryFile(response.bodyPath);
} }