From 8b5b66acf06c72dd96a2b9f1e178d464113d84c6 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 10 Jan 2025 06:27:57 -0800 Subject: [PATCH] Don't load response when blocking large responses --- src-tauri/yaak-models/src/queries.rs | 81 +++++++++++-------- src-web/components/ConfirmLargeResponse.tsx | 60 ++++++++++++++ src-web/components/CopyButton.tsx | 20 +++-- src-web/components/ResponsePane.tsx | 59 +++++++------- src-web/components/core/Dropdown.tsx | 5 ++ src-web/components/core/Tabs/Tabs.tsx | 2 + .../responseViewers/HTMLOrTextViewer.tsx | 5 +- .../components/responseViewers/TextViewer.tsx | 51 +----------- src-web/lib/contentType.ts | 32 +++++++- src-web/lib/model_util.ts | 4 +- 10 files changed, 197 insertions(+), 122 deletions(-) create mode 100644 src-web/components/ConfirmLargeResponse.tsx diff --git a/src-tauri/yaak-models/src/queries.rs b/src-tauri/yaak-models/src/queries.rs index a63da121..8bdc75cc 100644 --- a/src-tauri/yaak-models/src/queries.rs +++ b/src-tauri/yaak-models/src/queries.rs @@ -2106,57 +2106,68 @@ pub async fn batch_upsert( ) -> Result { let mut imported_resources = BatchUpsertResult::default(); - for v in workspaces { - let x = upsert_workspace(&window, v, update_source).await?; - imported_resources.workspaces.push(x.clone()); + if workspaces.len() > 0 { + for v in workspaces { + let x = upsert_workspace(&window, v, update_source).await?; + imported_resources.workspaces.push(x.clone()); + } + info!("Imported {} workspaces", imported_resources.workspaces.len()); } - info!("Imported {} workspaces", imported_resources.workspaces.len()); - while imported_resources.environments.len() < environments.len() { - for v in environments.clone() { - if let Some(fid) = v.environment_id.clone() { - let imported_parent = imported_resources.environments.iter().find(|f| f.id == fid); - if imported_parent.is_none() { + if environments.len() > 0 { + while imported_resources.environments.len() < environments.len() { + for v in environments.clone() { + if let Some(fid) = v.environment_id.clone() { + let imported_parent = + imported_resources.environments.iter().find(|f| f.id == fid); + if imported_parent.is_none() { + continue; + } + } + if let Some(_) = imported_resources.environments.iter().find(|f| f.id == v.id) { continue; } + let x = upsert_environment(&window, v, update_source).await?; + imported_resources.environments.push(x.clone()); } - if let Some(_) = imported_resources.environments.iter().find(|f| f.id == v.id) { - continue; - } - let x = upsert_environment(&window, v, update_source).await?; - imported_resources.environments.push(x.clone()); } + info!("Imported {} environments", imported_resources.environments.len()); } - info!("Imported {} environments", imported_resources.environments.len()); - while imported_resources.folders.len() < folders.len() { - for v in folders.clone() { - if let Some(fid) = v.folder_id.clone() { - let imported_parent = imported_resources.folders.iter().find(|f| f.id == fid); - if imported_parent.is_none() { + if folders.len() > 0 { + while imported_resources.folders.len() < folders.len() { + for v in folders.clone() { + if let Some(fid) = v.folder_id.clone() { + let imported_parent = imported_resources.folders.iter().find(|f| f.id == fid); + if imported_parent.is_none() { + continue; + } + } + if let Some(_) = imported_resources.folders.iter().find(|f| f.id == v.id) { continue; } + let x = upsert_folder(&window, v, update_source).await?; + imported_resources.folders.push(x.clone()); } - if let Some(_) = imported_resources.folders.iter().find(|f| f.id == v.id) { - continue; - } - let x = upsert_folder(&window, v, update_source).await?; - imported_resources.folders.push(x.clone()); } + info!("Imported {} folders", imported_resources.folders.len()); } - info!("Imported {} folders", imported_resources.folders.len()); - for v in http_requests { - let x = upsert_http_request(&window, v, update_source).await?; - imported_resources.http_requests.push(x.clone()); + if http_requests.len() > 0 { + for v in http_requests { + let x = upsert_http_request(&window, v, update_source).await?; + imported_resources.http_requests.push(x.clone()); + } + info!("Imported {} http_requests", imported_resources.http_requests.len()); } - info!("Imported {} http_requests", imported_resources.http_requests.len()); - for v in grpc_requests { - let x = upsert_grpc_request(&window, v, update_source).await?; - imported_resources.grpc_requests.push(x.clone()); + if grpc_requests.len() > 0 { + for v in grpc_requests { + let x = upsert_grpc_request(&window, v, update_source).await?; + imported_resources.grpc_requests.push(x.clone()); + } + info!("Imported {} grpc_requests", imported_resources.grpc_requests.len()); } - info!("Imported {} grpc_requests", imported_resources.grpc_requests.len()); Ok(imported_resources) } @@ -2211,7 +2222,7 @@ fn timestamp_for_upsert(update_source: &UpdateSource, dt: NaiveDateTime) -> Naiv } else { dt } - }, + } // Other sources will always update to the latest time _ => Utc::now().naive_utc(), } diff --git a/src-web/components/ConfirmLargeResponse.tsx b/src-web/components/ConfirmLargeResponse.tsx new file mode 100644 index 00000000..c6ab1b92 --- /dev/null +++ b/src-web/components/ConfirmLargeResponse.tsx @@ -0,0 +1,60 @@ +import type { HttpResponse } from '@yaakapp-internal/models'; +import { useMemo, type ReactNode } from 'react'; +import { useSaveResponse } from '../hooks/useSaveResponse'; +import { useToggle } from '../hooks/useToggle'; +import { isProbablyTextContentType } from '../lib/contentType'; +import { getContentTypeHeader } from '../lib/model_util'; +import { getResponseBodyText } from '../lib/responseBody'; +import { CopyButton } from './CopyButton'; +import { Banner } from './core/Banner'; +import { Button } from './core/Button'; +import { InlineCode } from './core/InlineCode'; +import { SizeTag } from './core/SizeTag'; +import { HStack } from './core/Stacks'; + +interface Props { + children: ReactNode; + response: HttpResponse; +} + +const LARGE_TEXT_BYTES = 2 * 1000 * 1000; +const LARGE_OTHER_BYTES = 10 * 1000 * 1000; + +export function ConfirmLargeResponse({ children, response }: Props) { + const { mutate: saveResponse } = useSaveResponse(response); + const [showLargeResponse, toggleShowLargeResponse] = useToggle(); + const isProbablyText = useMemo(() => { + const contentType = getContentTypeHeader(response.headers); + return isProbablyTextContentType(contentType); + }, [response.headers]); + + const contentLength = response.contentLength ?? 0; + const tooLargeBytes = isProbablyText ? LARGE_TEXT_BYTES : LARGE_OTHER_BYTES; + const isLarge = contentLength > tooLargeBytes; + if (!showLargeResponse && isLarge) { + return ( + +

