@@ -41,6 +41,7 @@ import {
4141import { CONNECTIONS_TABLE_ACCESSOR_KEY } from '~/constants'
4242import { formatIPv6 , formatTimeFromNow } from '~/helpers'
4343import { useI18n } from '~/i18n'
44+ import type { Dict } from '~/i18n/dict'
4445import {
4546 allConnections ,
4647 clientSourceIPTags ,
@@ -61,6 +62,8 @@ enum ActiveTab {
6162 closedConnections ,
6263}
6364
65+ type ColMeta = { headerKey : keyof Dict }
66+
6467const fuzzyFilter : FilterFn < Connection > = ( row , columnId , value , addMeta ) => {
6568 // Rank the item
6669 const itemRank = rankItem ( row . getValue ( columnId ) , value )
@@ -105,6 +108,7 @@ export default () => {
105108
106109 const columns : ColumnDef < Connection > [ ] = [
107110 {
111+ meta : { headerKey : 'details' } ,
108112 header : ( ) => t ( 'details' ) ,
109113 enableGrouping : false ,
110114 enableSorting : false ,
@@ -126,6 +130,7 @@ export default () => {
126130 ) ,
127131 } ,
128132 {
133+ meta : { headerKey : 'close' } ,
129134 header : ( ) => t ( 'close' ) ,
130135 enableGrouping : false ,
131136 enableSorting : false ,
@@ -143,18 +148,21 @@ export default () => {
143148 ) ,
144149 } ,
145150 {
151+ meta : { headerKey : 'ID' } ,
146152 header : ( ) => t ( 'ID' ) ,
147153 enableGrouping : false ,
148154 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . ID ,
149155 accessorFn : ( original ) => original . id ,
150156 } ,
151157 {
158+ meta : { headerKey : 'type' } ,
152159 header : ( ) => t ( 'type' ) ,
153160 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . Type ,
154161 accessorFn : ( original ) =>
155162 `${ original . metadata . type } (${ original . metadata . network } )` ,
156163 } ,
157164 {
165+ meta : { headerKey : 'process' } ,
158166 header : ( ) => t ( 'process' ) ,
159167 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . Process ,
160168 accessorFn : ( original ) =>
@@ -163,6 +171,7 @@ export default () => {
163171 '-' ,
164172 } ,
165173 {
174+ meta : { headerKey : 'host' } ,
166175 header : ( ) => t ( 'host' ) ,
167176 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . Host ,
168177 accessorFn : ( original ) =>
@@ -173,11 +182,13 @@ export default () => {
173182 } :${ original . metadata . destinationPort } `,
174183 } ,
175184 {
185+ meta : { headerKey : 'sniffHost' } ,
176186 header : ( ) => t ( 'sniffHost' ) ,
177187 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . SniffHost ,
178188 accessorFn : ( original ) => original . metadata . sniffHost || '-' ,
179189 } ,
180190 {
191+ meta : { headerKey : 'rule' } ,
181192 header : ( ) => t ( 'rule' ) ,
182193 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . Rule ,
183194 accessorFn : ( original ) =>
@@ -186,6 +197,7 @@ export default () => {
186197 : `${ original . rule } : ${ original . rulePayload } ` ,
187198 } ,
188199 {
200+ meta : { headerKey : 'chains' } ,
189201 header : ( ) => t ( 'chains' ) ,
190202 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . Chains ,
191203 cell : ( { row } ) => (
@@ -202,6 +214,7 @@ export default () => {
202214 ) ,
203215 } ,
204216 {
217+ meta : { headerKey : 'connectTime' } ,
205218 header : ( ) => t ( 'connectTime' ) ,
206219 enableGrouping : false ,
207220 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . ConnectTime ,
@@ -211,6 +224,7 @@ export default () => {
211224 dayjs ( next . original . start ) . valueOf ( ) ,
212225 } ,
213226 {
227+ meta : { headerKey : 'dlSpeed' } ,
214228 header : ( ) => t ( 'dlSpeed' ) ,
215229 enableGrouping : false ,
216230 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . DlSpeed ,
@@ -219,6 +233,7 @@ export default () => {
219233 prev . original . downloadSpeed - next . original . downloadSpeed ,
220234 } ,
221235 {
236+ meta : { headerKey : 'ulSpeed' } ,
222237 header : ( ) => t ( 'ulSpeed' ) ,
223238 enableGrouping : false ,
224239 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . UlSpeed ,
@@ -227,6 +242,7 @@ export default () => {
227242 prev . original . uploadSpeed - next . original . uploadSpeed ,
228243 } ,
229244 {
245+ meta : { headerKey : 'dl' } ,
230246 header : ( ) => t ( 'dl' ) ,
231247 enableGrouping : false ,
232248 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . Download ,
@@ -235,13 +251,15 @@ export default () => {
235251 prev . original . download - next . original . download ,
236252 } ,
237253 {
254+ meta : { headerKey : 'ul' } ,
238255 header : ( ) => t ( 'ul' ) ,
239256 enableGrouping : false ,
240257 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . Upload ,
241258 accessorFn : ( original ) => byteSize ( original . upload ) ,
242259 sortingFn : ( prev , next ) => prev . original . upload - next . original . upload ,
243260 } ,
244261 {
262+ meta : { headerKey : 'sourceIP' } ,
245263 header : ( ) => t ( 'sourceIP' ) ,
246264 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . SourceIP ,
247265 accessorFn : ( original ) => {
@@ -252,11 +270,13 @@ export default () => {
252270 } ,
253271 } ,
254272 {
273+ meta : { headerKey : 'sourcePort' } ,
255274 header : ( ) => t ( 'sourcePort' ) ,
256275 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . SourcePort ,
257276 accessorFn : ( original ) => original . metadata . sourcePort ,
258277 } ,
259278 {
279+ meta : { headerKey : 'destination' } ,
260280 header : ( ) => t ( 'destination' ) ,
261281 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . Destination ,
262282 accessorFn : ( original ) =>
@@ -265,6 +285,7 @@ export default () => {
265285 original . metadata . host ,
266286 } ,
267287 {
288+ meta : { headerKey : 'inboundUser' } ,
268289 header : ( ) => t ( 'inboundUser' ) ,
269290 accessorKey : CONNECTIONS_TABLE_ACCESSOR_KEY . InboundUser ,
270291 accessorFn : ( original ) =>
@@ -337,6 +358,39 @@ export default () => {
337358 getCoreRowModel : getCoreRowModel ( ) ,
338359 } )
339360
361+ // Sort controls state synced with table sorting
362+ const [ sortColumn , setSortColumn ] = createSignal < string > (
363+ sorting ( ) [ 0 ] ?. id || CONNECTIONS_TABLE_ACCESSOR_KEY . ConnectTime ,
364+ )
365+ const [ sortDesc , setSortDesc ] = createSignal < boolean > (
366+ sorting ( ) [ 0 ] ?. desc ?? true ,
367+ )
368+
369+ createEffect (
370+ on ( sorting , ( ) => {
371+ const s = sorting ( )
372+
373+ if ( s . length ) {
374+ setSortColumn ( s [ 0 ] . id )
375+ setSortDesc ( ! ! s [ 0 ] . desc )
376+ }
377+ } ) ,
378+ )
379+
380+ const sortables = createMemo (
381+ ( ) =>
382+ table
383+ . getAllLeafColumns ( )
384+ . filter ( ( c ) => c . getCanSort ( ) )
385+ . map ( ( c ) => ( {
386+ id : c . id ,
387+ key : ( c . columnDef . meta as ColMeta | undefined ) ?. headerKey as
388+ | keyof Dict
389+ | undefined ,
390+ } ) )
391+ . filter ( ( x ) => ! ! x . key ) as { id : string ; key : keyof Dict } [ ] ,
392+ )
393+
340394 const sourceIPHeader = table
341395 . getFlatHeaders ( )
342396 . find ( ( { id } ) => id === CONNECTIONS_TABLE_ACCESSOR_KEY . SourceIP )
@@ -382,7 +436,7 @@ export default () => {
382436 { ( tab ) => (
383437 < button
384438 class = { twMerge (
385- activeTab ( ) === tab ( ) . type && 'bg-primary ! text-neutral' ,
439+ activeTab ( ) === tab ( ) . type && 'bg-primary text-neutral! ' ,
386440 'tab gap-2 px-2' ,
387441 ) }
388442 onClick = { ( ) => setActiveTab ( tab ( ) . type ) }
@@ -429,6 +483,33 @@ export default () => {
429483 </ select >
430484 </ div >
431485
486+ { /* Sort controls */ }
487+ < div class = "flex items-center gap-2" >
488+ < span class = "w-32 text-sm sm:inline-block" > { t ( 'sortBy' ) } </ span >
489+ < select
490+ class = "select select-sm select-primary"
491+ value = { sortColumn ( ) }
492+ onChange = { ( e ) => {
493+ const id = e . target . value
494+ setSortColumn ( id )
495+ setSorting ( [ { id, desc : sortDesc ( ) } ] )
496+ } }
497+ >
498+ < Index each = { sortables ( ) } >
499+ { ( opt ) => < option value = { opt ( ) . id } > { t ( opt ( ) . key ) } </ option > }
500+ </ Index >
501+ </ select >
502+ < Button
503+ class = "btn btn-sm btn-primary"
504+ onClick = { ( ) => {
505+ const next = ! sortDesc ( )
506+ setSortDesc ( next )
507+ setSorting ( [ { id : sortColumn ( ) , desc : next } ] )
508+ } }
509+ icon = { sortDesc ( ) ? < IconSortDescending /> : < IconSortAscending /> }
510+ />
511+ </ div >
512+
432513 < div class = "join flex flex-1 items-center" >
433514 < input
434515 type = "search"
@@ -489,14 +570,18 @@ export default () => {
489570 'table-pin-rows table table-zebra' ,
490571 ) }
491572 >
492- < thead >
573+ < thead class = "hidden md:table-header-group" >
493574 < For each = { table . getHeaderGroups ( ) } >
494575 { ( headerGroup ) => (
495576 < tr class = "flex" >
496577 < For each = { headerGroup . headers } >
497578 { ( header ) => (
498- < th class = "bg-base-200" style = { { width : '150px' } } >
499- < div class = { twMerge ( 'flex items-center gap-2' ) } >
579+ < th class = "w-36 min-w-36 bg-base-200 sm:w-40 sm:min-w-40 md:w-44 md:min-w-44 lg:w-48 lg:min-w-48" >
580+ < div
581+ class = { twMerge (
582+ 'flex items-center gap-2 text-justify' ,
583+ ) }
584+ >
500585 { header . column . getCanGroup ( ) ? (
501586 < button
502587 class = "cursor-pointer"
@@ -514,7 +599,7 @@ export default () => {
514599 class = { twMerge (
515600 header . column . getCanSort ( ) &&
516601 'cursor-pointer select-none' ,
517- 'flex-1' ,
602+ 'justify flex-1 text-xs wrap-break-word whitespace-normal ' ,
518603 ) }
519604 onClick = { header . column . getToggleSortingHandler ( ) }
520605 >
@@ -541,17 +626,16 @@ export default () => {
541626 scrollRef = { scrollRef }
542627 data = { table . getRowModel ( ) . rows }
543628 as = "tbody"
544- item = "tr"
629+ item = { ( props ) => (
630+ < tr { ...props } class = "flex flex-wrap md:table-row" />
631+ ) }
545632 >
546633 { ( row ) => (
547634 < For each = { row . getVisibleCells ( ) } >
548635 { ( cell ) => {
549636 return (
550637 < td
551- class = "inline-block py-2 break-words"
552- style = { {
553- width : '150px' ,
554- } }
638+ class = "w-1/2 min-w-[50%] py-2 text-justify align-top wrap-break-word nth-[2n]:text-right sm:w-1/3 sm:min-w-[33.333%] sm:nth-[2n]:text-justify sm:nth-[3n]:text-right md:inline-block md:w-44 md:min-w-44 md:text-start lg:w-48 lg:min-w-48"
555639 onContextMenu = { ( e ) => {
556640 e . preventDefault ( )
557641
@@ -560,6 +644,16 @@ export default () => {
560644 if ( value ) writeClipboard ( value ) . catch ( ( ) => { } )
561645 } }
562646 >
647+ { /* Mobile label */ }
648+ < div class = "justify mb-1 text-[10px] text-base-content/60 uppercase md:hidden" >
649+ { ( ( ) => {
650+ const key = (
651+ cell . column . columnDef . meta as ColMeta | undefined
652+ ) ?. headerKey
653+
654+ return key ? t ( key ) : ''
655+ } ) ( ) }
656+ </ div >
563657 { cell . getIsGrouped ( ) ? (
564658 < button
565659 class = { twMerge (
0 commit comments