From 61105c2b1a8a7da64e07a9d9d9e4938704809c35 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 2 Jun 2026 14:14:25 +0200 Subject: [PATCH] feat(clients,routing): label inbounds by remark with tag fallback Inbound pickers and chips across the Users area, the inbounds attach-clients modals, and the routing rule inbound-tags selector showed the auto-generated tag (in-443-tcp). Show the inbound remark when set, falling back to the tag. Only display labels change; option values keep using the inbound id (or tag for routing rules, which match inbounds by tag), so filtering, attaching, and saved rules are unaffected. Routing reads remarks via a shared useInboundOptions hook that reuses the existing options query cache. --- frontend/src/api/queries/useInboundOptions.ts | 21 +++++++++++++++++++ .../pages/clients/BulkAttachInboundsModal.tsx | 2 +- .../pages/clients/BulkDetachInboundsModal.tsx | 2 +- .../src/pages/clients/ClientBulkAddModal.tsx | 2 +- .../src/pages/clients/ClientFormModal.tsx | 4 ++-- .../src/pages/clients/ClientInfoModal.tsx | 2 +- frontend/src/pages/clients/ClientsPage.tsx | 4 ++-- frontend/src/pages/clients/FilterDrawer.tsx | 2 +- .../inbounds/clients/AttachClientsModal.tsx | 4 ++-- .../clients/AttachExistingClientsModal.tsx | 2 +- .../src/pages/xray/routing/RuleFormModal.tsx | 14 +++++++++++-- 11 files changed, 45 insertions(+), 14 deletions(-) create mode 100644 frontend/src/api/queries/useInboundOptions.ts diff --git a/frontend/src/api/queries/useInboundOptions.ts b/frontend/src/api/queries/useInboundOptions.ts new file mode 100644 index 00000000..e861acc3 --- /dev/null +++ b/frontend/src/api/queries/useInboundOptions.ts @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; + +import { HttpUtil } from '@/utils'; +import { parseMsg } from '@/utils/zodValidate'; +import { keys } from '@/api/queryKeys'; +import { InboundOptionsSchema, type InboundOption } from '@/schemas/client'; + +async function fetchInboundOptions(): Promise { + const msg = await HttpUtil.get('/panel/api/inbounds/options', undefined, { silent: true }); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch inbound options'); + const validated = parseMsg(msg, InboundOptionsSchema, 'inbounds/options'); + return Array.isArray(validated.obj) ? validated.obj : []; +} + +export function useInboundOptions() { + return useQuery({ + queryKey: keys.inbounds.options(), + queryFn: fetchInboundOptions, + staleTime: Infinity, + }); +} diff --git a/frontend/src/pages/clients/BulkAttachInboundsModal.tsx b/frontend/src/pages/clients/BulkAttachInboundsModal.tsx index 8e31bf0e..6b17e5a4 100644 --- a/frontend/src/pages/clients/BulkAttachInboundsModal.tsx +++ b/frontend/src/pages/clients/BulkAttachInboundsModal.tsx @@ -36,7 +36,7 @@ export default function BulkAttachInboundsModal({ .filter((ib) => MULTI_USER_PROTOCOLS.has((ib.protocol || '').toLowerCase())) .map((ib) => ({ value: ib.id, - label: ib.tag, + label: ib.remark?.trim() || ib.tag || '', })); }, [inbounds]); diff --git a/frontend/src/pages/clients/BulkDetachInboundsModal.tsx b/frontend/src/pages/clients/BulkDetachInboundsModal.tsx index 2f5a5717..e63298f6 100644 --- a/frontend/src/pages/clients/BulkDetachInboundsModal.tsx +++ b/frontend/src/pages/clients/BulkDetachInboundsModal.tsx @@ -36,7 +36,7 @@ export default function BulkDetachInboundsModal({ .filter((ib) => MULTI_USER_PROTOCOLS.has((ib.protocol || '').toLowerCase())) .map((ib) => ({ value: ib.id, - label: ib.tag, + label: ib.remark?.trim() || ib.tag || '', })); }, [inbounds]); diff --git a/frontend/src/pages/clients/ClientBulkAddModal.tsx b/frontend/src/pages/clients/ClientBulkAddModal.tsx index a317119a..aae6aeb4 100644 --- a/frontend/src/pages/clients/ClientBulkAddModal.tsx +++ b/frontend/src/pages/clients/ClientBulkAddModal.tsx @@ -100,7 +100,7 @@ export default function ClientBulkAddModal({ () => (inbounds || []) .filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol || '')) .map((ib) => ({ - label: ib.tag ?? '', + label: ib.remark?.trim() || ib.tag || '', value: ib.id, })), [inbounds], diff --git a/frontend/src/pages/clients/ClientFormModal.tsx b/frontend/src/pages/clients/ClientFormModal.tsx index 69b763e8..9d1cafce 100644 --- a/frontend/src/pages/clients/ClientFormModal.tsx +++ b/frontend/src/pages/clients/ClientFormModal.tsx @@ -261,9 +261,9 @@ export default function ClientFormModal({ () => (inbounds || []) .filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol || '')) .map((ib) => ({ - label: ib.tag ?? '', + label: ib.remark?.trim() || ib.tag || '', value: ib.id, - title: ib.tag ?? '', + title: ib.remark?.trim() || ib.tag || '', })), [inbounds], ); diff --git a/frontend/src/pages/clients/ClientInfoModal.tsx b/frontend/src/pages/clients/ClientInfoModal.tsx index fd4cb7d8..d35337e5 100644 --- a/frontend/src/pages/clients/ClientInfoModal.tsx +++ b/frontend/src/pages/clients/ClientInfoModal.tsx @@ -382,7 +382,7 @@ export default function ClientInfoModal({ const ib = inboundsById[id]; const proto = (ib?.protocol || '').toLowerCase(); const color = INBOUND_PROTOCOL_COLORS[proto] ?? 'default'; - const label = ib?.tag ?? ''; + const label = ib?.remark?.trim() || ib?.tag || ''; return ( {label} diff --git a/frontend/src/pages/clients/ClientsPage.tsx b/frontend/src/pages/clients/ClientsPage.tsx index 0873770b..3e21f34c 100644 --- a/frontend/src/pages/clients/ClientsPage.tsx +++ b/frontend/src/pages/clients/ClientsPage.tsx @@ -304,7 +304,7 @@ export default function ClientsPage() { function inboundLabel(id: number) { const ib = inboundsById[id]; - return ib?.tag ?? ''; + return ib?.remark?.trim() || ib?.tag || ''; } const clientBucket = useCallback((row: ClientRecord | null | undefined): Bucket | null => { @@ -694,7 +694,7 @@ export default function ClientsPage() { const ib = inboundsById[id]; const proto = (ib?.protocol || '').toLowerCase(); const color = INBOUND_PROTOCOL_COLORS[proto] ?? 'default'; - const compactLabel = ib?.tag ?? ''; + const compactLabel = ib?.remark?.trim() || ib?.tag || ''; return ( diff --git a/frontend/src/pages/clients/FilterDrawer.tsx b/frontend/src/pages/clients/FilterDrawer.tsx index b4d235ce..8fb7ce8e 100644 --- a/frontend/src/pages/clients/FilterDrawer.tsx +++ b/frontend/src/pages/clients/FilterDrawer.tsx @@ -50,7 +50,7 @@ export default function FilterDrawer({ const inboundOptions = useMemo( () => inbounds.map((ib) => ({ value: ib.id, - label: ib.tag ?? '', + label: ib.remark?.trim() || ib.tag || '', })), [inbounds], ); diff --git a/frontend/src/pages/inbounds/clients/AttachClientsModal.tsx b/frontend/src/pages/inbounds/clients/AttachClientsModal.tsx index 1c1308a5..49a9e314 100644 --- a/frontend/src/pages/inbounds/clients/AttachClientsModal.tsx +++ b/frontend/src/pages/inbounds/clients/AttachClientsModal.tsx @@ -69,7 +69,7 @@ export default function AttachClientsModal({ if (!source) return []; return (dbInbounds || []) .filter((ib) => ib.id !== source.id && isInboundMultiUser(ib)) - .map((ib) => ({ value: ib.id, label: ib.tag ?? '' })); + .map((ib) => ({ value: ib.id, label: ib.remark?.trim() || ib.tag || '' })); }, [dbInbounds, source]); const filteredRows = useMemo(() => { @@ -150,7 +150,7 @@ export default function AttachClientsModal({ }} okText={t('pages.inbounds.attachClients')} cancelText={t('cancel')} - title={t('pages.inbounds.attachClientsTitle', { remark: source?.tag ?? '' })} + title={t('pages.inbounds.attachClientsTitle', { remark: source?.remark?.trim() || source?.tag || '' })} width={680} > {messageContextHolder} diff --git a/frontend/src/pages/inbounds/clients/AttachExistingClientsModal.tsx b/frontend/src/pages/inbounds/clients/AttachExistingClientsModal.tsx index 6fc2e9e6..edc865a2 100644 --- a/frontend/src/pages/inbounds/clients/AttachExistingClientsModal.tsx +++ b/frontend/src/pages/inbounds/clients/AttachExistingClientsModal.tsx @@ -170,7 +170,7 @@ export default function AttachExistingClientsModal({ okButtonProps={{ disabled: selectedEmails.length === 0, loading: saving }} okText={t('pages.inbounds.attachClients')} cancelText={t('cancel')} - title={t('pages.inbounds.attachExistingTitle', { remark: target?.tag ?? '' })} + title={t('pages.inbounds.attachExistingTitle', { remark: target?.remark?.trim() || target?.tag || '' })} width={680} > {messageContextHolder} diff --git a/frontend/src/pages/xray/routing/RuleFormModal.tsx b/frontend/src/pages/xray/routing/RuleFormModal.tsx index cf545c26..fa6c46bd 100644 --- a/frontend/src/pages/xray/routing/RuleFormModal.tsx +++ b/frontend/src/pages/xray/routing/RuleFormModal.tsx @@ -1,8 +1,9 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Form, Input, Modal, Select, Space, Tooltip } from 'antd'; import { PlusOutlined, MinusOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { InputAddon } from '@/components/ui'; +import { useInboundOptions } from '@/api/queries/useInboundOptions'; import { RuleFormSchema, type RuleFormValues } from '@/schemas/xray'; export interface RoutingRule { @@ -72,6 +73,15 @@ export default function RuleFormModal({ const [form, setForm] = useState(initialForm); const isEdit = rule != null; + const { data: inboundOptions } = useInboundOptions(); + const remarkByTag = useMemo(() => { + const map: Record = {}; + for (const ib of inboundOptions || []) { + if (ib.tag) map[ib.tag] = ib.remark?.trim() || ib.tag; + } + return map; + }, [inboundOptions]); + useEffect(() => { if (!open) return; if (rule) { @@ -269,7 +279,7 @@ export default function RuleFormModal({ mode="multiple" value={form.inboundTag} onChange={(v) => update('inboundTag', v)} - options={inboundTags.map((tag) => ({ value: tag, label: tag }))} + options={inboundTags.map((tag) => ({ value: tag, label: remarkByTag[tag] || tag }))} />