diff --git a/package-lock.json b/package-lock.json index dbd9863f..839b4af4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1700,6 +1700,21 @@ "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "license": "MIT" }, + "node_modules/@mjackson/headers": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@mjackson/headers/-/headers-0.11.1.tgz", + "integrity": "sha512-uXXhd4rtDdDwkqAuGef1nuafkCa1NlTmEc1Jzc0NL4YiA1yON1NFXuqJ3hOuKvNKQwkiDwdD+JJlKVyz4dunFA==", + "license": "MIT" + }, + "node_modules/@mjackson/multipart-parser": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@mjackson/multipart-parser/-/multipart-parser-0.10.1.tgz", + "integrity": "sha512-cHMD6+ErH/DrEfC0N6Ru/+1eAdavxdV0C35PzSb5/SD7z3XoaDMc16xPJcb8CahWjSpqHY+Too9sAb6/UNuq7A==", + "license": "MIT", + "dependencies": { + "@mjackson/headers": "^0.11.1" + } + }, "node_modules/@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -18680,6 +18695,7 @@ "@gilbarbara/deep-equal": "^0.3.1", "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.3", + "@mjackson/multipart-parser": "^0.10.1", "@prantlf/jsonlint": "^16.0.0", "@replit/codemirror-emacs": "^6.1.0", "@replit/codemirror-vim": "^6.3.0", diff --git a/src-web/components/HttpResponsePane.tsx b/src-web/components/HttpResponsePane.tsx index e29bda0e..410ef471 100644 --- a/src-web/components/HttpResponsePane.tsx +++ b/src-web/components/HttpResponsePane.tsx @@ -30,6 +30,7 @@ import { CsvViewer } from './responseViewers/CsvViewer'; import { EventStreamViewer } from './responseViewers/EventStreamViewer'; import { HTMLOrTextViewer } from './responseViewers/HTMLOrTextViewer'; import { ImageViewer } from './responseViewers/ImageViewer'; +import { MultipartViewer } from './responseViewers/MultipartViewer'; import { SvgViewer } from './responseViewers/SvgViewer'; import { VideoViewer } from './responseViewers/VideoViewer'; @@ -189,6 +190,8 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) { ) : mimeType?.match(/^video/i) ? ( + ) : mimeType?.match(/^multipart/i) ? ( + ) : mimeType?.match(/pdf/i) ? ( ) : mimeType?.match(/csv|tab-separated/i) ? ( diff --git a/src-web/components/core/Icon.tsx b/src-web/components/core/Icon.tsx index 5d206ada..8d6b6b49 100644 --- a/src-web/components/core/Icon.tsx +++ b/src-web/components/core/Icon.tsx @@ -53,6 +53,7 @@ import { EyeIcon, EyeOffIcon, FileCodeIcon, + FileIcon, FileTextIcon, FilterIcon, FlameIcon, @@ -180,7 +181,9 @@ const icons = { external_link: ExternalLinkIcon, eye: EyeIcon, eye_closed: EyeOffIcon, + file: FileIcon, file_code: FileCodeIcon, + file_text: FileTextIcon, filter: FilterIcon, flame: FlameIcon, flask: FlaskConicalIcon, diff --git a/src-web/components/responseViewers/CsvViewer.tsx b/src-web/components/responseViewers/CsvViewer.tsx index 9158494e..6664780c 100644 --- a/src-web/components/responseViewers/CsvViewer.tsx +++ b/src-web/components/responseViewers/CsvViewer.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import Papa from 'papaparse'; import { useMemo } from 'react'; import { useResponseBodyText } from '../../hooks/useResponseBodyText'; +import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from '../core/Table'; interface Props { response: HttpResponse; @@ -11,31 +12,42 @@ interface Props { export function CsvViewer({ response, className }: Props) { const body = useResponseBodyText({ response, filter: null }); + return ( +
+ +
+ ); +} +export function CsvViewerInner({ text, className }: { text: string | null; className?: string }) { const parsed = useMemo(() => { - if (body.data == null) return null; - return Papa.parse(body.data); - }, [body]); + if (text == null) return null; + return Papa.parse>(text, { header: true, skipEmptyLines: true }); + }, [text]); if (parsed === null) return null; return (
- - +
+ + + {parsed.meta.fields?.map((field) => ( + {field} + ))} + + + {parsed.data.map((row, i) => ( // biome-ignore lint/suspicious/noArrayIndexKey: none - 0 && 'border-b')}> - {row.map((col, j) => ( - // biome-ignore lint/suspicious/noArrayIndexKey: none - + + {parsed.meta.fields?.map((key) => ( + {row[key] ?? ''} ))} - + ))} - -
- {col} -
+ +
); } diff --git a/src-web/components/responseViewers/ImageViewer.tsx b/src-web/components/responseViewers/ImageViewer.tsx index 6967f27e..a584c7c4 100644 --- a/src-web/components/responseViewers/ImageViewer.tsx +++ b/src-web/components/responseViewers/ImageViewer.tsx @@ -1,10 +1,39 @@ import { convertFileSrc } from '@tauri-apps/api/core'; +import classNames from 'classnames'; +import { useEffect, useState } from 'react'; -interface Props { - bodyPath: string; -} +type Props = { className?: string } & ( + | { + bodyPath: string; + } + | { + data: ArrayBuffer; + } +); -export function ImageViewer({ bodyPath }: Props) { - const src = convertFileSrc(bodyPath); - return Response preview; +export function ImageViewer({ className, ...props }: Props) { + const [src, setSrc] = useState(); + const bodyPath = 'bodyPath' in props ? props.bodyPath : null; + const data = 'data' in props ? props.data : null; + + useEffect(() => { + if (bodyPath != null) { + setSrc(convertFileSrc(bodyPath)); + } else if (data != null) { + const blob = new Blob([data], { type: 'image/png' }); + const url = URL.createObjectURL(blob); + setSrc(url); + return () => URL.revokeObjectURL(url); + } else { + setSrc(undefined); + } + }, [bodyPath, data]); + + return ( + Response preview + ); } diff --git a/src-web/hooks/useResponseBodyText.ts b/src-web/hooks/useResponseBodyText.ts index 795ab6e5..68187d42 100644 --- a/src-web/hooks/useResponseBodyText.ts +++ b/src-web/hooks/useResponseBodyText.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import type { HttpResponse } from '@yaakapp-internal/models'; -import { getResponseBodyText } from '../lib/responseBody'; +import { getResponseBodyBytes, getResponseBodyText } from '../lib/responseBody'; export function useResponseBodyText({ response, @@ -21,3 +21,11 @@ export function useResponseBodyText({ queryFn: () => getResponseBodyText({ response, filter }), }); } + +export function useResponseBodyBytes({ response }: { response: HttpResponse }) { + return useQuery({ + placeholderData: (prev) => prev, // Keep previous data on refetch + queryKey: ['response_body_bytes', response.id, response.updatedAt, response.contentLength], + queryFn: () => getResponseBodyBytes(response), + }); +} diff --git a/src-web/lib/responseBody.ts b/src-web/lib/responseBody.ts index 164f4a6b..40a2a66f 100644 --- a/src-web/lib/responseBody.ts +++ b/src-web/lib/responseBody.ts @@ -1,3 +1,4 @@ +import { readFile } from '@tauri-apps/plugin-fs'; import type { HttpResponse } from '@yaakapp-internal/models'; import type { FilterResponse } from '@yaakapp-internal/plugins'; import type { ServerSentEvent } from '@yaakapp-internal/sse'; @@ -30,3 +31,10 @@ export async function getResponseBodyEventSource( filePath: response.bodyPath, }); } + +export async function getResponseBodyBytes( + response: HttpResponse, +): Promise | null> { + if (!response.bodyPath) return null; + return readFile(response.bodyPath); +} diff --git a/src-web/package.json b/src-web/package.json index 20f8ce78..8d21f01a 100644 --- a/src-web/package.json +++ b/src-web/package.json @@ -20,6 +20,7 @@ "@gilbarbara/deep-equal": "^0.3.1", "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.3", + "@mjackson/multipart-parser": "^0.10.1", "@prantlf/jsonlint": "^16.0.0", "@replit/codemirror-emacs": "^6.1.0", "@replit/codemirror-vim": "^6.3.0",