2023-08-24 04:20:53 +08:00
|
|
|
import { createEventSignal } from '@solid-primitives/event-listener'
|
2023-09-02 00:15:19 +08:00
|
|
|
import { useI18n } from '@solid-primitives/i18n'
|
2023-08-31 22:49:34 +08:00
|
|
|
import { makePersisted } from '@solid-primitives/storage'
|
2023-08-24 04:20:53 +08:00
|
|
|
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-31 22:49:34 +08:00
|
|
|
IconSettings,
|
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'
|
2023-09-03 03:26:29 +08:00
|
|
|
import { Button, ConnectionsModal } from '~/components'
|
|
|
|
import { AccessorKey, initColumnOrder, initColumnVisibility } from '~/constants'
|
2023-08-28 02:32:59 +08:00
|
|
|
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-31 22:49:34 +08:00
|
|
|
type ColumnVisibility = Partial<Record<AccessorKey, boolean>>
|
2023-09-01 00:12:33 +08:00
|
|
|
type ColumnOrder = AccessorKey[]
|
2023-08-31 22:49:34 +08:00
|
|
|
|
2023-08-29 14:44:49 +08:00
|
|
|
export default () => {
|
2023-09-02 00:15:19 +08:00
|
|
|
const [t] = useI18n()
|
2023-08-31 22:49:34 +08:00
|
|
|
const [columnVisibility, setColumnVisibility] = makePersisted(
|
|
|
|
createSignal<ColumnVisibility>(initColumnVisibility),
|
|
|
|
{
|
|
|
|
name: 'columnVisibility',
|
|
|
|
storage: localStorage,
|
|
|
|
},
|
|
|
|
)
|
2023-09-01 00:12:33 +08:00
|
|
|
const [columnOrder, setColumnOrder] = makePersisted(
|
|
|
|
createSignal<ColumnOrder>(initColumnOrder),
|
|
|
|
{
|
|
|
|
name: 'columnOrder',
|
|
|
|
storage: localStorage,
|
|
|
|
},
|
|
|
|
)
|
2023-08-31 22:49:34 +08:00
|
|
|
|
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
|
|
|
{
|
2023-08-31 22:49:34 +08:00
|
|
|
accessorKey: AccessorKey.Close,
|
2023-08-28 02:32:59 +08:00
|
|
|
header: () => (
|
|
|
|
<div class="flex h-full items-center">
|
2023-09-03 02:31:06 +08:00
|
|
|
<Button
|
|
|
|
class="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-09-03 02:31:06 +08:00
|
|
|
</Button>
|
2023-08-28 02:32:59 +08:00
|
|
|
</div>
|
|
|
|
),
|
|
|
|
cell: ({ row }) => (
|
|
|
|
<div class="flex h-full items-center">
|
2023-09-03 02:31:06 +08:00
|
|
|
<Button
|
|
|
|
class="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-09-03 02:31:06 +08:00
|
|
|
</Button>
|
2023-08-28 02:32:59 +08:00
|
|
|
</div>
|
|
|
|
),
|
|
|
|
},
|
2023-08-24 04:20:53 +08:00
|
|
|
{
|
2023-08-31 22:49:34 +08:00
|
|
|
accessorKey: AccessorKey.ID,
|
2023-08-24 04:20:53 +08:00
|
|
|
accessorFn: (row) => row.id,
|
|
|
|
},
|
|
|
|
{
|
2023-08-31 22:49:34 +08:00
|
|
|
accessorKey: AccessorKey.Type,
|
2023-08-29 21:39:52 +08:00
|
|
|
accessorFn: (row) => `${row.metadata.type}(${row.metadata.network})`,
|
2023-08-24 04:20:53 +08:00
|
|
|
},
|
|
|
|
{
|
2023-08-31 22:49:34 +08:00
|
|
|
accessorKey: AccessorKey.Process,
|
2023-09-01 13:39:26 +08:00
|
|
|
accessorFn: (row) =>
|
|
|
|
row.metadata.process ||
|
|
|
|
row.metadata.processPath.replace(/^.*[/\\](.*)$/, '$1') ||
|
|
|
|
'-',
|
2023-08-24 04:20:53 +08:00
|
|
|
},
|
|
|
|
{
|
2023-08-31 22:49:34 +08:00
|
|
|
accessorKey: AccessorKey.Host,
|
2023-08-29 23:16:17 +08:00
|
|
|
accessorFn: (row) =>
|
2023-09-01 15:49:47 +08:00
|
|
|
`${
|
|
|
|
row.metadata.host ? row.metadata.host : row.metadata.destinationIP
|
|
|
|
}:${row.metadata.destinationPort}`,
|
2023-08-24 04:20:53 +08:00
|
|
|
},
|
|
|
|
{
|
2023-08-31 22:49:34 +08:00
|
|
|
accessorKey: 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
|
|
|
},
|
|
|
|
{
|
2023-08-31 22:49:34 +08:00
|
|
|
accessorKey: 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-31 22:49:34 +08:00
|
|
|
accessorKey: AccessorKey.DlSpeed,
|
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,
|
|
|
|
},
|
|
|
|
{
|
2023-08-31 22:49:34 +08:00
|
|
|
accessorKey: AccessorKey.ULSpeed,
|
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,
|
|
|
|
},
|
|
|
|
{
|
2023-08-31 22:49:34 +08:00
|
|
|
accessorKey: AccessorKey.Download,
|
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-31 22:49:34 +08:00
|
|
|
accessorKey: AccessorKey.Upload,
|
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
|
|
|
},
|
|
|
|
{
|
2023-08-31 22:49:34 +08:00
|
|
|
accessorKey: AccessorKey.Source,
|
2023-08-24 04:20:53 +08:00
|
|
|
accessorFn: (row) =>
|
|
|
|
isIPv6(row.metadata.sourceIP)
|
|
|
|
? `[${row.metadata.sourceIP}]:${row.metadata.sourcePort}`
|
|
|
|
: `${row.metadata.sourceIP}:${row.metadata.sourcePort}`,
|
|
|
|
},
|
|
|
|
{
|
2023-08-31 22:49:34 +08:00
|
|
|
accessorKey: AccessorKey.Destination,
|
2023-08-24 04:20:53 +08:00
|
|
|
accessorFn: (row) =>
|
2023-09-01 15:49:47 +08:00
|
|
|
row.metadata.remoteDestination ||
|
|
|
|
row.metadata.destinationIP ||
|
|
|
|
row.metadata.host,
|
2023-08-24 04:20:53 +08:00
|
|
|
},
|
|
|
|
]
|
|
|
|
|
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: {
|
2023-09-01 00:12:33 +08:00
|
|
|
get columnOrder() {
|
|
|
|
return columnOrder()
|
|
|
|
},
|
2023-08-28 02:32:59 +08:00
|
|
|
get sorting() {
|
|
|
|
return sorting()
|
|
|
|
},
|
2023-08-31 22:49:34 +08:00
|
|
|
get columnVisibility() {
|
|
|
|
return columnVisibility()
|
2023-08-31 11:15:03 +08:00
|
|
|
},
|
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">
|
2023-08-31 22:49:34 +08:00
|
|
|
<div class="flex w-full">
|
|
|
|
<input
|
|
|
|
class="input input-primary mr-4 w-40 flex-1"
|
2023-09-02 00:15:19 +08:00
|
|
|
placeholder={t('search')}
|
2023-08-31 22:49:34 +08:00
|
|
|
onInput={(e) => setSearch(e.target.value)}
|
|
|
|
/>
|
2023-09-02 00:15:19 +08:00
|
|
|
|
2023-09-01 13:39:26 +08:00
|
|
|
<label for="connection-modal" class="btn btn-circle">
|
2023-08-31 22:49:34 +08:00
|
|
|
<IconSettings />
|
|
|
|
</label>
|
2023-09-02 00:15:19 +08:00
|
|
|
|
2023-08-31 22:49:34 +08:00
|
|
|
<ConnectionsModal
|
2023-09-01 00:12:33 +08:00
|
|
|
order={columnOrder()}
|
|
|
|
visible={columnVisibility()}
|
|
|
|
onOrderChange={(data: ColumnOrder) => {
|
|
|
|
setColumnOrder([...data])
|
|
|
|
}}
|
|
|
|
onVisibleChange={(data: ColumnVisibility) =>
|
2023-08-31 22:49:34 +08:00
|
|
|
setColumnVisibility({ ...data })
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</div>
|
2023-08-24 04:20:53 +08:00
|
|
|
|
2023-09-02 02:54:47 +08:00
|
|
|
<div class="overflow-x-auto whitespace-nowrap rounded-md">
|
|
|
|
<table class="table table-xs rounded-none bg-base-200">
|
2023-08-24 04:20:53 +08:00
|
|
|
<thead>
|
|
|
|
<For each={table.getHeaderGroups()}>
|
|
|
|
{(headerGroup) => (
|
|
|
|
<tr>
|
|
|
|
<For each={headerGroup.headers}>
|
|
|
|
{(header) => (
|
2023-09-01 10:29:12 +08:00
|
|
|
<th class="bg-base-300">
|
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()}
|
|
|
|
>
|
2023-09-02 13:50:24 +08:00
|
|
|
{header.column.id === AccessorKey.Close ? (
|
|
|
|
flexRender(
|
|
|
|
header.column.columnDef.header,
|
|
|
|
header.getContext(),
|
|
|
|
)
|
|
|
|
) : (
|
|
|
|
<span>{t(header.column.id)}</span>
|
|
|
|
)}
|
2023-08-28 02:32:59 +08:00
|
|
|
{{
|
|
|
|
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>
|
|
|
|
)
|
|
|
|
}
|