From 17dc1991f19d538793fb35f20c1dec885e8f8a24 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 3 Feb 2025 07:05:14 -0800 Subject: [PATCH] Auto-scroll component for websocket/grpc/sse --- src-tauri/yaak-ws/src/cmd.rs | 6 +- src-web/components/EmptyStateText.tsx | 18 ++-- .../components/GrpcConnectionMessagesPane.tsx | 40 ++++---- src-web/components/HttpRequestPane.tsx | 2 +- src-web/components/HttpResponsePane.tsx | 4 +- src-web/components/WebsocketResponsePane.tsx | 98 ++++++++++--------- src-web/components/core/AutoScroller.tsx | 89 +++++++++++++++++ src-web/components/core/Icon.tsx | 1 + src-web/components/core/JsonAttributeTree.tsx | 4 +- .../responseViewers/EventStreamViewer.tsx | 81 +++++---------- src-web/init/sync.ts | 2 +- 11 files changed, 208 insertions(+), 137 deletions(-) create mode 100644 src-web/components/core/AutoScroller.tsx diff --git a/src-tauri/yaak-ws/src/cmd.rs b/src-tauri/yaak-ws/src/cmd.rs index e3af7870..01519bc5 100644 --- a/src-tauri/yaak-ws/src/cmd.rs +++ b/src-tauri/yaak-ws/src/cmd.rs @@ -3,7 +3,7 @@ use crate::error::Result; use crate::manager::WebsocketManager; use crate::render::render_request; use chrono::Utc; -use log::info; +use log::{info, warn}; use std::str::FromStr; use tauri::http::{HeaderMap, HeaderName}; use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow}; @@ -143,7 +143,9 @@ pub(crate) async fn close( .ok_or(GenericError("WebSocket Request not found".to_string()))?; let mut ws_manager = ws_manager.lock().await; - ws_manager.send(&connection.id, Message::Close(None)).await?; + if let Err(e) = ws_manager.send(&connection.id, Message::Close(None)).await { + warn!("Failed to close WebSocket connection: {e:?}"); + }; upsert_websocket_event( &window, WebsocketEvent { diff --git a/src-web/components/EmptyStateText.tsx b/src-web/components/EmptyStateText.tsx index ab46801e..68676ab7 100644 --- a/src-web/components/EmptyStateText.tsx +++ b/src-web/components/EmptyStateText.tsx @@ -9,14 +9,16 @@ interface Props { export function EmptyStateText({ children, className }: Props) { return ( -
- {children} +
+
+ {children} +
); } diff --git a/src-web/components/GrpcConnectionMessagesPane.tsx b/src-web/components/GrpcConnectionMessagesPane.tsx index 6e757f20..6b8f8d78 100644 --- a/src-web/components/GrpcConnectionMessagesPane.tsx +++ b/src-web/components/GrpcConnectionMessagesPane.tsx @@ -2,11 +2,12 @@ import type { GrpcEvent, GrpcRequest } from '@yaakapp-internal/models'; import classNames from 'classnames'; import { format } from 'date-fns'; import type { CSSProperties } from 'react'; -import React, { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useCopy } from '../hooks/useCopy'; import { useGrpcEvents } from '../hooks/useGrpcEvents'; import { usePinnedGrpcConnection } from '../hooks/usePinnedGrpcConnection'; import { useStateWithDeps } from '../hooks/useStateWithDeps'; +import { AutoScroller } from './core/AutoScroller'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; import { Icon } from './core/Icon'; @@ -80,24 +81,25 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }: />
-
- {activeConnection.error && ( - - {activeConnection.error} - - )} - {...events.map((e) => ( + {activeConnection.error && ( + + {activeConnection.error} + + )} + ( { - if (e.id === activeEventId) setActiveEventId(null); - else setActiveEventId(e.id); + if (event.id === activeEventId) setActiveEventId(null); + else setActiveEventId(event.id); }} /> - ))} -
+ )} + /> ) } @@ -195,14 +197,16 @@ function EventRow({ event: GrpcEvent; }) { const { eventType, status, createdAt, content, error } = event; + const ref = useRef(null); + return ( -
+
) } @@ -113,30 +115,32 @@ export function WebsocketResponsePane({ activeRequest }: Props) {
-
+
{activeEvent.messageType === 'close' ? 'Connection Closed' : `Message ${activeEvent.isServer ? 'Received' : 'Sent'}`}
- - - copy(message)} - /> - + {message != '' && ( + + + copy(formattedMessage.data ?? '')} + /> + + )}
{!showLarge && activeEvent.message.length > 1000 * 1000 ? ( @@ -164,7 +168,7 @@ export function WebsocketResponsePane({ activeRequest }: Props) { ) : ( (null); const message = messageBytes ? new TextDecoder('utf-8').decode(Uint8Array.from(messageBytes)) : ''; + return ( -
+
) : ( <> - + {attrKey}: {labelEl} diff --git a/src-web/components/responseViewers/EventStreamViewer.tsx b/src-web/components/responseViewers/EventStreamViewer.tsx index fa795147..8158b3e8 100644 --- a/src-web/components/responseViewers/EventStreamViewer.tsx +++ b/src-web/components/responseViewers/EventStreamViewer.tsx @@ -1,12 +1,11 @@ -import { useVirtualizer } from '@tanstack/react-virtual'; import type { HttpResponse } from '@yaakapp-internal/models'; import type { ServerSentEvent } from '@yaakapp-internal/sse'; import classNames from 'classnames'; -import { motion } from 'framer-motion'; -import React, { Fragment, useMemo, useRef, useState } from 'react'; +import React, { Fragment, useMemo, useState } from 'react'; import { useFormatText } from '../../hooks/useFormatText'; import { useResponseBodyEventSource } from '../../hooks/useResponseBodyEventSource'; import { isJSON } from '../../lib/contentType'; +import { AutoScroller } from '../core/AutoScroller'; import { Button } from '../core/Button'; import type { EditorProps } from '../core/Editor/Editor'; import { Editor } from '../core/Editor/Editor'; @@ -52,7 +51,7 @@ function ActualEventStreamViewer({ response }: Props) { defaultRatio={0.4} minHeightPx={20} firstSlot={() => ( -
- + ; } -function EventStreamEventsVirtual({ +function EventStreamEvents({ events, activeEventIndex, setActiveEventIndex, @@ -122,53 +121,21 @@ function EventStreamEventsVirtual({ activeEventIndex: number | null; setActiveEventIndex: (eventId: number | null) => void; }) { - // The scrollable element for your list - const parentRef = useRef(null); - - // The virtualizer - const rowVirtualizer = useVirtualizer({ - count: events.length, - getScrollElement: () => parentRef.current, - estimateSize: () => 28, // react-virtual requires a height, so we'll give it one - }); - return ( -
-
- {rowVirtualizer.getVirtualItems().map((virtualItem) => { - const event = events[virtualItem.index]!; - return ( -
- { - if (virtualItem.index === activeEventIndex) setActiveEventIndex(null); - else setActiveEventIndex(virtualItem.index); - }} - /> -
- ); - })} -
-
+ ( + { + if (i === activeEventIndex) setActiveEventIndex(null); + else setActiveEventIndex(i); + }} + /> + )} + /> ); } @@ -186,22 +153,20 @@ function EventStreamEvent({ index: number; }) { return ( -
{event.data.slice(0, 1000)}
-
+ ); } diff --git a/src-web/init/sync.ts b/src-web/init/sync.ts index cbbda589..6fecb423 100644 --- a/src-web/init/sync.ts +++ b/src-web/init/sync.ts @@ -25,7 +25,7 @@ function initForWorkspace(workspaceId: string, syncDir: string | null) { const debouncedSync = debounce(() => { if (syncDir == null) return; syncWorkspace.mutate({ workspaceId, syncDir }); - }); + }, 1000); // Sync on model upsert const unsubUpsertedModels = listenToTauriEvent('upserted_model', (p) => {