mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-11 22:40:26 +01:00
Split up HTTP sending logic (#320)
This commit is contained in:
@@ -128,7 +128,7 @@ function ExportDataDialogContent({
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<DetailsBanner color="secondary" open summary="Extra Settings">
|
||||
<DetailsBanner color="secondary" defaultOpen summary="Extra Settings">
|
||||
<Checkbox
|
||||
checked={includePrivateEnvironments}
|
||||
onChange={setIncludePrivateEnvironments}
|
||||
|
||||
@@ -188,7 +188,10 @@ function HttpRequestCard({ request }: { request: HttpRequest }) {
|
||||
<span>•</span>
|
||||
<HttpResponseDurationTag response={latestResponse} />
|
||||
<span>•</span>
|
||||
<SizeTag contentLength={latestResponse.contentLength ?? 0} />
|
||||
<SizeTag
|
||||
contentLength={latestResponse.contentLength ?? 0}
|
||||
contentLengthCompressed={latestResponse.contentLength}
|
||||
/>
|
||||
</HStack>
|
||||
</button>
|
||||
) : (
|
||||
|
||||
@@ -34,11 +34,11 @@ export function HeadersEditor({
|
||||
const validInheritedHeaders =
|
||||
inheritedHeaders?.filter((pair) => pair.enabled && (pair.name || pair.value)) ?? [];
|
||||
return (
|
||||
<div className="@container w-full h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
<div className="@container w-full h-full grid grid-rows-[auto_minmax(0,1fr)] gap-y-1.5">
|
||||
{validInheritedHeaders.length > 0 ? (
|
||||
<DetailsBanner
|
||||
color="secondary"
|
||||
className="text-sm mb-1.5"
|
||||
className="text-sm"
|
||||
summary={
|
||||
<HStack>
|
||||
Inherited <CountBadge count={validInheritedHeaders.length} />
|
||||
|
||||
@@ -76,7 +76,8 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
||||
label: 'Headers',
|
||||
rightSlot: (
|
||||
<CountBadge
|
||||
count={activeResponse?.headers.filter((h) => h.name && h.value).length ?? 0}
|
||||
count2={activeResponse?.headers.length ?? 0}
|
||||
count={activeResponse?.requestHeaders.length ?? 0}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -85,7 +86,13 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
||||
label: 'Info',
|
||||
},
|
||||
],
|
||||
[activeResponse?.headers, mimeType, setViewMode, viewMode],
|
||||
[
|
||||
activeResponse?.headers,
|
||||
mimeType,
|
||||
setViewMode,
|
||||
viewMode,
|
||||
activeResponse?.requestHeaders.length,
|
||||
],
|
||||
);
|
||||
const activeTab = activeTabs?.[activeRequestId];
|
||||
const setActiveTab = useCallback(
|
||||
@@ -133,7 +140,10 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
||||
<span>•</span>
|
||||
<HttpResponseDurationTag response={activeResponse} />
|
||||
<span>•</span>
|
||||
<SizeTag contentLength={activeResponse.contentLength ?? 0} />
|
||||
<SizeTag
|
||||
contentLength={activeResponse.contentLength ?? 0}
|
||||
contentLengthCompressed={activeResponse.contentLengthCompressed}
|
||||
/>
|
||||
|
||||
<div className="ml-auto">
|
||||
<RecentHttpResponsesDropdown
|
||||
@@ -146,72 +156,87 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{activeResponse?.error ? (
|
||||
<Banner color="danger" className="m-2">
|
||||
{activeResponse.error}
|
||||
</Banner>
|
||||
) : (
|
||||
<Tabs
|
||||
key={activeRequestId} // Freshen tabs on request change
|
||||
value={activeTab}
|
||||
onChangeValue={setActiveTab}
|
||||
tabs={tabs}
|
||||
label="Response"
|
||||
className="ml-3 mr-3 mb-3"
|
||||
tabListClassName="mt-0.5"
|
||||
>
|
||||
<TabContent value={TAB_BODY}>
|
||||
<ErrorBoundary name="Http Response Viewer">
|
||||
<Suspense>
|
||||
<ConfirmLargeResponse response={activeResponse}>
|
||||
{activeResponse.state === 'initialized' ? (
|
||||
<EmptyStateText>
|
||||
<VStack space={3}>
|
||||
<HStack space={3}>
|
||||
<LoadingIcon className="text-text-subtlest" />
|
||||
Sending Request
|
||||
</HStack>
|
||||
<Button size="sm" variant="border" onClick={() => cancel.mutate()}>
|
||||
Cancel
|
||||
</Button>
|
||||
</VStack>
|
||||
</EmptyStateText>
|
||||
) : activeResponse.state === 'closed' &&
|
||||
activeResponse.contentLength === 0 ? (
|
||||
<EmptyStateText>Empty </EmptyStateText>
|
||||
) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? (
|
||||
<EventStreamViewer response={activeResponse} />
|
||||
) : mimeType?.match(/^image\/svg/) ? (
|
||||
<SvgViewer response={activeResponse} />
|
||||
) : mimeType?.match(/^image/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} Component={ImageViewer} />
|
||||
) : mimeType?.match(/^audio/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} Component={AudioViewer} />
|
||||
) : mimeType?.match(/^video/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} Component={VideoViewer} />
|
||||
) : mimeType?.match(/pdf/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} Component={PdfViewer} />
|
||||
) : mimeType?.match(/csv|tab-separated/i) ? (
|
||||
<CsvViewer className="pb-2" response={activeResponse} />
|
||||
) : (
|
||||
<HTMLOrTextViewer
|
||||
textViewerClassName="-mr-2 bg-surface" // Pull to the right
|
||||
response={activeResponse}
|
||||
pretty={viewMode === 'pretty'}
|
||||
/>
|
||||
)}
|
||||
</ConfirmLargeResponse>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_HEADERS}>
|
||||
<ResponseHeaders response={activeResponse} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_INFO}>
|
||||
<ResponseInfo response={activeResponse} />
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
)}
|
||||
<div className="overflow-hidden flex flex-col min-h-0">
|
||||
{activeResponse?.error && (
|
||||
<Banner color="danger" className="mx-3 mt-1 flex-shrink-0">
|
||||
{activeResponse.error}
|
||||
</Banner>
|
||||
)}
|
||||
{/* Show tabs if we have any data (headers, body, etc.) even if there's an error */}
|
||||
{(activeResponse?.headers.length > 0 ||
|
||||
activeResponse?.bodyPath ||
|
||||
!activeResponse?.error) && (
|
||||
<Tabs
|
||||
key={activeRequestId} // Freshen tabs on request change
|
||||
value={activeTab}
|
||||
onChangeValue={setActiveTab}
|
||||
tabs={tabs}
|
||||
label="Response"
|
||||
className="ml-3 mr-3 mb-3 min-h-0 flex-1"
|
||||
tabListClassName="mt-0.5"
|
||||
>
|
||||
<TabContent value={TAB_BODY}>
|
||||
<ErrorBoundary name="Http Response Viewer">
|
||||
<Suspense>
|
||||
<ConfirmLargeResponse response={activeResponse}>
|
||||
{activeResponse.state === 'initialized' ? (
|
||||
<EmptyStateText>
|
||||
<VStack space={3}>
|
||||
<HStack space={3}>
|
||||
<LoadingIcon className="text-text-subtlest" />
|
||||
Sending Request
|
||||
</HStack>
|
||||
<Button size="sm" variant="border" onClick={() => cancel.mutate()}>
|
||||
Cancel
|
||||
</Button>
|
||||
</VStack>
|
||||
</EmptyStateText>
|
||||
) : activeResponse.state === 'closed' &&
|
||||
activeResponse.contentLength === 0 ? (
|
||||
<EmptyStateText>Empty </EmptyStateText>
|
||||
) : mimeType?.match(/^text\/event-stream/i) && viewMode === 'pretty' ? (
|
||||
<EventStreamViewer response={activeResponse} />
|
||||
) : mimeType?.match(/^image\/svg/) ? (
|
||||
<SvgViewer response={activeResponse} />
|
||||
) : mimeType?.match(/^image/i) ? (
|
||||
<EnsureCompleteResponse
|
||||
response={activeResponse}
|
||||
Component={ImageViewer}
|
||||
/>
|
||||
) : mimeType?.match(/^audio/i) ? (
|
||||
<EnsureCompleteResponse
|
||||
response={activeResponse}
|
||||
Component={AudioViewer}
|
||||
/>
|
||||
) : mimeType?.match(/^video/i) ? (
|
||||
<EnsureCompleteResponse
|
||||
response={activeResponse}
|
||||
Component={VideoViewer}
|
||||
/>
|
||||
) : mimeType?.match(/pdf/i) ? (
|
||||
<EnsureCompleteResponse response={activeResponse} Component={PdfViewer} />
|
||||
) : mimeType?.match(/csv|tab-separated/i) ? (
|
||||
<CsvViewer className="pb-2" response={activeResponse} />
|
||||
) : (
|
||||
<HTMLOrTextViewer
|
||||
textViewerClassName="-mr-2 bg-surface" // Pull to the right
|
||||
response={activeResponse}
|
||||
pretty={viewMode === 'pretty'}
|
||||
/>
|
||||
)}
|
||||
</ConfirmLargeResponse>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_HEADERS}>
|
||||
<ResponseHeaders response={activeResponse} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_INFO}>
|
||||
<ResponseInfo response={activeResponse} />
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||
import { useMemo } from 'react';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { DetailsBanner } from './core/DetailsBanner';
|
||||
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
|
||||
|
||||
interface Props {
|
||||
@@ -7,20 +9,57 @@ interface Props {
|
||||
}
|
||||
|
||||
export function ResponseHeaders({ response }: Props) {
|
||||
const sortedHeaders = useMemo(
|
||||
() => [...response.headers].sort((a, b) => a.name.localeCompare(b.name)),
|
||||
const responseHeaders = useMemo(
|
||||
() =>
|
||||
[...response.headers].sort((a, b) =>
|
||||
a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()),
|
||||
),
|
||||
[response.headers],
|
||||
);
|
||||
const requestHeaders = useMemo(
|
||||
() =>
|
||||
[...response.requestHeaders].sort((a, b) =>
|
||||
a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()),
|
||||
),
|
||||
[response.requestHeaders],
|
||||
);
|
||||
return (
|
||||
<div className="overflow-auto h-full pb-4">
|
||||
<KeyValueRows>
|
||||
{sortedHeaders.map((h, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: none
|
||||
<KeyValueRow labelColor="primary" key={i} label={h.name}>
|
||||
{h.value}
|
||||
</KeyValueRow>
|
||||
))}
|
||||
</KeyValueRows>
|
||||
<div className="overflow-auto h-full pb-4 gap-y-3 flex flex-col pr-0.5">
|
||||
<DetailsBanner
|
||||
defaultOpen
|
||||
storageKey={`${response.requestId}.response_headers`}
|
||||
summary={
|
||||
<h2 className="flex items-center">
|
||||
Response <CountBadge count={responseHeaders.length} />
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<KeyValueRows>
|
||||
{responseHeaders.map((h, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: none
|
||||
<KeyValueRow labelColor="primary" key={i} label={h.name}>
|
||||
{h.value}
|
||||
</KeyValueRow>
|
||||
))}
|
||||
</KeyValueRows>
|
||||
</DetailsBanner>
|
||||
<DetailsBanner
|
||||
storageKey={`${response.requestId}.request_headers`}
|
||||
summary={
|
||||
<h2 className="flex items-center">
|
||||
Request <CountBadge count={requestHeaders.length} />
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<KeyValueRows>
|
||||
{requestHeaders.map((h, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: none
|
||||
<KeyValueRow labelColor="primary" key={i} label={h.name}>
|
||||
{h.value}
|
||||
</KeyValueRow>
|
||||
))}
|
||||
</KeyValueRows>
|
||||
</DetailsBanner>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ function CertificateEditor({ certificate, index, onUpdate, onRemove }: Certifica
|
||||
|
||||
return (
|
||||
<DetailsBanner
|
||||
open={defaultOpen.current}
|
||||
defaultOpen={defaultOpen.current}
|
||||
summary={
|
||||
<HStack alignItems="center" justifyContent="between" space={2} className="w-full">
|
||||
<HStack space={1.5}>
|
||||
|
||||
@@ -61,7 +61,6 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
'x-theme-button',
|
||||
`x-theme-button--${variant}`,
|
||||
`x-theme-button--${variant}--${color}`,
|
||||
'text-text',
|
||||
'border', // They all have borders to ensure the same width
|
||||
'max-w-full min-w-0', // Help with truncation
|
||||
'hocus:opacity-100', // Force opacity for certain hover effects
|
||||
@@ -81,7 +80,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
variant === 'solid' && color === 'custom' && 'focus-visible:outline-2 outline-border-focus',
|
||||
variant === 'solid' &&
|
||||
color !== 'custom' &&
|
||||
'enabled:hocus:text-text enabled:hocus:bg-surface-highlight outline-border-subtle',
|
||||
'text-text enabled:hocus:text-text enabled:hocus:bg-surface-highlight outline-border-subtle',
|
||||
variant === 'solid' && color !== 'custom' && color !== 'default' && 'bg-surface',
|
||||
|
||||
// Borders
|
||||
|
||||
@@ -3,11 +3,12 @@ import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
count: number | true;
|
||||
count2?: number | true;
|
||||
className?: string;
|
||||
color?: Color;
|
||||
}
|
||||
|
||||
export function CountBadge({ count, className, color }: Props) {
|
||||
export function CountBadge({ count, count2, className, color }: Props) {
|
||||
if (count === 0) return null;
|
||||
return (
|
||||
<div
|
||||
@@ -30,6 +31,16 @@ export function CountBadge({ count, className, color }: Props) {
|
||||
) : (
|
||||
count
|
||||
)}
|
||||
{count2 != null && (
|
||||
<>
|
||||
/
|
||||
{count2 === true ? (
|
||||
<div aria-hidden className="rounded-full h-1 w-1 bg-[currentColor]" />
|
||||
) : (
|
||||
count2
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,48 @@
|
||||
import classNames from 'classnames';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { atomWithKVStorage } from '../../lib/atoms/atomWithKVStorage';
|
||||
import type { BannerProps } from './Banner';
|
||||
import { Banner } from './Banner';
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDetailsElement> {
|
||||
summary: ReactNode;
|
||||
color?: BannerProps['color'];
|
||||
open?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
storageKey?: string;
|
||||
}
|
||||
|
||||
export function DetailsBanner({ className, color, summary, children, ...extraProps }: Props) {
|
||||
export function DetailsBanner({
|
||||
className,
|
||||
color,
|
||||
summary,
|
||||
children,
|
||||
defaultOpen,
|
||||
storageKey,
|
||||
...extraProps
|
||||
}: Props) {
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We only want to recompute the atom when storageKey changes
|
||||
const openAtom = useMemo(
|
||||
() =>
|
||||
storageKey
|
||||
? atomWithKVStorage<boolean>(['details_banner', storageKey], defaultOpen ?? false)
|
||||
: atom(defaultOpen ?? false),
|
||||
[storageKey],
|
||||
);
|
||||
|
||||
const [isOpen, setIsOpen] = useAtom(openAtom);
|
||||
|
||||
const handleToggle = (e: React.SyntheticEvent<HTMLDetailsElement>) => {
|
||||
if (storageKey) {
|
||||
setIsOpen(e.currentTarget.open);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Banner color={color} className={className}>
|
||||
<details className="group list-none" {...extraProps}>
|
||||
<summary className="!cursor-default !select-none list-none flex items-center gap-2 focus:outline-none opacity-70 hover:opacity-100 focus:opacity-100">
|
||||
<details className="group list-none" open={isOpen} onToggle={handleToggle} {...extraProps}>
|
||||
<summary className="!cursor-default !select-none list-none flex items-center gap-3 focus:outline-none opacity-70">
|
||||
<div
|
||||
className={classNames(
|
||||
'transition-transform',
|
||||
|
||||
@@ -12,7 +12,9 @@ import {
|
||||
ArrowDownIcon,
|
||||
ArrowDownToDotIcon,
|
||||
ArrowDownToLineIcon,
|
||||
ArrowLeftIcon,
|
||||
ArrowRightCircleIcon,
|
||||
ArrowRightIcon,
|
||||
ArrowUpDownIcon,
|
||||
ArrowUpFromDotIcon,
|
||||
ArrowUpFromLineIcon,
|
||||
@@ -142,6 +144,8 @@ const icons = {
|
||||
arrow_down: ArrowDownIcon,
|
||||
arrow_down_to_dot: ArrowDownToDotIcon,
|
||||
arrow_down_to_line: ArrowDownToLineIcon,
|
||||
arrow_left: ArrowLeftIcon,
|
||||
arrow_right: ArrowRightIcon,
|
||||
arrow_right_circle: ArrowRightCircleIcon,
|
||||
arrow_up: ArrowUpIcon,
|
||||
arrow_up_down: ArrowUpDownIcon,
|
||||
|
||||
@@ -2,11 +2,18 @@ import { formatSize } from '@yaakapp-internal/lib/formatSize';
|
||||
|
||||
interface Props {
|
||||
contentLength: number;
|
||||
contentLengthCompressed?: number | null;
|
||||
}
|
||||
|
||||
export function SizeTag({ contentLength }: Props) {
|
||||
export function SizeTag({ contentLength, contentLengthCompressed }: Props) {
|
||||
return (
|
||||
<span className="font-mono" title={`${contentLength} bytes`}>
|
||||
<span
|
||||
className="font-mono"
|
||||
title={
|
||||
`${contentLength} bytes` +
|
||||
(contentLengthCompressed ? `\n${contentLengthCompressed} bytes compressed` : '')
|
||||
}
|
||||
>
|
||||
{formatSize(contentLength)}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const encodings = ['*', 'gzip', 'compress', 'deflate', 'br', 'identity'];
|
||||
export const encodings = ['*', 'gzip', 'compress', 'deflate', 'br', 'zstd', 'identity'];
|
||||
|
||||
Reference in New Issue
Block a user