mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-21 08:59:07 +01:00
Support binary responses!
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
interface Props {
|
||||
data: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function ImageView({ data }: Props) {
|
||||
// const dataUri = `data:image/png;base64,${window.btoa(data)}`;
|
||||
return <div>Image preview not supported until binary response support is added</div>;
|
||||
}
|
||||
@@ -5,26 +5,26 @@ import { createGlobalState } from 'react-use';
|
||||
import { useActiveRequestId } from '../hooks/useActiveRequestId';
|
||||
import { useDeleteResponse } from '../hooks/useDeleteResponse';
|
||||
import { useDeleteResponses } from '../hooks/useDeleteResponses';
|
||||
import { useResponseContentType } from '../hooks/useResponseContentType';
|
||||
import { useResponses } from '../hooks/useResponses';
|
||||
import { useResponseViewMode } from '../hooks/useResponseViewMode';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { isResponseLoading } from '../lib/models';
|
||||
import { pluralize } from '../lib/pluralize';
|
||||
import { Banner } from './core/Banner';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Editor } from './core/Editor';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { Webview } from './core/Webview';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { ImageView } from './ImageView';
|
||||
import { ResponseHeaders } from './ResponseHeaders';
|
||||
import { ImageViewer } from './responseViewers/ImageViewer';
|
||||
import { TextViewer } from './responseViewers/TextViewer';
|
||||
import { WebPageViewer } from './responseViewers/WebPageViewer';
|
||||
|
||||
interface Props {
|
||||
style?: CSSProperties;
|
||||
@@ -48,12 +48,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
// Unset pinned response when a new one comes in
|
||||
useEffect(() => setPinnedResponseId(null), [responses.length]);
|
||||
|
||||
const contentType = useMemo(
|
||||
() =>
|
||||
activeResponse?.headers.find((h) => h.name.toLowerCase() === 'content-type')?.value ??
|
||||
'text/plain',
|
||||
[activeResponse],
|
||||
);
|
||||
const contentType = useResponseContentType(activeResponse);
|
||||
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() => [
|
||||
@@ -84,9 +79,6 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
[activeResponse?.headers, setViewMode, viewMode],
|
||||
);
|
||||
|
||||
// Don't render until we know the view mode
|
||||
if (viewMode === undefined) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
@@ -119,10 +111,10 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
<span>{activeResponse.elapsed}ms</span>
|
||||
</>
|
||||
)}
|
||||
{activeResponse.body.length > 0 && (
|
||||
{activeResponse.contentLength && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span>{(activeResponse.body.length / 1000).toFixed(1)} KB</span>
|
||||
<span>{(activeResponse.contentLength / 1000).toFixed(1)} KB</span>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
@@ -169,49 +161,29 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChangeValue={setActiveTab}
|
||||
label="Response"
|
||||
tabs={tabs}
|
||||
className="ml-3 mr-1"
|
||||
tabListClassName="mt-1.5"
|
||||
>
|
||||
<TabContent value="headers">
|
||||
<ResponseHeaders headers={activeResponse?.headers ?? []} />
|
||||
</TabContent>
|
||||
<TabContent value="body">
|
||||
{!activeResponse.body ? (
|
||||
<EmptyStateText>Empty Body</EmptyStateText>
|
||||
) : viewMode === 'pretty' && contentType.includes('html') ? (
|
||||
<Webview
|
||||
body={activeResponse.body}
|
||||
contentType={contentType}
|
||||
url={activeResponse.url}
|
||||
/>
|
||||
) : viewMode === 'pretty' && contentType.includes('json') ? (
|
||||
<Editor
|
||||
readOnly
|
||||
forceUpdateKey={`pretty::${activeResponse.updatedAt}`}
|
||||
className="bg-gray-50 dark:!bg-gray-100"
|
||||
defaultValue={tryFormatJson(activeResponse?.body)}
|
||||
contentType={contentType}
|
||||
/>
|
||||
) : contentType.startsWith('image') ? (
|
||||
<ImageView data={activeResponse?.body} />
|
||||
) : activeResponse?.body ? (
|
||||
<Editor
|
||||
readOnly
|
||||
forceUpdateKey={activeResponse.updatedAt}
|
||||
className="bg-gray-50 dark:!bg-gray-100"
|
||||
defaultValue={activeResponse?.body}
|
||||
contentType={contentType}
|
||||
/>
|
||||
) : null}
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
}
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChangeValue={setActiveTab}
|
||||
label="Response"
|
||||
tabs={tabs}
|
||||
className="ml-3 mr-1"
|
||||
tabListClassName="mt-1.5"
|
||||
>
|
||||
<TabContent value="headers">
|
||||
<ResponseHeaders headers={activeResponse?.headers ?? []} />
|
||||
</TabContent>
|
||||
<TabContent value="body">
|
||||
{!activeResponse.contentLength ? (
|
||||
<EmptyStateText>Empty Body</EmptyStateText>
|
||||
) : viewMode === 'pretty' && contentType?.includes('html') ? (
|
||||
<WebPageViewer response={activeResponse} />
|
||||
) : contentType?.startsWith('image') ? (
|
||||
<ImageViewer response={activeResponse} />
|
||||
) : (
|
||||
<TextViewer response={activeResponse} pretty={viewMode === 'pretty'} />
|
||||
)}
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useRouteError } from 'react-router-dom';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { Button } from './core/Button';
|
||||
import { Heading } from './core/Heading';
|
||||
import { VStack } from './core/Stacks';
|
||||
@@ -8,6 +9,7 @@ export default function RouteError() {
|
||||
const stringified = JSON.stringify(error);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const message = (error as any).message ?? stringified;
|
||||
const routes = useAppRoutes();
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<VStack space={5} className="max-w-[30rem] !h-auto">
|
||||
@@ -16,7 +18,12 @@ export default function RouteError() {
|
||||
{message}
|
||||
</pre>
|
||||
<VStack space={2}>
|
||||
<Button to="/" color="primary">
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
routes.navigate('workspaces');
|
||||
}}
|
||||
>
|
||||
Go Home
|
||||
</Button>
|
||||
<Button color="secondary" onClick={() => window.location.reload()}>
|
||||
|
||||
@@ -67,8 +67,8 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
if (hasFocus) return;
|
||||
focusActiveRequest(selectedIndex ?? 0);
|
||||
}, [focusActiveRequest, hasFocus, selectedIndex]);
|
||||
focusActiveRequest();
|
||||
}, [focusActiveRequest, hasFocus]);
|
||||
|
||||
const handleBlur = useCallback(() => setHasFocus(false), []);
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
|
||||
className="w-8 mr-0.5"
|
||||
icon={loading ? 'update' : 'paperPlane'}
|
||||
spin={loading}
|
||||
disabled={loading}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -137,11 +137,11 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ classN
|
||||
];
|
||||
}, [
|
||||
workspaces,
|
||||
activeWorkspace?.name,
|
||||
deleteWorkspace.mutate,
|
||||
activeWorkspaceId,
|
||||
dialog,
|
||||
routes,
|
||||
prompt,
|
||||
activeWorkspace?.name,
|
||||
updateWorkspace,
|
||||
createWorkspace,
|
||||
]);
|
||||
|
||||
@@ -24,10 +24,10 @@ export interface EditorProps {
|
||||
type?: 'text' | 'password';
|
||||
className?: string;
|
||||
heightMode?: 'auto' | 'full';
|
||||
contentType?: string;
|
||||
contentType?: string | null;
|
||||
forceUpdateKey?: string;
|
||||
autoFocus?: boolean;
|
||||
defaultValue?: string;
|
||||
defaultValue?: string | null;
|
||||
placeholder?: string;
|
||||
tooltipContainer?: HTMLElement;
|
||||
useTemplating?: boolean;
|
||||
|
||||
@@ -32,7 +32,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
|
||||
) {
|
||||
const [confirmed, setConfirmed] = useTimedBoolean();
|
||||
const handleClick = useCallback(
|
||||
(e: MouseEvent<HTMLElement>) => {
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (showConfirm) setConfirmed();
|
||||
onClick?.(e);
|
||||
},
|
||||
|
||||
15
src-web/components/responseViewers/ImageViewer.tsx
Normal file
15
src-web/components/responseViewers/ImageViewer.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri';
|
||||
import type { HttpResponse } from '../../lib/models';
|
||||
|
||||
interface Props {
|
||||
response: HttpResponse;
|
||||
}
|
||||
|
||||
export function ImageViewer({ response }: Props) {
|
||||
if (response.bodyPath === null) {
|
||||
return <div>Empty response body</div>;
|
||||
}
|
||||
|
||||
const src = convertFileSrc(response.bodyPath);
|
||||
return <img src={src} alt="Response preview" />;
|
||||
}
|
||||
26
src-web/components/responseViewers/TextViewer.tsx
Normal file
26
src-web/components/responseViewers/TextViewer.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
|
||||
import { useResponseContentType } from '../../hooks/useResponseContentType';
|
||||
import { tryFormatJson } from '../../lib/formatters';
|
||||
import type { HttpResponse } from '../../lib/models';
|
||||
import { Editor } from '../core/Editor';
|
||||
|
||||
interface Props {
|
||||
response: HttpResponse;
|
||||
pretty: boolean;
|
||||
}
|
||||
|
||||
export function TextViewer({ response, pretty }: Props) {
|
||||
const contentType = useResponseContentType(response);
|
||||
const rawBody = useResponseBodyText(response) ?? '';
|
||||
const body = pretty && contentType?.includes('json') ? tryFormatJson(rawBody) : rawBody;
|
||||
|
||||
return (
|
||||
<Editor
|
||||
readOnly
|
||||
forceUpdateKey={body}
|
||||
className="bg-gray-50 dark:!bg-gray-100"
|
||||
defaultValue={body}
|
||||
contentType={contentType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
|
||||
import type { HttpResponse } from '../../lib/models';
|
||||
|
||||
interface Props {
|
||||
body: string;
|
||||
contentType: string;
|
||||
url: string;
|
||||
response: HttpResponse;
|
||||
}
|
||||
|
||||
export function Webview({ body, url, contentType }: Props) {
|
||||
export function WebPageViewer({ response }: Props) {
|
||||
const { url } = response;
|
||||
const body = useResponseBodyText(response) ?? '';
|
||||
|
||||
const contentForIframe: string | undefined = useMemo(() => {
|
||||
if (!contentType.includes('html')) return;
|
||||
if (body.includes('<head>')) {
|
||||
return body.replace(/<head>/gi, `<head><base href="${url}"/>`);
|
||||
}
|
||||
return body;
|
||||
}, [url, body, contentType]);
|
||||
}, [url, body]);
|
||||
|
||||
return (
|
||||
<div className="h-full pb-3">
|
||||
Reference in New Issue
Block a user