Console 1889 remove react virtuoso in favor of tanstackvirtual (#7744)

This commit is contained in:
Jonathan Brennan 2026-02-27 04:10:50 -06:00 committed by GitHub
parent ade45f5a70
commit d6011f9c2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 147 additions and 43 deletions

View file

@ -133,7 +133,6 @@
"react-textarea-autosize": "8.5.9",
"react-toastify": "10.0.6",
"react-virtualized-auto-sizer": "1.0.25",
"react-virtuoso": "4.12.3",
"recharts": "2.15.1",
"regenerator-runtime": "0.14.1",
"snarkdown": "2.0.0",

View file

@ -1,8 +1,6 @@
import { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
import { format } from 'date-fns';
import { ReactElement, useCallback, useEffect, useState } from 'react';
import { useFormik } from 'formik';
import { useForm } from 'react-hook-form';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
import { useClient, useMutation, useQuery } from 'urql';
import { useDebouncedCallback } from 'use-debounce';
import { z } from 'zod';
@ -30,6 +28,7 @@ import { Separator } from '@/components/ui/separator';
import { Spinner } from '@/components/ui/spinner';
import { Switch } from '@/components/ui/switch';
import { useToast } from '@/components/ui/use-toast';
import { VirtualLogList } from '@/components/ui/virtual-log-list';
import { Tag } from '@/components/v2';
import { env } from '@/env/frontend';
import { DocumentType, FragmentType, graphql, useFragment } from '@/gql';
@ -1414,13 +1413,6 @@ function DebugOIDCIntegrationModal(props: {
const [isSubscribing, setIsSubscribing] = useResetState(true, [props.isOpen]);
const [logs, setLogs] = useResetState<Array<OIDCLogEventType>>([], [props.isOpen]);
const ref = useRef<VirtuosoHandle>(null);
useEffect(() => {
ref.current?.scrollToIndex({
index: logs.length - 1,
behavior: 'smooth',
});
}, [logs]);
useEffect(() => {
if (isSubscribing && props.oidcIntegrationId && props.isOpen) {
@ -1467,23 +1459,7 @@ function DebugOIDCIntegrationModal(props: {
Here you can listen to the live logs for debugging your OIDC integration.
</DialogDescription>
</DialogHeader>
<Virtuoso
ref={ref}
className="h-[300px]"
initialTopMostItemIndex={logs.length - 1}
followOutput
data={logs}
itemContent={(_, logRow) => {
return (
<div className="flex px-2 pb-1 font-mono text-xs">
<time dateTime={logRow.timestamp} className="pr-4">
{format(logRow.timestamp, 'HH:mm:ss')}
</time>
{logRow.message}
</div>
);
}}
/>
<VirtualLogList logs={logs} className="h-[300px]" />
<DialogFooter>
<Button type="button" onClick={props.close} tabIndex={0} variant="destructive">
Close

View file

@ -0,0 +1,70 @@
import { useEffect, useState } from 'react';
import type { Story, StoryDefault } from '@ladle/react';
import { VirtualLogList, type LogEntry } from './virtual-log-list';
export default {
title: 'UI / VirtualLogList',
} satisfies StoryDefault;
const sampleMessages = [
'OIDC discovery document fetched successfully',
'Token validation started for client_id=hive-console',
'ID token signature verified with RS256',
'Claims extracted: sub=user-123, email=user@example.com',
'User session created, redirecting to callback URL',
'Authorization code exchange completed',
'Refresh token rotation initiated',
'Access token issued, expires_in=3600',
'Userinfo endpoint called successfully',
'Logout request received, clearing session',
];
function generateLog(index: number): LogEntry {
const now = new Date();
now.setSeconds(now.getSeconds() - (100 - index));
return {
timestamp: now.toISOString(),
message: sampleMessages[index % sampleMessages.length],
};
}
const staticLogs: LogEntry[] = Array.from({ length: 100 }, (_, i) => generateLog(i));
export const Default: Story = () => (
<div className="p-8">
<p className="text-neutral-11 mb-2 text-sm">100 pre-loaded log entries (scrollable):</p>
<VirtualLogList logs={staticLogs} className="h-[300px]" />
</div>
);
export const StreamingLogs: Story = () => {
const [logs, setLogs] = useState<LogEntry[]>(() =>
Array.from({ length: 5 }, (_, i) => generateLog(i)),
);
useEffect(() => {
let index = 5;
const interval = setInterval(() => {
setLogs(prev => [...prev, generateLog(index++)]);
}, 500);
return () => clearInterval(interval);
}, []);
return (
<div className="p-8">
<p className="text-neutral-11 mb-2 text-sm">
Streaming logs (new entry every 500ms, auto-scrolls to bottom):
</p>
<p className="text-neutral-10 mb-4 text-xs">{logs.length} entries</p>
<VirtualLogList logs={logs} className="h-[300px]" />
</div>
);
};
export const Empty: Story = () => (
<div className="p-8">
<p className="text-neutral-11 mb-2 text-sm">Empty log list:</p>
<VirtualLogList logs={[]} className="h-[300px]" />
</div>
);

View file

@ -0,0 +1,74 @@
import { useEffect, useLayoutEffect, useRef } from 'react';
import { format } from 'date-fns';
import { useVirtualizer } from '@tanstack/react-virtual';
export type LogEntry = {
timestamp: string;
message: string;
};
const ITEM_HEIGHT = 24;
export function VirtualLogList(props: { logs: LogEntry[]; className?: string }) {
const { logs } = props;
const scrollRef = useRef<HTMLDivElement>(null);
const atBottomRef = useRef(true);
const virtualizer = useVirtualizer({
count: logs.length,
getScrollElement: () => scrollRef.current,
estimateSize: () => ITEM_HEIGHT,
overscan: 5,
});
useEffect(() => {
const scrollEl = scrollRef.current;
if (!scrollEl) return;
const handleScroll = () => {
const { scrollHeight, scrollTop, clientHeight } = scrollEl;
atBottomRef.current = scrollHeight - scrollTop - clientHeight < ITEM_HEIGHT;
};
scrollEl.addEventListener('scroll', handleScroll, { passive: true });
handleScroll();
return () => scrollEl.removeEventListener('scroll', handleScroll);
}, []);
useLayoutEffect(() => {
if (atBottomRef.current && logs.length > 0) {
virtualizer.scrollToIndex(logs.length - 1, { behavior: 'smooth', align: 'end' });
}
}, [logs.length, virtualizer]);
return (
<div ref={scrollRef} className={props.className} style={{ overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map(virtualItem => {
const logRow = logs[virtualItem.index];
return (
<div
key={virtualItem.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualItem.size,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<div className="flex px-2 pb-1 font-mono text-xs">
<time dateTime={logRow.timestamp} className="pr-4">
{format(logRow.timestamp, 'HH:mm:ss')}
</time>
{logRow.message}
</div>
</div>
);
})}
</div>
</div>
);
}

View file

@ -2061,9 +2061,6 @@ importers:
react-virtualized-auto-sizer:
specifier: 1.0.25
version: 1.0.25(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-virtuoso:
specifier: 4.12.3
version: 4.12.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
recharts:
specifier: 2.15.1
version: 2.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -16766,13 +16763,6 @@ packages:
react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0
react-virtuoso@4.12.3:
resolution: {integrity: sha512-6X1p/sU7hecmjDZMAwN+r3go9EVjofKhwkUbVlL8lXhBZecPv9XVCkZ/kBPYOr0Mv0Vl5+Ziwgexg9Kh7+NNXQ==}
engines: {node: '>=10'}
peerDependencies:
react: '>=16 || >=17 || >= 18'
react-dom: '>=16 || >=17 || >= 18'
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@ -40058,11 +40048,6 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-virtuoso@4.12.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react@18.3.1:
dependencies:
loose-envify: 1.4.0