metacubexd/src/pages/Connections.tsx

337 lines
10 KiB
TypeScript
Raw Normal View History

import { writeClipboard } from '@solid-primitives/clipboard'
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,
2023-09-05 00:47:27 +08:00
getFilteredRowModel,
getSortedRowModel,
2023-08-24 04:20:53 +08:00
} from '@tanstack/solid-table'
import byteSize from 'byte-size'
2023-09-04 18:48:39 +08:00
import dayjs from 'dayjs'
2023-08-24 04:20:53 +08:00
import { isIPv6 } from 'is-ip'
import { For, createEffect, createSignal } from 'solid-js'
import { twMerge } from 'tailwind-merge'
2023-09-03 03:36:12 +08:00
import { Button, ConnectionsTableOrderingModal } from '~/components'
import {
CONNECTIONS_TABLE_ACCESSOR_KEY,
CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER,
CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY,
} from '~/constants'
2023-09-04 18:48:39 +08:00
import { formatTimeFromNow } from '~/helpers'
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-09-03 03:36:12 +08:00
type ColumnVisibility = Partial<Record<CONNECTIONS_TABLE_ACCESSOR_KEY, boolean>>
type ColumnOrder = CONNECTIONS_TABLE_ACCESSOR_KEY[]
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(
2023-09-03 03:36:12 +08:00
createSignal<ColumnVisibility>(CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY),
2023-08-31 22:49:34 +08:00
{
name: 'columnVisibility',
storage: localStorage,
},
)
2023-09-01 00:12:33 +08:00
const [columnOrder, setColumnOrder] = makePersisted(
2023-09-03 03:36:12 +08:00
createSignal<ColumnOrder>(CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER),
2023-09-01 00:12:33 +08:00
{
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-09-03 03:36:12 +08:00
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.Close,
enableSorting: false,
header: () => <span>{t('close')}</span>,
cell: ({ row }) => (
2023-09-05 17:02:43 +08:00
<div class="flex h-4 items-center">
<Button
class="btn-circle btn-xs"
onClick={() => onCloseConnection(row.id)}
>
2023-09-05 17:02:43 +08:00
<IconCircleX size="16" />
</Button>
</div>
),
},
2023-08-24 04:20:53 +08:00
{
2023-09-03 03:36:12 +08:00
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.ID,
2023-08-24 04:20:53 +08:00
accessorFn: (row) => row.id,
},
{
2023-09-03 03:36:12 +08:00
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.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-09-03 03:36:12 +08:00
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.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-09-03 03:36:12 +08:00
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.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-09-03 03:36:12 +08:00
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.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-09-03 03:36:12 +08:00
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.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-09-04 18:48:39 +08:00
{
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.ConnectTime,
accessorFn: (row) => formatTimeFromNow(row.start),
sortingFn: (prev, next) =>
dayjs(prev.original.start).valueOf() -
dayjs(next.original.start).valueOf(),
},
2023-08-24 04:20:53 +08:00
{
2023-09-03 03:36:12 +08:00
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.DlSpeed,
accessorFn: (row) => `${byteSize(row.downloadSpeed)}/s`,
sortingFn: (prev, next) =>
prev.original.downloadSpeed - next.original.downloadSpeed,
},
{
2023-09-03 03:36:12 +08:00
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.ULSpeed,
accessorFn: (row) => `${byteSize(row.uploadSpeed)}/s`,
sortingFn: (prev, next) =>
prev.original.uploadSpeed - next.original.uploadSpeed,
},
{
2023-09-03 03:36:12 +08:00
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.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-09-03 03:36:12 +08:00
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.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-09-03 03:36:12 +08:00
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.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-09-03 03:36:12 +08:00
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.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-09-05 00:47:27 +08:00
get globalFilter() {
return search()
},
},
2023-08-24 04:20:53 +08:00
get data() {
2023-09-05 00:47:27 +08:00
return connectionsWithSpeed()
2023-08-24 04:20:53 +08:00
},
enableHiding: true,
2023-08-24 04:20:53 +08:00
columns,
2023-09-05 00:47:27 +08:00
onGlobalFilterChange: setSearch,
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
2023-08-24 04:20:53 +08:00
getCoreRowModel: getCoreRowModel(),
})
return (
2023-09-05 00:20:19 +08:00
<div class="flex h-full flex-col gap-4 overflow-y-auto p-1">
<div class="flex w-full items-center gap-2">
2023-08-31 22:49:34 +08:00
<input
class="input input-primary 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
<Button
class="btn-circle"
onClick={() => request.delete('connections')}
>
<IconCircleX />
</Button>
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-09-03 03:36:12 +08:00
<ConnectionsTableOrderingModal
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-05 00:06:24 +08:00
<div class="overflow-x-auto whitespace-nowrap rounded-md bg-base-300">
2023-09-05 16:50:18 +08:00
<table class="table table-zebra table-xs relative rounded-none">
2023-09-05 17:02:43 +08:00
<thead class="sticky top-0 z-10 h-8">
2023-08-24 04:20:53 +08:00
<For each={table.getHeaderGroups()}>
{(headerGroup) => (
<tr>
<For each={headerGroup.headers}>
{(header) => (
2023-09-05 00:06:24 +08:00
<th class="bg-base-200">
<div
class={twMerge(
'flex items-center justify-between gap-2',
header.column.getCanSort() &&
'cursor-pointer select-none',
)}
onClick={header.column.getToggleSortingHandler()}
>
2023-09-03 03:36:12 +08:00
{header.column.id ===
CONNECTIONS_TABLE_ACCESSOR_KEY.Close ? (
2023-09-02 13:50:24 +08:00
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-09-05 17:02:43 +08:00
<tr class="h-8 hover:!bg-primary hover:text-primary-content">
2023-08-24 04:20:53 +08:00
<For each={row.getVisibleCells()}>
{(cell) => (
<td
onContextMenu={(e) => {
e.preventDefault()
typeof cell.renderValue() === 'string' &&
void writeClipboard(cell.renderValue() as string)
}}
>
2023-08-24 04:20:53 +08:00
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</td>
)}
</For>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
)
}