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
- |
- {col}
- |
+
+ {parsed.meta.fields?.map((key) => (
+ {row[key] ?? ''}
))}
-
+
))}
-
-
+
+
);
}
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
;
+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 (
+
+ );
}
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",