feat(connections): tag client source ip with name, closes #181

This commit is contained in:
kunish 2023-09-16 23:31:40 +08:00
parent b2aa3585c5
commit e538f9d057
No known key found for this signature in database
GPG Key ID: 647A12B4F782C430
10 changed files with 201 additions and 53 deletions

View File

@ -20,6 +20,7 @@
"@solid-primitives/clipboard": "^1.5.7", "@solid-primitives/clipboard": "^1.5.7",
"@solid-primitives/event-listener": "^2.3.0", "@solid-primitives/event-listener": "^2.3.0",
"@solid-primitives/i18n": "^1.4.1", "@solid-primitives/i18n": "^1.4.1",
"@solid-primitives/keyed": "^1.2.0",
"@solid-primitives/media": "^2.2.5", "@solid-primitives/media": "^2.2.5",
"@solid-primitives/resize-observer": "^2.0.22", "@solid-primitives/resize-observer": "^2.0.22",
"@solid-primitives/storage": "^2.1.1", "@solid-primitives/storage": "^2.1.1",

View File

@ -26,6 +26,9 @@ dependencies:
'@solid-primitives/i18n': '@solid-primitives/i18n':
specifier: ^1.4.1 specifier: ^1.4.1
version: 1.4.1(solid-js@1.7.11) version: 1.4.1(solid-js@1.7.11)
'@solid-primitives/keyed':
specifier: ^1.2.0
version: 1.2.0(solid-js@1.7.11)
'@solid-primitives/media': '@solid-primitives/media':
specifier: ^2.2.5 specifier: ^2.2.5
version: 2.2.5(solid-js@1.7.11) version: 2.2.5(solid-js@1.7.11)
@ -2044,6 +2047,14 @@ packages:
solid-js: 1.7.11 solid-js: 1.7.11
dev: false dev: false
/@solid-primitives/keyed@1.2.0(solid-js@1.7.11):
resolution: {integrity: sha512-0DuTeJdxWjCTu73XnDZs24JzfXckBnpvCfQ6Mf/kTPKkMZJh7tjkBnZEk48ckrE9xmwat9stIdfrBmZctsepIw==}
peerDependencies:
solid-js: ^1.6.12
dependencies:
solid-js: 1.7.11
dev: false
/@solid-primitives/media@2.2.5(solid-js@1.7.11): /@solid-primitives/media@2.2.5(solid-js@1.7.11):
resolution: {integrity: sha512-wTESNFteSwOZsNIBPLMIVLuOHIIzt2AIZdaCYYxfsJIr/xjDqSomlmdFlAmxfJD3ondO7fwtWfc0rcmAvjoPCA==} resolution: {integrity: sha512-wTESNFteSwOZsNIBPLMIVLuOHIIzt2AIZdaCYYxfsJIr/xjDqSomlmdFlAmxfJD3ondO7fwtWfc0rcmAvjoPCA==}
peerDependencies: peerDependencies:
@ -2259,10 +2270,6 @@ packages:
resolution: {integrity: sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g==} resolution: {integrity: sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g==}
dev: false dev: false
/@types/node@20.6.0:
resolution: {integrity: sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==}
dev: false
/@types/node@20.6.1: /@types/node@20.6.1:
resolution: {integrity: sha512-4LcJvuXQlv4lTHnxwyHQZ3uR9Zw2j7m1C9DfuwoTFQQP4Pmu04O6IfLYgMmHoOCt0nosItLLZAH+sOrRE0Bo8g==} resolution: {integrity: sha512-4LcJvuXQlv4lTHnxwyHQZ3uR9Zw2j7m1C9DfuwoTFQQP4Pmu04O6IfLYgMmHoOCt0nosItLLZAH+sOrRE0Bo8g==}
dev: false dev: false
@ -2274,7 +2281,7 @@ packages:
/@types/resolve@1.17.1: /@types/resolve@1.17.1:
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
dependencies: dependencies:
'@types/node': 20.6.0 '@types/node': 20.6.1
dev: false dev: false
/@types/semver@7.5.1: /@types/semver@7.5.1:
@ -4140,7 +4147,7 @@ packages:
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
engines: {node: '>= 10.13.0'} engines: {node: '>= 10.13.0'}
dependencies: dependencies:
'@types/node': 20.6.0 '@types/node': 20.6.1
merge-stream: 2.0.0 merge-stream: 2.0.0
supports-color: 7.2.0 supports-color: 7.2.0
dev: false dev: false

