11import { useState , useEffect , useRef , useMemo , useCallback } from "react" ;
22import { useAPI } from "../contexts/APIProvider" ;
33import { usePersistentState } from "../hooks/usePersistentState" ;
4+ import { Panel , PanelGroup , PanelResizeHandle } from "react-resizable-panels" ;
5+ import {
6+ RiTextWrap ,
7+ RiAlignJustify ,
8+ RiFontSize ,
9+ RiMenuSearchLine ,
10+ RiMenuSearchFill ,
11+ RiCloseCircleFill ,
12+ } from "react-icons/ri" ;
13+ import { useTheme } from "../contexts/ThemeProvider" ;
414
515const LogViewer = ( ) => {
616 const { proxyLogs, upstreamLogs } = useAPI ( ) ;
17+ const { isNarrow } = useTheme ( ) ;
18+ const direction = isNarrow ? "vertical" : "horizontal" ;
719
820 return (
9- < div className = "flex flex-col lg:flex-row gap-5" style = { { height : "calc(100vh - 125px)" } } >
10- < LogPanel id = "proxy" title = "Proxy Logs" logData = { proxyLogs } />
11- < LogPanel id = "upstream" title = "Upstream Logs" logData = { upstreamLogs } />
12- </ div >
21+ < PanelGroup direction = { direction } className = "gap-2" autoSaveId = { `logviewer-panel-group-${ direction } ` } >
22+ < Panel id = "proxy" defaultSize = { 50 } minSize = { 5 } maxSize = { 100 } collapsible = { true } >
23+ < LogPanel id = "proxy" title = "Proxy Logs" logData = { proxyLogs } />
24+ </ Panel >
25+ < PanelResizeHandle
26+ className = {
27+ direction === "horizontal"
28+ ? "w-2 h-full bg-primary hover:bg-success transition-colors rounded"
29+ : "w-full h-2 bg-primary hover:bg-success transition-colors rounded"
30+ }
31+ />
32+ < Panel id = "upstream" defaultSize = { 50 } minSize = { 5 } maxSize = { 100 } collapsible = { true } >
33+ < LogPanel id = "upstream" title = "Upstream Logs" logData = { upstreamLogs } />
34+ </ Panel >
35+ </ PanelGroup >
1336 ) ;
1437} ;
1538
1639interface LogPanelProps {
1740 id : string ;
1841 title : string ;
1942 logData : string ;
20- className ?: string ;
2143}
22-
23- export const LogPanel = ( { id, title, logData, className } : LogPanelProps ) => {
24- const [ isCollapsed , setIsCollapsed ] = usePersistentState ( `logPanel-${ id } -isCollapsed` , false ) ;
44+ export const LogPanel = ( { id, title, logData } : LogPanelProps ) => {
2545 const [ filterRegex , setFilterRegex ] = useState ( "" ) ;
2646 const [ fontSize , setFontSize ] = usePersistentState < "xxs" | "xs" | "small" | "normal" > (
2747 `logPanel-${ id } -fontSize` ,
2848 "normal"
2949 ) ;
3050 const [ wrapText , setTextWrap ] = usePersistentState ( `logPanel-${ id } -wrapText` , false ) ;
51+ const [ showFilter , setShowFilter ] = usePersistentState ( `logPanel-${ id } -showFilter` , false ) ;
3152
3253 const textWrapClass = useMemo ( ( ) => {
3354 return wrapText ? "whitespace-pre-wrap" : "whitespace-pre" ;
@@ -48,6 +69,19 @@ export const LogPanel = ({ id, title, logData, className }: LogPanelProps) => {
4869 } ) ;
4970 } , [ ] ) ;
5071
72+ const toggleWrapText = useCallback ( ( ) => {
73+ setTextWrap ( ( prev ) => ! prev ) ;
74+ } , [ ] ) ;
75+
76+ const toggleFilter = useCallback ( ( ) => {
77+ if ( showFilter ) {
78+ setShowFilter ( false ) ;
79+ setFilterRegex ( "" ) ; // Clear filter when closing
80+ } else {
81+ setShowFilter ( true ) ;
82+ }
83+ } , [ filterRegex , setFilterRegex , showFilter ] ) ;
84+
5185 const fontSizeClass = useMemo ( ( ) => {
5286 switch ( fontSize ) {
5387 case "xxs" :
@@ -81,58 +115,47 @@ export const LogPanel = ({ id, title, logData, className }: LogPanelProps) => {
81115 } , [ filteredLogs ] ) ;
82116
83117 return (
84- < div
85- className = { `bg-surface border border-border rounded-lg overflow-hidden flex flex-col ${
86- ! isCollapsed && "h-full"
87- } ${ className || "" } `}
88- >
118+ < div className = "bg-surface border border-border rounded-lg overflow-hidden flex flex-col h-full" >
89119 < div className = "p-4 border-b border-border bg-secondary" >
90- < div className = "flex flex-col md:flex-row md:items-center md:justify-between gap-4" >
91- { /* Title - Always full width on mobile, normal on desktop */ }
92- < div className = "w-full md:w-auto" onClick = { ( ) => setIsCollapsed ( ! isCollapsed ) } >
93- < h3 className = "m-0 text-lg p-0" > { title } </ h3 >
94- </ div >
120+ < div className = "flex items-center justify-between" >
121+ < h3 className = "m-0 text-lg p-0" > { title } </ h3 >
95122
96- { ! isCollapsed && (
97- < div className = "flex flex-col sm:flex-row gap-4 w-full md:w-auto" >
98- { /* Sizing Buttons - Stacks vertically on mobile */ }
99- < div className = "flex flex-wrap gap-2" >
100- < button className = "btn" onClick = { toggleFontSize } >
101- font: { fontSize }
102- </ button >
103- < button className = "btn" onClick = { ( ) => setTextWrap ( ( prev ) => ! prev ) } >
104- { wrapText ? "wrap" : "wrap off" }
105- </ button >
106- </ div >
123+ < div className = "flex gap-2 items-center" >
124+ < button className = "btn" onClick = { toggleFontSize } >
125+ < RiFontSize />
126+ </ button >
127+ < button className = "btn" onClick = { toggleWrapText } >
128+ { wrapText ? < RiTextWrap /> : < RiAlignJustify /> }
129+ </ button >
130+ < button className = "btn" onClick = { toggleFilter } >
131+ { showFilter ? < RiMenuSearchFill /> : < RiMenuSearchLine /> }
132+ </ button >
133+ </ div >
134+ </ div >
107135
108- { /* Filtering Options - Full width on mobile, normal on desktop */ }
109- < div className = "flex flex-1 min-w-0 gap-2" >
110- < input
111- type = "text"
112- className = "flex-1 min-w-[120px] text-sm border p-2 rounded"
113- placeholder = "Filter logs..."
114- value = { filterRegex }
115- onChange = { ( e ) => setFilterRegex ( e . target . value ) }
116- />
117- < button className = "btn" onClick = { ( ) => setFilterRegex ( "" ) } >
118- Clear
119- </ button >
120- </ div >
136+ { /* Filtering Options - Full width on mobile, normal on desktop */ }
137+ { showFilter && (
138+ < div className = "mt-2 w-full" >
139+ < div className = "flex gap-2 items-center w-full" >
140+ < input
141+ type = "text"
142+ className = "w-full text-sm border p-2 rounded"
143+ placeholder = "Filter logs..."
144+ value = { filterRegex }
145+ onChange = { ( e ) => setFilterRegex ( e . target . value ) }
146+ />
147+ < button className = "pl-2" onClick = { ( ) => setFilterRegex ( "" ) } >
148+ < RiCloseCircleFill size = "24" />
149+ </ button >
121150 </ div >
122- ) }
123- </ div >
151+ </ div >
152+ ) }
153+ </ div >
154+ < div className = "bg-background font-mono text-sm flex-1 overflow-hidden" >
155+ < pre ref = { preTagRef } className = { `${ textWrapClass } ${ fontSizeClass } h-full overflow-auto p-4` } >
156+ { filteredLogs }
157+ </ pre >
124158 </ div >
125-
126- { ! isCollapsed && (
127- < div className = "flex-1 bg-background font-mono text-sm p-3 overflow-hidden" >
128- < pre
129- ref = { preTagRef }
130- className = { `h-full p-4 overflow-y-auto whitespace-pre min-h-0 ${ textWrapClass } ${ fontSizeClass } ` }
131- >
132- { filteredLogs }
133- </ pre >
134- </ div >
135- ) }
136159 </ div >
137160 ) ;
138161} ;
0 commit comments