metacubexd/src/pages/Connections.tsx

319 lines
9.1 KiB
TypeScript
Raw Normal View History

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'
import {
2023-08-31 13:04:38 +08:00
IconCircleX,
2023-08-31 22:49:34 +08:00
IconSettings,
IconSortAscending,
IconSortDescending,
} from '@tabler/icons-solidjs'
2023-08-24 04:20:53 +08:00
import {
ColumnDef,
SortingState,
2023-08-24 04:20:53 +08:00
createSolidTable,
flexRender,
getCoreRowModel,
getSortedRowModel,
2023-08-24 04:20:53 +08:00
} from '@tanstack/solid-table'
import byteSize from 'byte-size'
import { isIPv6 } from 'is-ip'
import { For, createEffect, createSignal } from 'solid-js'
import { twMerge } from 'tailwind-merge'
import { Button, ConnectionsModal } from '~/components'
import { AccessorKey, initColumnOrder, initColumnVisibility } from '~/constants'
import { secret, useRequest, wsEndpointURL } from '~/signals'
import type { Connection } from '~/types'
2023-08-24 04:20:53 +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
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
const request = useRequest()
2023-08-24 04:20:53 +08:00
const [search, setSearch] = createSignal('')
const ws = createReconnectingWS(
`${wsEndpointURL()}/connections?token=${secret()}`,
)
2023-08-24 04:20:53 +08:00
const messageEvent = createEventSignal<{
message: WebSocketEventMap['message']
}>(ws, 'message')
const [connectionsWithSpeed, setConnectionsWithSpeed] = createSignal<
ConnectionWithSpeed[]
>([])
createEffect(() => {
2023-08-24 04:20:53 +08:00
const data = messageEvent()?.data
if (!data) {
return
2023-08-24 04:20:53 +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
const onCloseConnection = (id: string) => request.delete(`connections/${id}`)
const columns: ColumnDef<ConnectionWithSpeed>[] = [
{
2023-08-31 22:49:34 +08:00
accessorKey: AccessorKey.Close,
header: () => (
<div class="flex h-full items-center">
<Button
class="btn-circle btn-xs"
onClick={() => request.delete('connections')}
>
2023-08-31 13:04:38 +08:00
<IconCircleX size="18" />
</Button>
</div>
),
cell: ({ row }) => (
<div class="flex h-full items-center">
<Button
class="btn-circle btn-xs"
onClick={() => onCloseConnection(row.id)}
>
2023-08-31 13:04:38 +08:00
<IconCircleX size="18" />
</Button>
</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,
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,
accessorFn: (row) => `${byteSize(row.downloadSpeed)}/s`,
sortingFn: (prev, next) =>
prev.original.downloadSpeed - next.original.downloadSpeed,
},
{
2023-08-31 22:49:34 +08:00
accessorKey: AccessorKey.ULSpeed,
accessorFn: (row) => `${byteSize(row.uploadSpeed)}/s`,
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),
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),
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({
state: {
2023-09-01 00:12:33 +08:00
get columnOrder() {
return columnOrder()
},
get sorting() {
return sorting()
},
2023-08-31 22:49:34 +08:00
get columnVisibility() {
return columnVisibility()
},
},
2023-08-24 04:20:53 +08:00
get data() {
return search()
? connectionsWithSpeed().filter((connection) =>
2023-08-24 04:20:53 +08:00
Object.values(connection).some((conn) =>
JSON.stringify(conn)
.toLowerCase()
.includes(search().toLowerCase()),
),
)
: connectionsWithSpeed()
2023-08-24 04:20:53 +08:00
},
enableHiding: true,
2023-08-24 04:20:53 +08:00
columns,
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">
<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>
)}
{{
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>
)
}