View File

@ -1,4 +1,7 @@
import { createForm } from '@felte/solid'
import { validator } from '@felte/validator-zod'
import { useI18n } from '@solid-primitives/i18n' import { useI18n } from '@solid-primitives/i18n'
import { IconX } from '@tabler/icons-solidjs'
import type { import type {
DragEventHandler, DragEventHandler,
Draggable, Draggable,
@ -13,7 +16,9 @@ import {
createSortable, createSortable,
useDragDropContext, useDragDropContext,
} from '@thisbeyond/solid-dnd' } from '@thisbeyond/solid-dnd'
import { Component, For, Show, createSignal } from 'solid-js' import { uniq } from 'lodash'
import { Component, For, Index, Show, createSignal } from 'solid-js'
import { z } from 'zod'
import { Button, ConfigTitle } from '~/components' import { Button, ConfigTitle } from '~/components'
import { import {
CONNECTIONS_TABLE_ACCESSOR_KEY, CONNECTIONS_TABLE_ACCESSOR_KEY,
@ -22,16 +27,87 @@ import {
MODAL, MODAL,
TAILWINDCSS_SIZE, TAILWINDCSS_SIZE,
} from '~/constants' } from '~/constants'
import { connectionsTableSize, setConnectionsTableSize } from '~/signals' import {
allConnections,
clientSourceIPTags,
connectionsTableSize,
setClientSourceIPTags,
setConnectionsTableSize,
} from '~/signals'
import {
ConnectionsTableColumnOrder,
ConnectionsTableColumnVisibility,
} from '~/types'
type ColumnVisibility = Partial<Record<CONNECTIONS_TABLE_ACCESSOR_KEY, boolean>> const TagClientSourceIPWithNameForm: Component = () => {
type ColumnOrder = CONNECTIONS_TABLE_ACCESSOR_KEY[] const schema = z.object({
tagName: z.string().nonempty(),
sourceIP: z.string().nonempty(),
})
const [t] = useI18n()
const { form } = createForm<z.infer<typeof schema>>({
extend: validator({ schema }),
onSubmit: ({ tagName, sourceIP }) =>
setClientSourceIPTags((tags) => {
if (
tags.some(
(tag) => tag.tagName === tagName || tag.sourceIP === sourceIP,
)
) {
return tags
}
return [...tags, { tagName, sourceIP }]
}),
})
return (
<form use:form={form}>
<div class="join flex">
<select name="sourceIP" class="select join-item select-bordered">
<option />
<Index
each={uniq(
allConnections().map(({ metadata: { sourceIP } }) => sourceIP),
)
.sort()
.filter(
(sourceIP) =>
!clientSourceIPTags().some(
({ sourceIP: tagSourceIP }) => tagSourceIP === sourceIP,
),
)}
>
{(sourceIP) => (
<option class="badge" value={sourceIP()}>
{sourceIP()}
</option>
)}
</Index>
</select>
<input
name="tagName"
class="input join-item input-bordered min-w-0 flex-1"
placeholder="name"
/>
<Button type="submit" class="join-item">
{t('tag')}
</Button>
</div>
</form>
)
}
export const ConnectionsSettingsModal = (props: { export const ConnectionsSettingsModal = (props: {
order: ColumnOrder order: ConnectionsTableColumnOrder
visible: ColumnVisibility visible: ConnectionsTableColumnVisibility
onOrderChange: (value: ColumnOrder) => void onOrderChange: (value: ConnectionsTableColumnOrder) => void
onVisibleChange: (value: ColumnVisibility) => void onVisibleChange: (value: ConnectionsTableColumnVisibility) => void
}) => { }) => {
const [t] = useI18n() const [t] = useI18n()
const [activeKey, setActiveKey] = const [activeKey, setActiveKey] =
@ -123,6 +199,39 @@ export const ConnectionsSettingsModal = (props: {
</select> </select>
</div> </div>
<div>
<ConfigTitle withDivider>
{t('tagClientSourceIPWithName')}
</ConfigTitle>
<div class="flex flex-col gap-4">
<TagClientSourceIPWithNameForm />
<div class="flex flex-wrap gap-2">
<For each={clientSourceIPTags()}>
{({ tagName, sourceIP }) => (
<div class="badge badge-primary badge-lg items-center gap-2">
<span>
{tagName} - {sourceIP}
</span>
<Button
class="btn-circle btn-ghost btn-xs"
onClick={() =>
setClientSourceIPTags((tags) =>
tags.filter((tag) => tag.tagName !== tagName),
)
}
>
<IconX size={12} />
</Button>
</div>
)}
</For>
</div>
</div>
</div>
<div> <div>
<ConfigTitle withDivider>{t('sort')}</ConfigTitle> <ConfigTitle withDivider>{t('sort')}</ConfigTitle>

View File

@ -15,7 +15,7 @@ export const ConnectionsTableDetailsModal: Component<{
<pre> <pre>
<code> <code>
{JSON.stringify( {JSON.stringify(
allConnections.find( allConnections().find(
({ id }) => id === props.selectedConnectionID, ({ id }) => id === props.selectedConnectionID,
), ),
null, null,

View File

@ -89,4 +89,6 @@ export default {
hideUnAvailableProxies: 'Hide UnAvailable Proxies', hideUnAvailableProxies: 'Hide UnAvailable Proxies',
reloadConfigFile: 'Reload Config File', reloadConfigFile: 'Reload Config File',
flushFakeIPData: 'Flush Fake-IP Data', flushFakeIPData: 'Flush Fake-IP Data',
tagClientSourceIPWithName: 'Tag Client Source IP With Name',
tag: 'Tag',
} }

View File

@ -89,4 +89,6 @@ export default {
hideUnAvailableProxies: '隐藏不可用节点', hideUnAvailableProxies: '隐藏不可用节点',
reloadConfigFile: '重新加载配置文件', reloadConfigFile: '重新加载配置文件',
flushFakeIPData: '清空 Fake-IP 数据', flushFakeIPData: '清空 Fake-IP 数据',
tagClientSourceIPWithName: '为客户端源 IP 地址添加名称标记',
tag: '标记',
} }

View File

@ -36,23 +36,20 @@ import {
ConnectionsSettingsModal, ConnectionsSettingsModal,
ConnectionsTableDetailsModal, ConnectionsTableDetailsModal,
} from '~/components' } from '~/components'
import { import { CONNECTIONS_TABLE_ACCESSOR_KEY, MODAL } from '~/constants'
CONNECTIONS_TABLE_ACCESSOR_KEY,
CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER,
CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY,
MODAL,
} from '~/constants'
import { formatTimeFromNow } from '~/helpers' import { formatTimeFromNow } from '~/helpers'
import { import {
clientSourceIPTags,
connectionsTableColumnOrder,
connectionsTableColumnVisibility,
connectionsTableSize, connectionsTableSize,
setConnectionsTableColumnOrder,
setConnectionsTableColumnVisibility,
tableSizeClassName, tableSizeClassName,
useConnections, useConnections,
} from '~/signals' } from '~/signals'
import type { Connection } from '~/types' import type { Connection } from '~/types'
type ColumnVisibility = Partial<Record<CONNECTIONS_TABLE_ACCESSOR_KEY, boolean>>
type ColumnOrder = CONNECTIONS_TABLE_ACCESSOR_KEY[]
enum ActiveTab { enum ActiveTab {
activeConnections, activeConnections,
closedConnections, closedConnections,
@ -79,20 +76,6 @@ export default () => {
useConnections() useConnections()
const [globalFilter, setGlobalFilter] = createSignal('') const [globalFilter, setGlobalFilter] = createSignal('')
const [columnVisibility, setColumnVisibility] = makePersisted(
createSignal<ColumnVisibility>(CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY),
{
name: 'columnVisibility',
storage: localStorage,
},
)
const [columnOrder, setColumnOrder] = makePersisted(
createSignal<ColumnOrder>(CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER),
{
name: 'columnOrder',
storage: localStorage,
},
)
const [selectedConnectionID, setSelectedConnectionID] = createSignal<string>() const [selectedConnectionID, setSelectedConnectionID] = createSignal<string>()
@ -227,7 +210,13 @@ export default () => {
{ {
header: () => t('sourceIP'), header: () => t('sourceIP'),
accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.SourceIP, accessorKey: CONNECTIONS_TABLE_ACCESSOR_KEY.SourceIP,
accessorFn: (original) => original.metadata.sourceIP, accessorFn: (original) => {
const tag = clientSourceIPTags().find(
(tag) => tag.sourceIP === original.metadata.sourceIP,
)
return tag ? tag.tagName : original.metadata.sourceIP
},
}, },
{ {
header: () => t('sourcePort'), header: () => t('sourcePort'),
@ -258,7 +247,7 @@ export default () => {
}, },
state: { state: {
get columnOrder() { get columnOrder() {
return columnOrder() return connectionsTableColumnOrder()
}, },
get grouping() { get grouping() {
return grouping() return grouping()
@ -267,7 +256,7 @@ export default () => {
return sorting() return sorting()
}, },
get columnVisibility() { get columnVisibility() {
return columnVisibility() return connectionsTableColumnVisibility()
}, },
get globalFilter() { get globalFilter() {
return globalFilter() return globalFilter()
@ -494,11 +483,11 @@ export default () => {
</div> </div>
<ConnectionsSettingsModal <ConnectionsSettingsModal
order={columnOrder()} order={connectionsTableColumnOrder()}
visible={columnVisibility()} visible={connectionsTableColumnVisibility()}
onOrderChange={(data: ColumnOrder) => setColumnOrder(data)} onOrderChange={(data) => setConnectionsTableColumnOrder(data)}
onVisibleChange={(data: ColumnVisibility) => onVisibleChange={(data) =>
setColumnVisibility({ ...data }) setConnectionsTableColumnVisibility({ ...data })
} }
/> />

View File

@ -2,6 +2,8 @@ import { usePrefersDark } from '@solid-primitives/media'
import { makePersisted } from '@solid-primitives/storage' import { makePersisted } from '@solid-primitives/storage'
import { createEffect, createSignal } from 'solid-js' import { createEffect, createSignal } from 'solid-js'
import { import {
CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER,
CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY,
LATENCY_QUALITY_MAP_HTTP, LATENCY_QUALITY_MAP_HTTP,
LATENCY_QUALITY_MAP_HTTPS, LATENCY_QUALITY_MAP_HTTPS,
LOG_LEVEL, LOG_LEVEL,
@ -10,6 +12,10 @@ import {
TAILWINDCSS_SIZE, TAILWINDCSS_SIZE,
} from '~/constants' } from '~/constants'
import { setCurTheme } from '~/signals' import { setCurTheme } from '~/signals'
import {
ConnectionsTableColumnOrder,
ConnectionsTableColumnVisibility,
} from '~/types'
export const [proxiesPreviewType, setProxiesPreviewType] = makePersisted( export const [proxiesPreviewType, setProxiesPreviewType] = makePersisted(
createSignal(PROXIES_PREVIEW_TYPE.Auto), createSignal(PROXIES_PREVIEW_TYPE.Auto),
@ -54,6 +60,35 @@ export const [connectionsTableSize, setConnectionsTableSize] = makePersisted(
createSignal<TAILWINDCSS_SIZE>(TAILWINDCSS_SIZE.XS), createSignal<TAILWINDCSS_SIZE>(TAILWINDCSS_SIZE.XS),
{ name: 'connectionsTableSize', storage: localStorage }, { name: 'connectionsTableSize', storage: localStorage },
) )
export const [
connectionsTableColumnVisibility,
setConnectionsTableColumnVisibility,
] = makePersisted(
createSignal<ConnectionsTableColumnVisibility>(
CONNECTIONS_TABLE_INITIAL_COLUMN_VISIBILITY,
),
{
name: 'connectionsTableColumnVisibility',
storage: localStorage,
},
)
export const [connectionsTableColumnOrder, setConnectionsTableColumnOrder] =
makePersisted(
createSignal<ConnectionsTableColumnOrder>(
CONNECTIONS_TABLE_INITIAL_COLUMN_ORDER,
),
{
name: 'connectionsTableColumnOrder',
storage: localStorage,
},
)
export const [clientSourceIPTags, setClientSourceIPTags] = makePersisted(
createSignal<{ tagName: string; sourceIP: string }[]>([]),
{
name: 'clientSourceIPTags',
storage: localStorage,
},
)
export const [logsTableSize, setLogsTableSize] = makePersisted( export const [logsTableSize, setLogsTableSize] = makePersisted(
createSignal<TAILWINDCSS_SIZE>(TAILWINDCSS_SIZE.XS), createSignal<TAILWINDCSS_SIZE>(TAILWINDCSS_SIZE.XS),
{ name: 'logsTableSize', storage: localStorage }, { name: 'logsTableSize', storage: localStorage },

View File

@ -12,11 +12,9 @@ export type WsMsg = {
// we make connections global, so we can keep track of connections when user in proxy page // we make connections global, so we can keep track of connections when user in proxy page
// when user selects proxy and close some connections they can back and check connections // when user selects proxy and close some connections they can back and check connections
// they closed // they closed
export let allConnections: Connection[] = [] export const [allConnections, setAllConnections] = createSignal<Connection[]>(
[],
export const setAllConnections = (allConns: Connection[]) => { )
allConnections = allConns
}
export let latestConnectionMsg: Accessor<WsMsg> = () => ({ export let latestConnectionMsg: Accessor<WsMsg> = () => ({
uploadTotal: 0, uploadTotal: 0,
@ -103,7 +101,7 @@ export const restructRawMsgToConnection = (
} }
export const mergeAllConnections = (activeConns: Connection[]) => { export const mergeAllConnections = (activeConns: Connection[]) => {
return unionWith(allConnections, activeConns, (a, b) => a.id === b.id) return unionWith(allConnections(), activeConns, (a, b) => a.id === b.id)
} }
const diffClosedConnections = ( const diffClosedConnections = (

View File

@ -1,4 +1,4 @@
import { LOG_LEVEL } from '~/constants' import { CONNECTIONS_TABLE_ACCESSOR_KEY, LOG_LEVEL } from '~/constants'
declare module 'solid-js' { declare module 'solid-js' {
namespace JSX { namespace JSX {
@ -176,3 +176,8 @@ export type BackendVersion = {
meta: boolean meta: boolean
version: string version: string
} }
export type ConnectionsTableColumnVisibility = Partial<
Record<CONNECTIONS_TABLE_ACCESSOR_KEY, boolean>
>
export type ConnectionsTableColumnOrder = CONNECTIONS_TABLE_ACCESSOR_KEY[]