@@ -9,16 +9,21 @@ import {
99} from "@/components/ui/table" ;
1010import { LogEntry } from "@/hooks/useWebSocket" ;
1111import { format } from "date-fns" ;
12- import { Globe , Server , Router } from "lucide-react" ;
12+ import { Globe , Server , Router , Network , ExternalLink , ChevronUp , ChevronDown , ChevronsUpDown } from "lucide-react" ;
1313import { useEffect , useRef , useState } from "react" ;
1414
1515interface LogTableProps {
1616 logs : LogEntry [ ] ;
1717 requestLogs : ( params : { page : number , limit : number } ) => void ;
1818}
1919
20+ type SortColumn = 'method' | 'status' | 'responseTime' | 'serviceName' | 'routerName' | 'requestAddr' | 'requestHost' | 'clientIP' | 'location' ;
21+ type SortDirection = 'asc' | 'desc' | null ;
22+
2023export function LogTable ( { logs, requestLogs } : LogTableProps ) {
2124 const [ page , setPage ] = useState ( 1 ) ;
25+ const [ sortColumn , setSortColumn ] = useState < SortColumn | null > ( null ) ;
26+ const [ sortDirection , setSortDirection ] = useState < SortDirection > ( null ) ;
2227 const observer = useRef < IntersectionObserver | null > ( null ) ;
2328 const loader = useRef < HTMLTableRowElement | null > ( null ) ;
2429
@@ -46,6 +51,95 @@ export function LogTable({ logs, requestLogs }: LogTableProps) {
4651 }
4752 } , [ page , requestLogs ] ) ;
4853
54+ const handleSort = ( column : SortColumn ) => {
55+ if ( sortColumn === column ) {
56+ // Cycle through: asc -> desc -> none
57+ if ( sortDirection === 'asc' ) {
58+ setSortDirection ( 'desc' ) ;
59+ } else if ( sortDirection === 'desc' ) {
60+ setSortDirection ( null ) ;
61+ setSortColumn ( null ) ;
62+ }
63+ } else {
64+ setSortColumn ( column ) ;
65+ setSortDirection ( 'asc' ) ;
66+ }
67+ } ;
68+
69+ const getSortIcon = ( column : SortColumn ) => {
70+ if ( sortColumn !== column ) {
71+ return < ChevronsUpDown className = "h-4 w-4 text-muted-foreground" /> ;
72+ }
73+ if ( sortDirection === 'asc' ) {
74+ return < ChevronUp className = "h-4 w-4 text-foreground" /> ;
75+ }
76+ if ( sortDirection === 'desc' ) {
77+ return < ChevronDown className = "h-4 w-4 text-foreground" /> ;
78+ }
79+ return < ChevronsUpDown className = "h-4 w-4 text-muted-foreground" /> ;
80+ } ;
81+
82+ const sortedLogs = [ ...logs ] . sort ( ( a , b ) => {
83+ if ( ! sortColumn || ! sortDirection ) return 0 ;
84+
85+ let aValue : any ;
86+ let bValue : any ;
87+
88+ switch ( sortColumn ) {
89+ case 'method' :
90+ aValue = a . method ;
91+ bValue = b . method ;
92+ break ;
93+ case 'status' :
94+ aValue = a . status ;
95+ bValue = b . status ;
96+ break ;
97+ case 'responseTime' :
98+ aValue = a . responseTime ;
99+ bValue = b . responseTime ;
100+ break ;
101+ case 'serviceName' :
102+ aValue = a . serviceName ;
103+ bValue = b . serviceName ;
104+ break ;
105+ case 'routerName' :
106+ aValue = a . routerName ;
107+ bValue = b . routerName ;
108+ break ;
109+ case 'requestAddr' :
110+ aValue = a . requestAddr || '' ;
111+ bValue = b . requestAddr || '' ;
112+ break ;
113+ case 'requestHost' :
114+ aValue = a . requestHost || '' ;
115+ bValue = b . requestHost || '' ;
116+ break ;
117+ case 'clientIP' :
118+ aValue = a . clientIP ;
119+ bValue = b . clientIP ;
120+ break ;
121+ case 'location' :
122+ aValue = a . country || '' ;
123+ bValue = b . country || '' ;
124+ break ;
125+ default :
126+ return 0 ;
127+ }
128+
129+ // Handle different data types
130+ if ( typeof aValue === 'number' && typeof bValue === 'number' ) {
131+ return sortDirection === 'asc' ? aValue - bValue : bValue - aValue ;
132+ }
133+
134+ // String comparison
135+ const aStr = String ( aValue ) . toLowerCase ( ) ;
136+ const bStr = String ( bValue ) . toLowerCase ( ) ;
137+
138+ if ( aStr < bStr ) return sortDirection === 'asc' ? - 1 : 1 ;
139+ if ( aStr > bStr ) return sortDirection === 'asc' ? 1 : - 1 ;
140+ return 0 ;
141+ } ) ;
142+
49143
50144 const getStatusBadgeVariant = ( status : number ) => {
51145 if ( status >= 200 && status < 300 ) return "success" ;
@@ -83,26 +177,100 @@ export function LogTable({ logs, requestLogs }: LogTableProps) {
83177 < TableHeader >
84178 < TableRow >
85179 < TableHead > Time</ TableHead >
86- < TableHead > Method</ TableHead >
180+ < TableHead
181+ className = "cursor-pointer select-none hover:bg-muted/50 transition-colors"
182+ onClick = { ( ) => handleSort ( 'method' ) }
183+ >
184+ < div className = "flex items-center gap-1" >
185+ Method
186+ { getSortIcon ( 'method' ) }
187+ </ div >
188+ </ TableHead >
87189 < TableHead > Path</ TableHead >
88- < TableHead > Status</ TableHead >
89- < TableHead > Response Time</ TableHead >
90- < TableHead > Service</ TableHead >
91- < TableHead > Router</ TableHead >
92- < TableHead > Client IP</ TableHead >
93- < TableHead > Location</ TableHead >
190+ < TableHead
191+ className = "cursor-pointer select-none hover:bg-muted/50 transition-colors"
192+ onClick = { ( ) => handleSort ( 'status' ) }
193+ >
194+ < div className = "flex items-center gap-1" >
195+ Status
196+ { getSortIcon ( 'status' ) }
197+ </ div >
198+ </ TableHead >
199+ < TableHead
200+ className = "cursor-pointer select-none hover:bg-muted/50 transition-colors"
201+ onClick = { ( ) => handleSort ( 'responseTime' ) }
202+ >
203+ < div className = "flex items-center gap-1" >
204+ Response Time
205+ { getSortIcon ( 'responseTime' ) }
206+ </ div >
207+ </ TableHead >
208+ < TableHead
209+ className = "cursor-pointer select-none hover:bg-muted/50 transition-colors"
210+ onClick = { ( ) => handleSort ( 'serviceName' ) }
211+ >
212+ < div className = "flex items-center gap-1" >
213+ Service
214+ { getSortIcon ( 'serviceName' ) }
215+ </ div >
216+ </ TableHead >
217+ < TableHead
218+ className = "cursor-pointer select-none hover:bg-muted/50 transition-colors"
219+ onClick = { ( ) => handleSort ( 'routerName' ) }
220+ >
221+ < div className = "flex items-center gap-1" >
222+ Router
223+ { getSortIcon ( 'routerName' ) }
224+ </ div >
225+ </ TableHead >
226+ < TableHead
227+ className = "cursor-pointer select-none hover:bg-muted/50 transition-colors"
228+ onClick = { ( ) => handleSort ( 'requestAddr' ) }
229+ >
230+ < div className = "flex items-center gap-1" >
231+ Request Addr
232+ { getSortIcon ( 'requestAddr' ) }
233+ </ div >
234+ </ TableHead >
235+ < TableHead
236+ className = "cursor-pointer select-none hover:bg-muted/50 transition-colors"
237+ onClick = { ( ) => handleSort ( 'requestHost' ) }
238+ >
239+ < div className = "flex items-center gap-1" >
240+ Request Host
241+ { getSortIcon ( 'requestHost' ) }
242+ </ div >
243+ </ TableHead >
244+ < TableHead
245+ className = "cursor-pointer select-none hover:bg-muted/50 transition-colors"
246+ onClick = { ( ) => handleSort ( 'clientIP' ) }
247+ >
248+ < div className = "flex items-center gap-1" >
249+ Client IP
250+ { getSortIcon ( 'clientIP' ) }
251+ </ div >
252+ </ TableHead >
253+ < TableHead
254+ className = "cursor-pointer select-none hover:bg-muted/50 transition-colors"
255+ onClick = { ( ) => handleSort ( 'location' ) }
256+ >
257+ < div className = "flex items-center gap-1" >
258+ Location
259+ { getSortIcon ( 'location' ) }
260+ </ div >
261+ </ TableHead >
94262 < TableHead > Size</ TableHead >
95263 </ TableRow >
96264 </ TableHeader >
97265 < TableBody >
98- { logs . length === 0 ? (
266+ { sortedLogs . length === 0 ? (
99267 < TableRow >
100- < TableCell colSpan = { 10 } className = "h-24 text-center text-muted-foreground" >
268+ < TableCell colSpan = { 12 } className = "h-24 text-center text-muted-foreground" >
101269 No logs found. Waiting for incoming requests...
102270 </ TableCell >
103271 </ TableRow >
104272 ) : (
105- logs . map ( ( log ) => {
273+ sortedLogs . map ( ( log ) => {
106274 const responseTime = formatResponseTime ( log . responseTime ) ;
107275 return (
108276 < TableRow key = { log . id } >
@@ -139,6 +307,22 @@ export function LogTable({ logs, requestLogs }: LogTableProps) {
139307 < span className = "text-xs" > { log . routerName } </ span >
140308 </ div >
141309 </ TableCell >
310+ < TableCell >
311+ < div className = "flex items-center gap-1" >
312+ < Network className = "h-3 w-3 text-muted-foreground" />
313+ < span className = "text-xs font-mono max-w-32 truncate" title = { log . requestAddr } >
314+ { log . requestAddr || '-' }
315+ </ span >
316+ </ div >
317+ </ TableCell >
318+ < TableCell >
319+ < div className = "flex items-center gap-1" >
320+ < ExternalLink className = "h-3 w-3 text-muted-foreground" />
321+ < span className = "text-xs font-mono max-w-32 truncate" title = { log . requestHost } >
322+ { log . requestHost || '-' }
323+ </ span >
324+ </ div >
325+ </ TableCell >
142326 < TableCell className = "font-mono text-xs" >
143327 { log . clientIP }
144328 </ TableCell >
@@ -160,12 +344,12 @@ export function LogTable({ logs, requestLogs }: LogTableProps) {
160344 } )
161345 ) }
162346 < TableRow ref = { loader } >
163- < TableCell colSpan = { 10 } className = "text-center text-muted-foreground" >
347+ < TableCell colSpan = { 12 } className = "text-center text-muted-foreground" >
164348 Loading more logs...
165349 </ TableCell >
166350 </ TableRow >
167351 </ TableBody >
168352 </ Table >
169353 </ div >
170354 ) ;
171- }
355+ }
0 commit comments