Skip to content

Commit 8d97c5a

Browse files
feat: Responsive connections table for mobile with sort controls (#1684)
* feat(connections)!: responsive table + sort controls - 2/3 column mobile layout - inline cell labels - sort dropdown with toggle - text justification * fix: reorder class names for consistent styling in Header, Connections, and Rules * chore: update package manager version to [email protected] * fix: adjust sort label styling for improved visibility
1 parent 3686927 commit 8d97c5a

File tree

2 files changed

+105
-11
lines changed

2 files changed

+105
-11
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
"vite-plugin-solid": "^2.11.10",
8383
"zod": "^4.1.12"
8484
},
85-
"packageManager": "pnpm@10.20.0",
85+
"packageManager": "pnpm@10.21.0",
8686
"pnpm": {
8787
"overrides": {
8888
"vite": "npm:rolldown-vite@latest"

src/pages/Connections.tsx

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
import { CONNECTIONS_TABLE_ACCESSOR_KEY } from '~/constants'
4242
import { formatIPv6, formatTimeFromNow } from '~/helpers'
4343
import { useI18n } from '~/i18n'
44+
import type { Dict } from '~/i18n/dict'
4445
import {
4546
allConnections,
4647
clientSourceIPTags,
@@ -61,6 +62,8 @@ enum ActiveTab {
6162
closedConnections,
6263
}
6364

65+
type ColMeta = { headerKey: keyof Dict }
66+
6467
const 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

Comments
 (0)