Started multi-part response viewer

This commit is contained in:
Gregory Schier
2025-12-06 06:47:09 -08:00
parent 7b78fac24e
commit 113d0dc3c7
8 changed files with 101 additions and 21 deletions

View File

@@ -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) {
<EnsureCompleteResponse response={activeResponse} Component={AudioViewer} />
) : mimeType?.match(/^video/i) ? (
<EnsureCompleteResponse response={activeResponse} Component={VideoViewer} />
) : mimeType?.match(/^multipart/i) ? (
<MultipartViewer response={activeResponse} />
) : mimeType?.match(/pdf/i) ? (
<EnsureCompleteResponse response={activeResponse} Component={PdfViewer} />
) : mimeType?.match(/csv|tab-separated/i) ? (

View File

@@ -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,

View File

@@ -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 (
<div className="overflow-auto h-full">
<CsvViewerInner text={body.data ?? null} className={className} />
</div>
);
}
export function CsvViewerInner({ text, className }: { text: string | null; className?: string }) {
const parsed = useMemo(() => {
if (body.data == null) return null;
return Papa.parse<string[]>(body.data);
}, [body]);
if (text == null) return null;
return Papa.parse<Record<string, string>>(text, { header: true, skipEmptyLines: true });
}, [text]);
if (parsed === null) return null;
return (
<div className="overflow-auto h-full">
<table className={classNames(className, 'text-sm')}>
<tbody>
<Table className={classNames(className, 'text-sm')}>
<TableHead>
<TableRow>
{parsed.meta.fields?.map((field) => (
<TableHeaderCell key={field}>{field}</TableHeaderCell>
))}
</TableRow>
</TableHead>
<TableBody>
{parsed.data.map((row, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: none
<tr key={i} className={classNames('border-l border-t', i > 0 && 'border-b')}>
{row.map((col, j) => (
// biome-ignore lint/suspicious/noArrayIndexKey: none
<td key={j} className="border-r px-1.5">
{col}
</td>
<TableRow key={i}>
{parsed.meta.fields?.map((key) => (
<TableCell key={key}>{row[key] ?? ''}</TableCell>
))}
</tr>
</TableRow>
))}
</tbody>
</table>
</TableBody>
</Table>
</div>
);
}

View File

@@ -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 <img src={src} alt="Response preview" className="max-w-full max-h-full pb-2" />;
export function ImageViewer({ className, ...props }: Props) {
const [src, setSrc] = useState<string>();
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 (
<img
src={src}
alt="Response preview"
className={classNames(className, 'max-w-full max-h-full')}
/>
);
}