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
2426export 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 ) ;
0 commit comments