2023-08-24 04:20:53 +08:00
|
|
|
import { createEventSignal } from '@solid-primitives/event-listener'
|
|
|
|
import { createReconnectingWS } from '@solid-primitives/websocket'
|
2023-08-28 02:32:59 +08:00
|
|
|
import {
|
2023-08-31 13:04:38 +08:00
|
|
|
IconCircleX,
|
2023-08-28 02:32:59 +08:00
|
|
|
IconSortAscending,
|
|
|
|
IconSortDescending,
|
|
|
|
} from '@tabler/icons-solidjs'
|
2023-08-24 04:20:53 +08:00
|
|
|
import {
|
|
|
|
ColumnDef,
|
2023-08-28 02:32:59 +08:00
|
|
|
SortingState,
|
2023-08-24 04:20:53 +08:00
|
|
|
createSolidTable,
|
|
|
|
flexRender,
|
|
|
|
getCoreRowModel,
|
2023-08-28 02:32:59 +08:00
|
|
|
getSortedRowModel,
|
2023-08-24 04:20:53 +08:00
|
|
|
} from '@tanstack/solid-table'
|
|
|
|
import byteSize from 'byte-size'
|
|
|
|
import { isIPv6 } from 'is-ip'
|
2023-08-29 22:51:23 +08:00
|
|
|
import { For, createEffect, createSignal } from 'solid-js'
|
2023-08-28 02:32:59 +08:00
|
|
|
import { twMerge } from 'tailwind-merge'
|
|
|
|
import { secret, useRequest, wsEndpointURL } from '~/signals'
|
2023-08-28 01:20:09 +08:00
|
|
|
import type { Connection } from '~/types'
|
2023-08-24 04:20:53 +08:00
|
|
|
|
2023-08-29 22:51:23 +08:00
|
|
|
type ConnectionWithSpeed = Connection & {
|
|
|
|
downloadSpeed: number
|
|
|
|
uploadSpeed: number
|
|
|
|
}
|
|
|
|
|
2023-08-29 14:44:49 +08:00
|
|
|
export default () => {
|
2023-08-28 02:32:59 +08:00
|
|
|
const request = useRequest()
|
2023-08-24 04:20:53 +08:00
|
|
|
const [search, setSearch] = createSignal('')
|
|
|
|
|
2023-08-27 23:30:13 +08:00
|
|
|
const ws = createReconnectingWS(
|
|
|
|
`${wsEndpointURL()}/connections?token=${secret()}`,
|
|
|
|
)
|
2023-08-24 04:20:53 +08:00
|
|
|
|
|
|
|
const messageEvent = createEventSignal<{
|
|
|
|
message: WebSocketEventMap['message']
|
|
|
|
}>(ws, 'message')
|
|
|
|
|
2023-08-29 22:51:23 +08:00
|
|
|
const [connectionsWithSpeed, setConnectionsWithSpeed] = createSignal<
|
|
|
|
ConnectionWithSpeed[]
|
|
|
|
>([])
|
|
|
|
|
|
|
|
createEffect(() => {
|
2023-08-24 04:20:53 +08:00
|
|
|
const data = messageEvent()?.data
|
|
|
|
|
|
|
|
if (!data) {
|
2023-08-29 22:51:23 +08:00
|
|
|
return
|
2023-08-24 04:20:53 +08:00
|
|
|
}
|
|
|
|
|
2023-08-29 22:51:23 +08:00
|
|
|
setConnectionsWithSpeed((prevConnections) => {
|
|
|
|
const prevMap = new Map<string, Connection>()
|
|
|
|
prevConnections.forEach((prev) => prevMap.set(prev.id, prev))
|
|
|
|
|
|
|
|
const connections = (
|
|
|
|
JSON.parse(data) as { connections: Connection[] }
|
|
|
|
).connections.map((connection) => {
|
|
|
|
const prevConn = prevMap.get(connection.id)
|
|
|
|
|
|
|
|
if (!prevConn) {
|
|
|
|
return { ...connection, downloadSpeed: 0, uploadSpeed: 0 }
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
...connection,
|
|
|
|
downloadSpeed: prevConn.download
|
|
|
|
? connection.download - prevConn.download
|
|
|
|
: 0,
|
|
|
|
uploadSpeed: prevConn.upload
|
|
|
|
? connection.upload - prevConn.upload
|
|
|
|
: 0,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
return connections.slice(-100)
|
|
|
|
})
|
|
|
|
})
|
2023-08-24 04:20:53 +08:00
|
|
|
|
2023-08-28 02:32:59 +08:00
|
|
|
const onCloseConnection = (id: string) => request.delete(`connections/${id}`)
|
|
|
|
|
2023-08-29 22:51:23 +08:00
|
|
|
const columns: ColumnDef<ConnectionWithSpeed>[] = [
|
2023-08-28 02:32:59 +08:00
|
|
|
{
|
|
|
|
id: 'close',
|
|
|
|
header: () => (
|
|
|
|
<div class="flex h-full items-center">
|
|
|
|
<button
|
2023-08-31 15:54:38 +08:00
|
|
|
class="btn btn-circle btn-xs"
|
2023-08-28 02:32:59 +08:00
|
|
|
onClick={() => request.delete('connections')}
|
|
|
|
>
|
2023-08-31 13:04:38 +08:00
|
|
|
<IconCircleX size="18" />
|
2023-08-28 02:32:59 +08:00
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
),
|
|
|
|
cell: ({ row }) => (
|
|
|
|
<div class="flex h-full items-center">
|
|
|
|
<button
|
2023-08-31 15:54:38 +08:00
|
|
|
class="btn btn-circle btn-xs"
|
2023-08-28 02:32:59 +08:00
|
|
|
onClick={() => onCloseConnection(row.id)}
|
|
|
|
>
|
2023-08-31 13:04:38 +08:00
|
|
|
<IconCircleX size="18" />
|
2023-08-28 02:32:59 +08:00
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
),
|
|
|
|
},
|
2023-08-24 04:20:53 +08:00
|
|
|
{
|
|
|
|
accessorKey: 'ID',
|
|
|
|
accessorFn: (row) => row.id,
|
|
|
|
},
|
|
|
|
{
|
2023-08-29 21:39:52 +08:00
|
|
|
accessorKey: 'Type',
|
|
|
|
accessorFn: (row) => `${row.metadata.type}(${row.metadata.network})`,
|
2023-08-24 04:20:53 +08:00
|
|
|
},
|
|
|
|
{
|
2023-08-29 21:39:52 +08:00
|
|
|
accessorKey: 'Process',
|
|
|
|
accessorFn: (row) => row.metadata.process || '-',
|
2023-08-24 04:20:53 +08:00
|
|
|
},
|
|
|
|
{
|
2023-08-29 21:39:52 +08:00
|
|
|
accessorKey: 'Host',
|
2023-08-29 23:16:17 +08:00
|
|
|
accessorFn: (row) =>
|
|
|
|
row.metadata.host ? row.metadata.host : row.metadata.destinationIP,
|
2023-08-24 04:20:53 +08:00
|
|
|
},
|
|
|
|
{
|
|
|
|
accessorKey: 'Rule',
|
2023-08-29 21:39:52 +08:00
|
|
|
accessorFn: (row) =>
|
|
|
|
!row.rulePayload ? row.rule : `${row.rule} :: ${row.rulePayload}`,
|
2023-08-24 04:20:53 +08:00
|
|
|
},
|
|
|
|
{
|
|
|
|
accessorKey: 'Chains',
|
2023-08-30 17:48:28 +08:00
|
|
|
accessorFn: (row) => row.chains.slice().reverse().join(' :: '),
|
2023-08-24 04:20:53 +08:00
|
|
|
},
|
|
|
|
{
|
2023-08-29 22:51:23 +08:00
|
|
|
accessorKey: 'DL Speed',
|
2023-08-29 23:07:52 +08:00
|
|
|
accessorFn: (row) => `${byteSize(row.downloadSpeed)}/s`,
|
2023-08-29 22:51:23 +08:00
|
|
|
sortingFn: (prev, next) =>
|
|
|
|
prev.original.downloadSpeed - next.original.downloadSpeed,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
accessorKey: 'UL Speed',
|
2023-08-29 23:07:52 +08:00
|
|
|
accessorFn: (row) => `${byteSize(row.uploadSpeed)}/s`,
|
2023-08-29 22:51:23 +08:00
|
|
|
sortingFn: (prev, next) =>
|
|
|
|
prev.original.uploadSpeed - next.original.uploadSpeed,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
accessorKey: 'DL',
|
2023-08-29 21:39:52 +08:00
|
|
|
accessorFn: (row) => byteSize(row.download),
|
2023-08-29 22:51:23 +08:00
|
|
|
sortingFn: (prev, next) =>
|
|
|
|
prev.original.download - next.original.download,
|
2023-08-24 04:20:53 +08:00
|
|
|
},
|
|
|
|
{
|
2023-08-29 22:51:23 +08:00
|
|
|
accessorKey: 'UL',
|
2023-08-29 21:39:52 +08:00
|
|
|
accessorFn: (row) => byteSize(row.upload),
|
2023-08-29 22:51:23 +08:00
|
|
|
sortingFn: (prev, next) => prev.original.upload - next.original.upload,
|
2023-08-24 04:20:53 +08:00
|
|
|
},
|
|
|
|
{
|
|
|
|
accessorKey: 'Source',
|
|
|
|
accessorFn: (row) =>
|
|
|
|
isIPv6(row.metadata.sourceIP)
|
|
|
|
? `[${row.metadata.sourceIP}]:${row.metadata.sourcePort}`
|
|
|
|
: `${row.metadata.sourceIP}:${row.metadata.sourcePort}`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
accessorKey: 'Destination',
|
|
|
|
accessorFn: (row) =>
|
|
|
|
isIPv6(row.metadata.destinationIP)
|
|
|
|
? `[${row.metadata.destinationIP}]:${row.metadata.destinationPort}`
|
|
|
|
: `${row.metadata.destinationIP}:${row.metadata.destinationPort}`,
|
|
|
|
},
|
|
|
|
]
|
|
|
|
|
2023-08-29 21:39:52 +08:00
|
|
|
const [sorting, setSorting] = createSignal<SortingState>([
|
|
|
|
{ id: 'ID', desc: true },
|
|
|
|
])
|
|
|
|
|
2023-08-24 04:20:53 +08:00
|
|
|
const table = createSolidTable({
|
2023-08-28 02:32:59 +08:00
|
|
|
state: {
|
|
|
|
get sorting() {
|
|
|
|
return sorting()
|
|
|
|
},
|
2023-08-31 11:15:03 +08:00
|
|
|
columnVisibility: {
|
|
|
|
ID: false,
|
|
|
|
},
|
2023-08-28 02:32:59 +08:00
|
|
|
},
|
2023-08-24 04:20:53 +08:00
|
|
|
get data() {
|
|
|
|
return search()
|
2023-08-29 22:51:23 +08:00
|
|
|
? connectionsWithSpeed().filter((connection) =>
|
2023-08-24 04:20:53 +08:00
|
|
|
Object.values(connection).some((conn) =>
|
|
|
|
JSON.stringify(conn)
|
|
|
|
.toLowerCase()
|
|
|
|
.includes(search().toLowerCase()),
|
|
|
|
),
|
|
|
|
)
|
2023-08-29 22:51:23 +08:00
|
|
|
: connectionsWithSpeed()
|
2023-08-24 04:20:53 +08:00
|
|
|
},
|
2023-08-31 11:15:03 +08:00
|
|
|
enableHiding: true,
|
2023-08-24 04:20:53 +08:00
|
|
|
columns,
|
2023-08-28 02:32:59 +08:00
|
|
|
onSortingChange: setSorting,
|
|
|
|
getSortedRowModel: getSortedRowModel(),
|
2023-08-24 04:20:53 +08:00
|
|
|
getCoreRowModel: getCoreRowModel(),
|
|
|
|
})
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div class="flex flex-col gap-4">
|
|
|
|
<input
|
|
|
|
class="input input-primary"
|
|
|
|
placeholder="Search"
|
|
|
|
onInput={(e) => setSearch(e.target.value)}
|
|
|
|
/>
|
|
|
|
|
|
|
|
<div class="overflow-x-auto whitespace-nowrap">
|
2023-08-29 00:03:32 +08:00
|
|
|
<table class="table table-xs">
|
2023-08-24 04:20:53 +08:00
|
|
|
<thead>
|
|
|
|
<For each={table.getHeaderGroups()}>
|
|
|
|
{(headerGroup) => (
|
|
|
|
<tr>
|
|
|
|
<For each={headerGroup.headers}>
|
|
|
|
{(header) => (
|
2023-08-31 01:30:08 +08:00
|
|
|
<th class="bg-base-200">
|
2023-08-28 02:32:59 +08:00
|
|
|
<div
|
|
|
|
class={twMerge(
|
|
|
|
'flex items-center justify-between',
|
|
|
|
header.column.getCanSort() &&
|
|
|
|
'cursor-pointer select-none',
|
|
|
|
)}
|
|
|
|
onClick={header.column.getToggleSortingHandler()}
|
|
|
|
>
|
|
|
|
{header.isPlaceholder
|
|
|
|
? null
|
|
|
|
: flexRender(
|
|
|
|
header.column.columnDef.header,
|
|
|
|
header.getContext(),
|
|
|
|
)}
|
|
|
|
{{
|
|
|
|
asc: <IconSortAscending />,
|
|
|
|
desc: <IconSortDescending />,
|
|
|
|
}[header.column.getIsSorted() as string] ?? null}
|
|
|
|
</div>
|
2023-08-24 04:20:53 +08:00
|
|
|
</th>
|
|
|
|
)}
|
|
|
|
</For>
|
|
|
|
</tr>
|
|
|
|
)}
|
|
|
|
</For>
|
|
|
|
</thead>
|
|
|
|
|
|
|
|
<tbody>
|
|
|
|
<For each={table.getRowModel().rows}>
|
|
|
|
{(row) => (
|
2023-08-29 00:03:32 +08:00
|
|
|
<tr class="hover">
|
2023-08-24 04:20:53 +08:00
|
|
|
<For each={row.getVisibleCells()}>
|
|
|
|
{(cell) => (
|
|
|
|
<td>
|
|
|
|
{flexRender(
|
|
|
|
cell.column.columnDef.cell,
|
|
|
|
cell.getContext(),
|
|
|
|
)}
|
|
|
|
</td>
|
|
|
|
)}
|
|
|
|
</For>
|
|
|
|
</tr>
|
|
|
|
)}
|
|
|
|
</For>
|
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|