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-nesting": "^11.2.1",
"prettier": "^2.8.4",
"react-devtools": "^4.28.5",
"tailwindcss": "^3.2.7",
"typescript": "^5.0.2",
"vite": "^4.0.0",

View File

@@ -1,6 +1,6 @@
{
"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": {
"columns": [
{
@@ -53,34 +53,29 @@
"ordinal": 9,
"type_info": "Int64"
},
{
"name": "body",
"ordinal": 10,
"type_info": "Blob"
},
{
"name": "body_path",
"ordinal": 11,
"ordinal": 10,
"type_info": "Text"
},
{
"name": "elapsed",
"ordinal": 12,
"ordinal": 11,
"type_info": "Int64"
},
{
"name": "error",
"ordinal": 13,
"ordinal": 12,
"type_info": "Text"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
"ordinal": 14,
"ordinal": 13,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
"Right": 2
},
"nullable": [
false,
@@ -94,11 +89,10 @@
true,
true,
true,
true,
false,
true,
false
]
},
"hash": "26072725d536c3cfdffd9a681d17c0ee2f246ca98e0459630a2430236d3bbdd2"
"hash": "07b0c398efd1d5f8f479652de658716a9e7faef6aba6583dd209a4f290c5edd1"
}

View File

@@ -1,12 +1,12 @@
{
"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": {
"columns": [],
"parameters": {
"Right": 11
"Right": 10
},
"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",
"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": {
"columns": [
{
@@ -53,29 +53,24 @@
"ordinal": 9,
"type_info": "Int64"
},
{
"name": "body",
"ordinal": 10,
"type_info": "Blob"
},
{
"name": "body_path",
"ordinal": 11,
"ordinal": 10,
"type_info": "Text"
},
{
"name": "elapsed",
"ordinal": 12,
"ordinal": 11,
"type_info": "Int64"
},
{
"name": "error",
"ordinal": 13,
"ordinal": 12,
"type_info": "Text"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
"ordinal": 14,
"ordinal": 13,
"type_info": "Text"
}
],
@@ -94,11 +89,10 @@
true,
true,
true,
true,
false,
true,
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",
"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": {
"columns": [
{
@@ -53,29 +53,24 @@
"ordinal": 9,
"type_info": "Int64"
},
{
"name": "body",
"ordinal": 10,
"type_info": "Blob"
},
{
"name": "body_path",
"ordinal": 11,
"ordinal": 10,
"type_info": "Text"
},
{
"name": "elapsed",
"ordinal": 12,
"ordinal": 11,
"type_info": "Int64"
},
{
"name": "error",
"ordinal": 13,
"ordinal": 12,
"type_info": "Text"
},
{
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
"ordinal": 14,
"ordinal": 13,
"type_info": "Text"
}
],
@@ -94,11 +89,10 @@
true,
true,
true,
true,
false,
true,
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,
"module": "ESNext",
"lib": [
"ESNext"
"ESNext",
],
"skipLibCheck": true,
"moduleResolution": "bundler",
@@ -18,6 +18,6 @@
"noFallthroughCasesInSwitch": true
},
"include": [
"src"
"./src"
]
}

View File

@@ -16,20 +16,20 @@ use fern::colors::ColoredLevelConfig;
use log::{debug, info, warn};
use rand::random;
use serde::Serialize;
use sqlx::{Pool, Sqlite, SqlitePool};
use sqlx::migrate::Migrator;
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")]
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_window_state::{StateFlags, WindowExt};
use tokio::sync::Mutex;
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::send::actually_send_request;
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
@@ -186,7 +186,7 @@ async fn send_request(
.await
.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
.expect("Failed to create response");
@@ -551,10 +551,11 @@ async fn get_workspace(
#[tauri::command]
async fn list_responses(
request_id: &str,
limit: Option<i64>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<Vec<models::HttpResponse>, String> {
let pool = &*db_instance.lock().await;
models::find_responses(request_id, pool)
models::find_responses(request_id, limit, pool)
.await
.map_err(|e| e.to_string())
}

View File

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

View File

@@ -92,9 +92,11 @@ export function GlobalHooks() {
}
if (!shouldIgnoreModel(payload)) {
console.time('set query date');
queryClient.setQueryData<Model[]>(queryKey, (values) =>
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 type { CSSProperties } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { createGlobalState } from 'react-use';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
import { tryFormatJson } from '../lib/formatters';
@@ -47,7 +43,6 @@ const useActiveTab = createGlobalState<string>('body');
export const RequestPane = memo(function RequestPane({ style, fullHeight, className }: Props) {
const activeRequest = useActiveRequest();
const activeRequestId = activeRequest?.id ?? null;
const activeEnvironmentId = useActiveEnvironmentId();
const updateRequest = useUpdateRequest(activeRequestId);
const [activeTab, setActiveTab] = useActiveTab();
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
@@ -183,18 +178,6 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
[updateRequest],
);
useListenToTauriEvent(
'send_request',
async ({ windowLabel }) => {
if (windowLabel !== appWindow.label) return;
await invoke('send_request', {
requestId: activeRequestId,
environmentId: activeEnvironmentId,
});
},
[activeRequestId, activeEnvironmentId],
);
return (
<div
style={style}

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
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 { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useLatestResponse } from '../hooks/useLatestResponse';
@@ -18,12 +18,12 @@ import { StatusTag } from './core/StatusTag';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
import { RecentResponsesDropdown } from './RecentResponsesDropdown';
import { ResponseHeaders } from './ResponseHeaders';
import { CsvViewer } from './responseViewers/CsvViewer';
import { ImageViewer } from './responseViewers/ImageViewer';
import { TextViewer } from './responseViewers/TextViewer';
import { WebPageViewer } from './responseViewers/WebPageViewer';
import { RecentResponsesDropdown } from './RecentResponsesDropdown';
interface Props {
style?: CSSProperties;
@@ -48,11 +48,14 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
const contentType = useResponseContentType(activeResponse);
const handlePinnedResponse = useCallback((r: HttpResponse) => {
setPinnedResponseId(r.id);
}, [setPinnedResponseId])
const handlePinnedResponse = useCallback(
(r: HttpResponse) => {
setPinnedResponseId(r.id);
},
[setPinnedResponseId],
);
const tabs: TabItem[] = useMemo(
const tabs = useMemo<TabItem[]>(
() => [
{
value: 'body',
@@ -62,7 +65,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
onChange: setViewMode,
items: [
{ 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',
},
],
[activeResponse?.headers, setViewMode, viewMode],
[activeResponse?.headers, contentType, setViewMode, viewMode],
);
return (
@@ -145,10 +148,14 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
<TabContent value="body">
{!activeResponse.contentLength ? (
<EmptyStateText>Empty Body</EmptyStateText>
) : viewMode === 'pretty' && contentType?.includes('html') ? (
<WebPageViewer response={activeResponse} />
) : contentType?.startsWith('image') ? (
<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/) ? (
<CsvViewer className="pb-2" response={activeResponse} />
) : (

View File

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

View File

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

View File

@@ -2,8 +2,8 @@ import classNames from 'classnames';
import type { EditorView } from 'codemirror';
import type { FormEvent } from 'react';
import { memo, useCallback, useRef, useState } from 'react';
import { useHotkey } from '../hooks/useHotkey';
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useSendRequest } from '../hooks/useSendRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
@@ -40,9 +40,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
[sendRequest],
);
useListenToTauriEvent('focus_url', () => {
inputRef.current?.focus();
});
useHotkey('url.focus', () => inputRef.current?.focus());
return (
<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"
icon={loading ? 'update' : 'paperPlane'}
spin={loading}
hotkeyAction="request.send"
/>
}
/>

View File

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

View File

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

View File

@@ -5,12 +5,6 @@ export type { EditorProps } from './Editor';
// showing any content
// 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 graphql = editor.graphql;
export const getIntrospectionQuery = editor.getIntrospectionQuery;

View File

@@ -20,6 +20,7 @@ export function WebPageViewer({ response }: Props) {
return (
<div className="h-full pb-3">
<iframe
key={body ? 'has-body' : 'no-body'}
title="Response preview"
srcDoc={contentForIframe}
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: [],
queryKey: responsesQueryKey({ requestId: requestId ?? 'n/a' }),
queryFn: async () => {
return (await invoke('list_responses', {
requestId,
})) as HttpResponse[];
return (await invoke('list_responses', { requestId, limit: 200 })) as HttpResponse[];
},
}).data ?? []
);

View File

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

View File

@@ -2,10 +2,6 @@ import { readBinaryFile, readTextFile } from '@tauri-apps/api/fs';
import type { HttpResponse } from './models';
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) {
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> {
if (response.body) {
return Uint8Array.from(response.body);
}
if (response.bodyPath) {
return readBinaryFile(response.bodyPath);
}