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.
This commit is contained in:
MHSanaei
2026-06-02 14:14:25 +02:00
parent 10c185a592
commit 61105c2b1a
11 changed files with 45 additions and 14 deletions

View File

@@ -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<InboundOption[]> {
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,
});
}

View File

@@ -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]);

View File

@@ -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]);

View File

@@ -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],

View File

@@ -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],
);

View File

@@ -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 (
<Tooltip key={id} title={label}>
<Tag color={color}>{label}</Tag>

View File

@@ -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 (
<Tooltip key={id} title={inboundLabel(id)}>
<Tag color={color} style={{ margin: 2 }}>

View File

@@ -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],
);

View File

@@ -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}

View File

@@ -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}

View File

@@ -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<FormState>(initialForm);
const isEdit = rule != null;
const { data: inboundOptions } = useInboundOptions();
const remarkByTag = useMemo(() => {
const map: Record<string, string> = {};
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 }))}
/>
</Form.Item>