+ Showing responses over{' '} + + + {' '} + may impact performance +

+ + + + {isProbablyText && ( + getResponseBodyText(response)} /> + )} + +
+ ); + } + + return <>{children}; +} diff --git a/src-web/components/CopyButton.tsx b/src-web/components/CopyButton.tsx index 852da8c2..e5a892f9 100644 --- a/src-web/components/CopyButton.tsx +++ b/src-web/components/CopyButton.tsx @@ -1,10 +1,11 @@ import { useCopy } from '../hooks/useCopy'; import { useTimedBoolean } from '../hooks/useTimedBoolean'; +import { showToast } from '../lib/toast'; import type { ButtonProps } from './core/Button'; import { Button } from './core/Button'; -interface Props extends ButtonProps { - text: string; +interface Props extends Omit { + text: string | (() => Promise); } export function CopyButton({ text, ...props }: Props) { @@ -13,9 +14,18 @@ export function CopyButton({ text, ...props }: Props) { return ( - - copy(text)} text={text} /> - - - ); - } - if (formattedBody.data == null) { return null; } diff --git a/src-web/lib/contentType.ts b/src-web/lib/contentType.ts index 23fe0e0a..2122d265 100644 --- a/src-web/lib/contentType.ts +++ b/src-web/lib/contentType.ts @@ -1,3 +1,4 @@ +import MimeType from 'whatwg-mimetype'; import type { EditorProps } from '../components/core/Editor/Editor'; export function languageFromContentType( @@ -40,10 +41,39 @@ export function isJSON(content: string | null | undefined): boolean { if (typeof content !== 'string') return false; try { - JSON.parse(content) + JSON.parse(content); return true; // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (err) { return false; } } + +export function isProbablyTextContentType(contentType: string | null): boolean { + if (contentType == null) return false; + + const mimeType = getMimeTypeFromContentType(contentType).essence; + const normalized = mimeType.toLowerCase(); + + // Check if it starts with "text/" + if (normalized.startsWith('text/')) { + return true; + } + + // Common text mimetypes and suffixes + return [ + 'application/json', + 'application/xml', + 'application/javascript', + 'application/yaml', + '+json', + '+xml', + '+yaml', + '+text', + ].some((textType) => normalized === textType || normalized.endsWith(textType)); +} + +export function getMimeTypeFromContentType(contentType: string) { + const mimeType = new MimeType(contentType); + return mimeType; +} diff --git a/src-web/lib/model_util.ts b/src-web/lib/model_util.ts index 0f74d596..6bcbc86a 100644 --- a/src-web/lib/model_util.ts +++ b/src-web/lib/model_util.ts @@ -5,7 +5,7 @@ import type { HttpResponse, HttpResponseHeader, } from '@yaakapp-internal/models'; -import MimeType from 'whatwg-mimetype'; +import { getMimeTypeFromContentType } from './contentType'; export const BODY_TYPE_NONE = null; export const BODY_TYPE_GRAPHQL = 'graphql'; @@ -61,6 +61,6 @@ export function getCharsetFromContentType(headers: HttpResponseHeader[]): string const contentType = getContentTypeHeader(headers); if (contentType == null) return null; - const mimeType = new MimeType(contentType); + const mimeType = getMimeTypeFromContentType(contentType); return mimeType.parameters.get('charset') ?? null; }