diff --git a/frontend/src/pages/inbounds/InboundsPage.tsx b/frontend/src/pages/inbounds/InboundsPage.tsx index 3a2b557b8..705933113 100644 --- a/frontend/src/pages/inbounds/InboundsPage.tsx +++ b/frontend/src/pages/inbounds/InboundsPage.tsx @@ -803,6 +803,7 @@ export default function InboundsPage() { subEnable={subSettings.enable} nodesById={nodesById} hasActiveNode={showNodeInfo} + hosts={hosts} onAddInbound={onAddInbound} onGeneralAction={onGeneralAction} onRowAction={({ key, dbInbound }) => diff --git a/frontend/src/pages/inbounds/list/InboundList.css b/frontend/src/pages/inbounds/list/InboundList.css index 16f2db9a9..e1b30f7fb 100644 --- a/frontend/src/pages/inbounds/list/InboundList.css +++ b/frontend/src/pages/inbounds/list/InboundList.css @@ -180,3 +180,34 @@ padding: 4px; } } + +.inbound-remark-cell { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + min-width: 0; +} + +.inbound-remark { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.inbound-host-remarks { + font-size: 11px; + font-weight: 400; + opacity: 0.65; + line-height: 1.2; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: default; +} + +.tag-name .inbound-host-remarks { + font-weight: 400; +} diff --git a/frontend/src/pages/inbounds/list/InboundList.tsx b/frontend/src/pages/inbounds/list/InboundList.tsx index 1b3931585..33748a4d8 100644 --- a/frontend/src/pages/inbounds/list/InboundList.tsx +++ b/frontend/src/pages/inbounds/list/InboundList.tsx @@ -32,10 +32,21 @@ import { activateOnKey } from '@/utils/a11y'; import { buildRowActionsMenu } from './RowActions'; import { useInboundColumns } from './useInboundColumns'; +import { buildHostRemarksByInboundId, formatHostRemarksLabel } from './helpers'; import InboundStatsModal from './InboundStatsModal'; import type { DBInboundRecord, GeneralAction, InboundListProps, RowAction } from './types'; import './InboundList.css'; +function HostRemarksSuffix({ remarks }: { remarks: string[] }) { + if (remarks.length === 0) return null; + const { display, full } = formatHostRemarksLabel(remarks); + return ( + + ({display}) + + ); +} + export default function InboundList({ dbInbounds, clientCount, @@ -48,6 +59,7 @@ export default function InboundList({ subEnable, nodesById, hasActiveNode, + hosts, onAddInbound, onGeneralAction, onRowAction, @@ -86,19 +98,22 @@ export default function InboundList({ [nodesById, t], ); + const hostRemarksByInboundId = useMemo(() => buildHostRemarksByInboundId(hosts), [hosts]); + const visibleInbounds = useMemo(() => { let list = dbInbounds; if (nodeFilter === 0) list = list.filter((ib) => ib.nodeId == null); else if (nodeFilter !== 'all') list = list.filter((ib) => ib.nodeId === nodeFilter); const q = searchKey.trim().toLowerCase(); if (!q) return list; - return list.filter( - (ib) => - (ib.remark || '').toLowerCase().includes(q) || - String(ib.port).includes(q) || - (ib.protocol || '').toLowerCase().includes(q), - ); - }, [dbInbounds, nodeFilter, searchKey]); + return list.filter((ib) => { + if ((ib.remark || '').toLowerCase().includes(q)) return true; + if (String(ib.port).includes(q)) return true; + if ((ib.protocol || '').toLowerCase().includes(q)) return true; + const hostRemarks = hostRemarksByInboundId.get(ib.id) ?? []; + return hostRemarks.some((remark) => remark.toLowerCase().includes(q)); + }); + }, [dbInbounds, nodeFilter, searchKey, hostRemarksByInboundId]); const onSwitchEnable = useCallback(async (dbInbound: DBInboundRecord, next: boolean) => { const previous = dbInbound.enable; @@ -114,8 +129,10 @@ export default function InboundList({ }, []); const hasAnyRemark = useMemo( - () => dbInbounds.some((i) => typeof i.remark === 'string' && i.remark.trim() !== ''), - [dbInbounds], + () => + dbInbounds.some((i) => typeof i.remark === 'string' && i.remark.trim() !== '') || + dbInbounds.some((i) => (hostRemarksByInboundId.get(i.id)?.length ?? 0) > 0), + [dbInbounds, hostRemarksByInboundId], ); const hasAnySubSortIndex = useMemo( @@ -154,6 +171,7 @@ export default function InboundList({ hasAnySubSortIndex, hasActiveNode, nodesById, + hostRemarksByInboundId, clientCount, inboundSpeed, subEnable, @@ -293,7 +311,10 @@ export default function InboundList({ onChange={(e) => toggleSelect(record.id, e.target.checked)} /> #{record.id} - {record.remark} + + {record.remark} + +
[], +): Map { + const map = new Map(); + for (const host of hosts) { + if (host.isDisabled) continue; + const addressFallback = Array.isArray(host.hosts) + ? host.hosts.map((h) => (h || '').trim()).find(Boolean) || '' + : ''; + const label = (host.remark || '').trim() || addressFallback; + if (!label) continue; + for (const inboundId of host.inboundIds || []) { + if (!Number.isFinite(inboundId)) continue; + const list = map.get(inboundId) ?? []; + if (!list.includes(label)) list.push(label); + map.set(inboundId, list); + } + } + return map; +} + +export function formatHostRemarksLabel( + remarks: string[], + visibleLimit = HOST_REMARK_VISIBLE_LIMIT, +): { display: string; full: string } { + const full = remarks.join(', '); + if (remarks.length <= visibleLimit) { + return { display: full, full }; + } + const visible = remarks.slice(0, visibleLimit).join(', '); + const more = remarks.length - visibleLimit; + return { display: `${visible}, +${more}`, full }; +} diff --git a/frontend/src/pages/inbounds/list/types.ts b/frontend/src/pages/inbounds/list/types.ts index 5891dbcc2..49c759175 100644 --- a/frontend/src/pages/inbounds/list/types.ts +++ b/frontend/src/pages/inbounds/list/types.ts @@ -1,4 +1,5 @@ import type { NodeRecord } from '@/api/queries/useNodesQuery'; +import type { HostRecord } from '@/schemas/api/host'; export interface StreamHints { network: string; @@ -78,6 +79,7 @@ export interface InboundListProps { subEnable: boolean; nodesById: Map; hasActiveNode: boolean; + hosts: HostRecord[]; onAddInbound: () => void; onGeneralAction: (key: GeneralAction) => void; onRowAction: (action: { key: RowAction; dbInbound: DBInboundRecord }) => void; diff --git a/frontend/src/pages/inbounds/list/useInboundColumns.tsx b/frontend/src/pages/inbounds/list/useInboundColumns.tsx index 1089d95e8..9f08e868c 100644 --- a/frontend/src/pages/inbounds/list/useInboundColumns.tsx +++ b/frontend/src/pages/inbounds/list/useInboundColumns.tsx @@ -23,6 +23,7 @@ import { shadowsocksNetworkLabel, tunnelNetworkLabel, mixedNetworkLabel, + formatHostRemarksLabel, } from './helpers'; import type { ClientCountEntry, DBInboundRecord, InboundSpeedEntry, RowAction } from './types'; @@ -31,6 +32,7 @@ interface UseInboundColumnsParams { hasAnySubSortIndex: boolean; hasActiveNode: boolean; nodesById: Map; + hostRemarksByInboundId: Map; clientCount: Record; inboundSpeed: Record; subEnable: boolean; @@ -45,6 +47,7 @@ export function useInboundColumns({ hasAnySubSortIndex, hasActiveNode, nodesById, + hostRemarksByInboundId, clientCount, inboundSpeed, subEnable, @@ -138,8 +141,23 @@ export function useInboundColumns({ dataIndex: 'remark', key: 'remark', align: 'center', - width: 90, + width: 140, sorter: (a, b) => compareText(a.remark, b.remark), + render: (_, record) => { + const hostRemarks = hostRemarksByInboundId.get(record.id) ?? []; + if (hostRemarks.length === 0) { + return record.remark || null; + } + const { display, full } = formatHostRemarksLabel(hostRemarks); + return ( +
+
{record.remark}
+ +
({display})
+
+
+ ); + }, }); } @@ -453,6 +471,7 @@ export function useInboundColumns({ hasAnySubSortIndex, hasActiveNode, nodesById, + hostRemarksByInboundId, clientCount, inboundSpeed, subEnable, diff --git a/frontend/src/test/inbound-host-remarks.test.ts b/frontend/src/test/inbound-host-remarks.test.ts new file mode 100644 index 000000000..896bc620d --- /dev/null +++ b/frontend/src/test/inbound-host-remarks.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest'; + +import { buildHostRemarksByInboundId } from '@/pages/inbounds/list/helpers'; + +describe('buildHostRemarksByInboundId', () => { + it('joins only host groups a client can reach (#6026)', () => { + const map = buildHostRemarksByInboundId([ + { remark: 'USA IPv4', inboundIds: [1, 2], hosts: ['1.2.3.4:443'], isDisabled: false }, + { remark: 'USA IPv6', inboundIds: [1], hosts: ['[2001:db8::1]:443'], isDisabled: true }, + { remark: '', inboundIds: [2], hosts: ['', 'cdn.example.com:443'], isDisabled: false }, + { remark: 'USA IPv4', inboundIds: [2], hosts: ['5.6.7.8'] }, + ]); + expect(map.get(1)).toEqual(['USA IPv4']); + expect(map.get(2)).toEqual(['USA IPv4', 'cdn.example.com:443']); + expect(map.has(3)).toBe(false); + }); +});