Url parameters for websocket URLs

This commit is contained in:
Gregory Schier
2025-02-03 11:40:19 -08:00
parent dd0516cc55
commit fcf2577430
27 changed files with 432 additions and 259 deletions

View File

@@ -81,13 +81,15 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
/>
</div>
</HStack>
{activeConnection.error && (
<Banner color="danger" className="m-3">
{activeConnection.error}
</Banner>
)}
<AutoScroller
data={events}
header={
activeConnection.error && (
<Banner color="danger" className="m-3">
{activeConnection.error}
</Banner>
)
}
render={(event) => (
<EventRow
key={event.id}

View File

@@ -85,13 +85,15 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
/>
</HStack>
</HStack>
{activeConnection.error && (
<Banner color="danger" className="m-3">
{activeConnection.error}
</Banner>
)}
<AutoScroller
data={events}
header={
activeConnection.error && (
<Banner color="danger" className="m-3">
{activeConnection.error}
</Banner>
)
}
render={(event) => (
<EventRow
key={event.id}

View File

@@ -2,6 +2,7 @@ import classNames from 'classnames';
import { motion } from 'framer-motion';
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import {duplicateWebsocketRequest} from "../commands/duplicateWebsocketRequest";
import {
useEnsureActiveCookieJar,
useSubscribeActiveCookieJarId,
@@ -253,10 +254,17 @@ function useGlobalWorkspaceHooks() {
useHotKey('http_request.duplicate', async () => {
const activeRequest = getActiveRequest();
if (activeRequest?.model === 'http_request') {
if (activeRequest == null) {
// Nothing
} else if (activeRequest.model === 'http_request') {
await duplicateHttpRequest.mutateAsync();
} else {
} else if (activeRequest.model === 'grpc_request') {
await duplicateGrpcRequest.mutateAsync();
} else if (activeRequest.model === 'websocket_request') {
await duplicateWebsocketRequest.mutateAsync(activeRequest);
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
throw new Error('Failed to duplicate invalid request model: ' + (activeRequest as any).model);
}
});
}

View File

@@ -1,14 +1,15 @@
import { useVirtualizer } from '@tanstack/react-virtual';
import type { ReactElement, UIEvent } from 'react';
import type { ReactElement, ReactNode, UIEvent } from 'react';
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { IconButton } from './IconButton';
interface Props<T> {
data: T[];
render: (item: T, index: number) => ReactElement<HTMLElement>;
header?: ReactNode;
}
export function AutoScroller<T>({ data, render }: Props<T>) {
export function AutoScroller<T>({ data, render, header }: Props<T>) {
const containerRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState<boolean>(true);
@@ -45,7 +46,7 @@ export function AutoScroller<T>({ data, render }: Props<T>) {
}, [autoScroll, data.length]);
return (
<div className="h-full w-full relative">
<div className="h-full w-full relative grid grid-rows-[auto_minmax(0,1fr)]">
{!autoScroll && (
<div className="absolute bottom-0 right-0 m-2">
<IconButton
@@ -59,6 +60,7 @@ export function AutoScroller<T>({ data, render }: Props<T>) {
/>
</div>
)}
{header ?? <span aria-hidden/>}
<div ref={containerRef} className="h-full w-full overflow-y-auto" onScroll={handleScroll}>
<div
style={{
@@ -75,7 +77,7 @@ export function AutoScroller<T>({ data, render }: Props<T>) {
top: 0,
left: 0,
width: '100%',
// height: `${virtualItem.size}px`,
height: `${virtualItem.size * 1000}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>

View File

@@ -9,19 +9,17 @@ interface Props {
export function Banner({ children, className, color }: Props) {
return (
<div>
<div
className={classNames(
className,
`x-theme-banner--${color}`,
'whitespace-pre-wrap',
'border border-dashed border-border bg-surface',
'px-3 py-2 rounded select-auto',
'overflow-x-auto text-text',
)}
>
{children}
</div>
<div
className={classNames(
className,
`x-theme-banner--${color}`,
'whitespace-pre-wrap',
'border border-dashed border-border bg-surface',
'px-3 py-2 rounded select-auto',
'overflow-auto text-text',
)}
>
{children}
</div>
);
}

View File

@@ -6,6 +6,7 @@ import { useFormatText } from '../../hooks/useFormatText';
import { useResponseBodyEventSource } from '../../hooks/useResponseBodyEventSource';
import { isJSON } from '../../lib/contentType';
import { AutoScroller } from '../core/AutoScroller';
import { Banner } from '../core/Banner';
import { Button } from '../core/Button';
import type { EditorProps } from '../core/Editor/Editor';
import { Editor } from '../core/Editor/Editor';
@@ -51,10 +52,26 @@ function ActualEventStreamViewer({ response }: Props) {
defaultRatio={0.4}
minHeightPx={20}
firstSlot={() => (
<EventStreamEvents
events={events.data ?? []}
activeEventIndex={activeEventIndex}
setActiveEventIndex={setActiveEventIndex}
<AutoScroller
data={events.data ?? []}
header={
events.error && (
<Banner color="danger" className="m-3">
{String(events.error)}
</Banner>
)
}
render={(event, i) => (
<EventRow
event={event}
isActive={i === activeEventIndex}
index={i}
onClick={() => {
if (i === activeEventIndex) setActiveEventIndex(null);
else setActiveEventIndex(i);
}}
/>
)}
/>
)}
secondSlot={
@@ -112,34 +129,7 @@ function FormattedEditor({ text, language }: { text: string; language: EditorPro
return <Editor readOnly defaultValue={formatted.data} language={language} stateKey={null} />;
}
function EventStreamEvents({
events,
activeEventIndex,
setActiveEventIndex,
}: {
events: ServerSentEvent[];
activeEventIndex: number | null;
setActiveEventIndex: (eventId: number | null) => void;
}) {
return (
<AutoScroller
data={events}
render={(event, i) => (
<EventStreamEvent
event={event}
isActive={i === activeEventIndex}
index={i}
onClick={() => {
if (i === activeEventIndex) setActiveEventIndex(null);
else setActiveEventIndex(i);
}}
/>
)}
/>
);
}
function EventStreamEvent({
function EventRow({
onClick,
isActive,
event,