Skip to content

Commit 4c1ca16

Browse files
committed
use virtualized window for logs
1 parent fd777a6 commit 4c1ca16

File tree

6 files changed

+224
-91
lines changed

6 files changed

+224
-91
lines changed

bun.lock

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@next/mdx": "15.3.3",
1313
"@opennextjs/cloudflare": "^1.3.0",
1414
"@tabler/icons-react": "3.34.0",
15+
"@tanstack/react-virtual": "^3.13.12",
1516
"axios": "^1.9.0",
1617
"motion": "^12.16.0",
1718
"next": "15.3.3",
@@ -28,7 +29,6 @@
2829
"@types/node": "^24.0.1",
2930
"@types/react": "19.1.6",
3031
"@types/react-dom": "19.1.6",
31-
"@types/react-window": "^1.8.8",
3232
"eslint": "^9.28.0",
3333
"eslint-config-next": "15.3.3",
3434
"eslint-config-prettier": "^10.1.5",
@@ -562,6 +562,10 @@
562562

563563
"@tailwindcss/typography": ["@tailwindcss/[email protected]", "", { "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA=="],
564564

565+
"@tanstack/react-virtual": ["@tanstack/[email protected]", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="],
566+
567+
"@tanstack/virtual-core": ["@tanstack/[email protected]", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="],
568+
565569
"@tsconfig/node18": ["@tsconfig/[email protected]", "", {}, "sha512-RbwvSJQsuN9TB04AQbGULYfOGE/RnSFk/FLQ5b0NmDf5Kx2q/lABZbHQPKCO1vZ6Fiwkplu+yb9pGdLy1iGseQ=="],
566570

567571
"@types/acorn": ["@types/[email protected]", "", { "dependencies": { "@types/estree": "*" } }, "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ=="],
@@ -590,8 +594,6 @@
590594

591595
"@types/react-dom": ["@types/[email protected]", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="],
592596

593-
"@types/react-window": ["@types/[email protected]", "", { "dependencies": { "@types/react": "*" } }, "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q=="],
594-
595597
"@types/unist": ["@types/[email protected]", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
596598

597599
"@types/uuid": ["@types/[email protected]", "", {}, "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="],

components/dump/dump-file-card.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import { CodeHighlight } from '@mantine/code-highlight';
2-
import { ActionIcon, Card, CardSection, Collapse, Text } from '@mantine/core';
2+
import {
3+
ActionIcon,
4+
Card,
5+
CardSection,
6+
Collapse,
7+
ScrollArea,
8+
Text,
9+
} from '@mantine/core';
310
import { IconChevronUp } from '@tabler/icons-react';
411
import { useState } from 'react';
12+
import { VirtualizedText } from './virtualized-text';
513

614
interface DumpFileCardProps {
715
title: string;
@@ -16,6 +24,12 @@ export default function DumpFileCard({
1624
}: DumpFileCardProps) {
1725
const [sectionOpen, setSectionOpen] = useState(false);
1826

27+
// Use virtualized text for large files that don't need syntax highlighting
28+
const shouldVirtualize =
29+
title === 'Latest Log' ||
30+
title === 'Command Map' ||
31+
title === 'Command Overrides';
32+
1933
return (
2034
<Card padding={0} radius='sm' withBorder>
2135
<CardSection
@@ -45,7 +59,12 @@ export default function DumpFileCard({
4559
</CardSection>
4660

4761
<Collapse in={sectionOpen}>
48-
<CodeHighlight code={content} language={language} />
62+
{shouldVirtualize ?
63+
<VirtualizedText content={content} maxHeight={400} />
64+
: <ScrollArea h={400} scrollbarSize={8}>
65+
<CodeHighlight code={content} language={language} />
66+
</ScrollArea>
67+
}
4968
</Collapse>
5069
</Card>
5170
);

components/dump/dump-plugins-card.tsx

Lines changed: 118 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
Card,
77
CardSection,
88
Collapse,
9+
ScrollArea,
910
Table,
1011
TableTh,
1112
TableThead,
@@ -19,7 +20,8 @@ import {
1920
IconChevronUp,
2021
IconSearch,
2122
} from '@tabler/icons-react';
22-
import { useState } from 'react';
23+
import { useVirtualizer } from '@tanstack/react-virtual';
24+
import { useMemo, useRef, useState } from 'react';
2325

2426
export default function DumpPluginsCard({
2527
title,
@@ -31,9 +33,99 @@ export default function DumpPluginsCard({
3133
const [sectionOpen, setSectionOpen] = useState(false);
3234
const [expandedPlugin, setExpandedPlugin] = useState<string | null>(null);
3335
const [searchQuery, setSearchQuery] = useState('');
36+
const parentRef = useRef<HTMLDivElement>(null);
3437

35-
const filteredPlugins = plugins.filter(plugin =>
36-
plugin.name.toLowerCase().includes(searchQuery.toLowerCase()),
38+
const filteredPlugins = useMemo(
39+
() =>
40+
plugins.filter(plugin =>
41+
plugin.name.toLowerCase().includes(searchQuery.toLowerCase()),
42+
),
43+
[plugins, searchQuery],
44+
);
45+
46+
const shouldVirtualize = filteredPlugins.length > 50;
47+
48+
const virtualizer = useVirtualizer({
49+
count: filteredPlugins.length,
50+
getScrollElement: () => parentRef.current,
51+
estimateSize: () => 60, // Approximate height of each plugin row
52+
overscan: 5,
53+
});
54+
55+
const renderPlugin = (plugin: DumpPlugin) => (
56+
<div key={plugin.name}>
57+
<div
58+
style={{
59+
padding: '12px 16px',
60+
display: 'flex',
61+
alignItems: 'center',
62+
borderBottom: '1px solid #e9ecef',
63+
cursor: 'pointer',
64+
}}
65+
onClick={() =>
66+
setExpandedPlugin(expandedPlugin === plugin.name ? null : plugin.name)
67+
}
68+
>
69+
<ActionIcon
70+
color='red'
71+
variant='transparent'
72+
style={{ marginRight: '8px' }}
73+
>
74+
{expandedPlugin === plugin.name ?
75+
<IconChevronDown size={16} />
76+
: <IconChevronRight size={16} />}
77+
</ActionIcon>
78+
79+
<div style={{ display: 'flex', width: '100%' }}>
80+
<Text style={{ flex: '40%' }}>{plugin.name}</Text>
81+
<Text style={{ flex: '30%' }}>{plugin.version}</Text>
82+
<div style={{ flex: '30%' }}>
83+
<Badge
84+
color={plugin.enabled ? 'green' : 'red'}
85+
variant='filled'
86+
style={{
87+
textTransform: 'uppercase',
88+
fontWeight: 'normal',
89+
fontSize: '0.75rem',
90+
}}
91+
>
92+
{plugin.enabled ? 'Enabled' : 'Disabled'}
93+
</Badge>
94+
</div>
95+
</div>
96+
</div>
97+
98+
<Collapse in={expandedPlugin === plugin.name}>
99+
<Box p='md' style={{ borderBottom: '1px solid #e9ecef' }}>
100+
{plugin.description !== undefined && (
101+
<div style={{ marginBottom: '10px' }}>
102+
<Text fw={700} component='span'>
103+
Description:{' '}
104+
</Text>
105+
<Text component='span'>{plugin.description}</Text>
106+
</div>
107+
)}
108+
109+
{plugin.authors !== undefined && (
110+
<div style={{ marginBottom: '10px' }}>
111+
<Text fw={700} component='span'>
112+
Authors:{' '}
113+
</Text>
114+
<Text component='span'>{plugin.authors.join(', ')}</Text>
115+
</div>
116+
)}
117+
118+
{plugin.main !== undefined && (
119+
<div>
120+
<Text fw={700} component='span'>
121+
Main class:{' '}
122+
</Text>
123+
<Text component='span'>{plugin.main}</Text>
124+
</div>
125+
)}
126+
</Box>
127+
</Collapse>
128+
</div>
37129
);
38130

39131
return (
@@ -84,85 +176,32 @@ export default function DumpPluginsCard({
84176
/>
85177
</Box>
86178

87-
<div>
88-
{filteredPlugins.map(plugin => (
89-
<div key={plugin.name}>
90-
<div
91-
style={{
92-
padding: '12px 16px',
93-
display: 'flex',
94-
alignItems: 'center',
95-
borderBottom: '1px solid #e9ecef',
96-
cursor: 'pointer',
97-
}}
98-
onClick={() =>
99-
setExpandedPlugin(
100-
expandedPlugin === plugin.name ? null : plugin.name,
101-
)
102-
}
103-
>
104-
<ActionIcon
105-
color='red'
106-
variant='transparent'
107-
style={{ marginRight: '8px' }}
179+
{shouldVirtualize ?
180+
<ScrollArea ref={parentRef} h={400} scrollbarSize={8}>
181+
<div
182+
style={{
183+
height: virtualizer.getTotalSize(),
184+
width: '100%',
185+
position: 'relative',
186+
}}
187+
>
188+
{virtualizer.getVirtualItems().map(virtualItem => (
189+
<div
190+
key={virtualItem.key}
191+
style={{
192+
position: 'absolute',
193+
top: 0,
194+
left: 0,
195+
width: '100%',
196+
transform: `translateY(${virtualItem.start}px)`,
197+
}}
108198
>
109-
{expandedPlugin === plugin.name ?
110-
<IconChevronDown size={16} />
111-
: <IconChevronRight size={16} />}
112-
</ActionIcon>
113-
114-
<div style={{ display: 'flex', width: '100%' }}>
115-
<Text style={{ flex: '40%' }}>{plugin.name}</Text>
116-
<Text style={{ flex: '30%' }}>{plugin.version}</Text>
117-
<div style={{ flex: '30%' }}>
118-
<Badge
119-
color={plugin.enabled ? 'green' : 'red'}
120-
variant='filled'
121-
style={{
122-
textTransform: 'uppercase',
123-
fontWeight: 'normal',
124-
fontSize: '0.75rem',
125-
}}
126-
>
127-
{plugin.enabled ? 'Enabled' : 'Disabled'}
128-
</Badge>
129-
</div>
199+
{renderPlugin(filteredPlugins[virtualItem.index])}
130200
</div>
131-
</div>
132-
133-
<Collapse in={expandedPlugin === plugin.name}>
134-
<Box p='md' style={{ borderBottom: '1px solid #e9ecef' }}>
135-
{plugin.description !== undefined && (
136-
<div style={{ marginBottom: '10px' }}>
137-
<Text fw={700} component='span'>
138-
Description:{' '}
139-
</Text>
140-
<Text component='span'>{plugin.description}</Text>
141-
</div>
142-
)}
143-
144-
{plugin.authors !== undefined && (
145-
<div style={{ marginBottom: '10px' }}>
146-
<Text fw={700} component='span'>
147-
Authors:{' '}
148-
</Text>
149-
<Text component='span'>{plugin.authors.join(', ')}</Text>
150-
</div>
151-
)}
152-
153-
{plugin.main !== undefined && (
154-
<div>
155-
<Text fw={700} component='span'>
156-
Main class:{' '}
157-
</Text>
158-
<Text component='span'>{plugin.main}</Text>
159-
</div>
160-
)}
161-
</Box>
162-
</Collapse>
201+
))}
163202
</div>
164-
))}
165-
</div>
203+
</ScrollArea>
204+
: <div>{filteredPlugins.map(renderPlugin)}</div>}
166205
</Collapse>
167206
</Card>
168207
);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use client';
2+
3+
import { useVirtualizer } from '@tanstack/react-virtual';
4+
import { useMemo, useRef } from 'react';
5+
6+
interface VirtualizedTextProps {
7+
content: string;
8+
maxHeight?: number;
9+
lineHeight?: number;
10+
}
11+
12+
export function VirtualizedText({
13+
content,
14+
maxHeight = 400,
15+
lineHeight = 18,
16+
}: VirtualizedTextProps) {
17+
const parentRef = useRef<HTMLDivElement>(null);
18+
19+
const lines = useMemo(() => content.split('\n'), [content]);
20+
21+
const virtualizer = useVirtualizer({
22+
count: lines.length,
23+
getScrollElement: () => parentRef.current,
24+
estimateSize: () => lineHeight,
25+
overscan: 10,
26+
});
27+
28+
return (
29+
<div
30+
ref={parentRef}
31+
style={{
32+
height: maxHeight,
33+
overflow: 'auto',
34+
fontFamily:
35+
'ui-monospace, SFMono-Regular, "SF Mono", Monaco, Menlo, "Liberation Mono", "Courier New", monospace',
36+
fontSize: '13px',
37+
lineHeight: `${lineHeight}px`,
38+
border: '1px solid #e9ecef',
39+
borderRadius: '4px',
40+
}}
41+
>
42+
<div
43+
style={{
44+
height: virtualizer.getTotalSize(),
45+
width: 'max-content',
46+
minWidth: '100%',
47+
position: 'relative',
48+
}}
49+
>
50+
{virtualizer.getVirtualItems().map(virtualItem => (
51+
<div
52+
key={virtualItem.key}
53+
style={{
54+
position: 'absolute',
55+
top: 0,
56+
left: 0,
57+
minWidth: '100%',
58+
width: 'max-content',
59+
height: lineHeight,
60+
transform: `translateY(${virtualItem.start}px)`,
61+
padding: '2px 12px',
62+
whiteSpace: 'pre',
63+
display: 'flex',
64+
alignItems: 'center',
65+
}}
66+
>
67+
{lines[virtualItem.index] || ''}
68+
</div>
69+
))}
70+
</div>
71+
</div>
72+
);
73+
}

0 commit comments

Comments
 (0)