feat(routing): show tag (remark) in routing rules list (#5151)

* feat(routing): show tag (remark) in routing rules list

Rules table and mobile cards showed raw inboundTag while the form already
used remarks. Display "tag (remark)" when a remark exists; saved rules
still store tags only.

Signed-off-by: aleskxyz <39186039+aleskxyz@users.noreply.github.com>

* feat(inbounds): show "tag (remark)" consistently wherever an inbound is listed

Add a shared formatInboundLabel/formatInboundTag helper and apply the "tag (remark)" format across the routing rules table, mobile cards, the rule form and route tester, plus the client attach/detach/filter modals and the attached-inbounds column. Falls back to the bare tag when no distinct remark exists.

Also fix the routing rules list mis-rendering inbounds whose remark contains a comma: formatted entries are now carried as an array end to end instead of being joined and re-split on commas.

---------

Signed-off-by: aleskxyz <39186039+aleskxyz@users.noreply.github.com>
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
aleskxyz
2026-06-11 12:46:24 +02:00
committed by GitHub
parent 941eba546d
commit 8f408d2d6a
16 changed files with 128 additions and 37 deletions
+12
View File
@@ -0,0 +1,12 @@
/**
* Display label for an inbound: `tag (remark)` when a distinct remark exists,
* otherwise just the tag. Falls back to the remark when no tag is set, and to an
* empty string when neither is present.
*/
export function formatInboundLabel(tag?: string, remark?: string): string {
const tagText = (tag || '').trim();
const remarkText = (remark || '').trim();
if (!tagText) return remarkText;
if (!remarkText || remarkText === tagText) return tagText;
return `${tagText} (${remarkText})`;
}
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Alert, Modal, Select, Typography, message } from 'antd'; import { Alert, Modal, Select, Typography, message } from 'antd';
import type { InboundOption } from '@/hooks/useClients'; import type { InboundOption } from '@/hooks/useClients';
import { formatInboundLabel } from '@/lib/inbounds/label';
import type { BulkAttachResult } from '@/schemas/client'; import type { BulkAttachResult } from '@/schemas/client';
const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks']); const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks']);
@@ -36,7 +37,7 @@ export default function BulkAttachInboundsModal({
.filter((ib) => MULTI_USER_PROTOCOLS.has((ib.protocol || '').toLowerCase())) .filter((ib) => MULTI_USER_PROTOCOLS.has((ib.protocol || '').toLowerCase()))
.map((ib) => ({ .map((ib) => ({
value: ib.id, value: ib.id,
label: ib.remark?.trim() || ib.tag || '', label: formatInboundLabel(ib.tag, ib.remark),
})); }));
}, [inbounds]); }, [inbounds]);
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Alert, Modal, Select, Typography, message } from 'antd'; import { Alert, Modal, Select, Typography, message } from 'antd';
import type { InboundOption } from '@/hooks/useClients'; import type { InboundOption } from '@/hooks/useClients';
import { formatInboundLabel } from '@/lib/inbounds/label';
import type { BulkDetachResult } from '@/schemas/client'; import type { BulkDetachResult } from '@/schemas/client';
const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks']); const MULTI_USER_PROTOCOLS = new Set(['vmess', 'vless', 'trojan', 'hysteria', 'shadowsocks']);
@@ -36,7 +37,7 @@ export default function BulkDetachInboundsModal({
.filter((ib) => MULTI_USER_PROTOCOLS.has((ib.protocol || '').toLowerCase())) .filter((ib) => MULTI_USER_PROTOCOLS.has((ib.protocol || '').toLowerCase()))
.map((ib) => ({ .map((ib) => ({
value: ib.id, value: ib.id,
label: ib.remark?.trim() || ib.tag || '', label: formatInboundLabel(ib.tag, ib.remark),
})); }));
}, [inbounds]); }, [inbounds]);
@@ -6,6 +6,7 @@ import dayjs from 'dayjs';
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import { RandomUtil, SizeFormatter } from '@/utils'; import { RandomUtil, SizeFormatter } from '@/utils';
import { formatInboundLabel } from '@/lib/inbounds/label';
import { TLS_FLOW_CONTROL } from '@/schemas/primitives'; import { TLS_FLOW_CONTROL } from '@/schemas/primitives';
import { DateTimePicker } from '@/components/form'; import { DateTimePicker } from '@/components/form';
import { useClients, type InboundOption } from '@/hooks/useClients'; import { useClients, type InboundOption } from '@/hooks/useClients';
@@ -109,7 +110,7 @@ export default function ClientBulkAddModal({
() => (inbounds || []) () => (inbounds || [])
.filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol || '')) .filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol || ''))
.map((ib) => ({ .map((ib) => ({
label: ib.remark?.trim() || ib.tag || '', label: formatInboundLabel(ib.tag, ib.remark),
value: ib.id, value: ib.id,
})), })),
[inbounds], [inbounds],
@@ -20,6 +20,7 @@ import dayjs from 'dayjs';
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import { HttpUtil, RandomUtil } from '@/utils'; import { HttpUtil, RandomUtil } from '@/utils';
import { formatInboundLabel } from '@/lib/inbounds/label';
import { DateTimePicker } from '@/components/form'; import { DateTimePicker } from '@/components/form';
import { TLS_FLOW_CONTROL } from '@/schemas/primitives'; import { TLS_FLOW_CONTROL } from '@/schemas/primitives';
import type { ClientRecord, InboundOption } from '@/hooks/useClients'; import type { ClientRecord, InboundOption } from '@/hooks/useClients';
@@ -288,9 +289,9 @@ export default function ClientFormModal({
() => (inbounds || []) () => (inbounds || [])
.filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol || '')) .filter((ib) => MULTI_CLIENT_PROTOCOLS.has(ib.protocol || ''))
.map((ib) => ({ .map((ib) => ({
label: ib.remark?.trim() || ib.tag || '', label: formatInboundLabel(ib.tag, ib.remark),
value: ib.id, value: ib.id,
title: ib.remark?.trim() || ib.tag || '', title: formatInboundLabel(ib.tag, ib.remark),
})), })),
[inbounds], [inbounds],
); );
@@ -4,6 +4,7 @@ import { Button, Divider, Modal, Popover, Tag, Tooltip, message } from 'antd';
import { CopyOutlined, EyeOutlined, QrcodeOutlined, ReloadOutlined } from '@ant-design/icons'; import { CopyOutlined, EyeOutlined, QrcodeOutlined, ReloadOutlined } from '@ant-design/icons';
import { ClipboardManager, HttpUtil, IntlUtil, SizeFormatter } from '@/utils'; import { ClipboardManager, HttpUtil, IntlUtil, SizeFormatter } from '@/utils';
import { formatInboundLabel } from '@/lib/inbounds/label';
import { useDatepicker } from '@/hooks/useDatepicker'; import { useDatepicker } from '@/hooks/useDatepicker';
import type { ClientRecord, InboundOption } from '@/hooks/useClients'; import type { ClientRecord, InboundOption } from '@/hooks/useClients';
import { isPostQuantumLink } from '@/lib/xray/inbound-link'; import { isPostQuantumLink } from '@/lib/xray/inbound-link';
@@ -316,7 +317,7 @@ export default function ClientInfoModal({
const ib = inboundsById[id]; const ib = inboundsById[id];
const proto = (ib?.protocol || '').toLowerCase(); const proto = (ib?.protocol || '').toLowerCase();
const color = INBOUND_PROTOCOL_COLORS[proto] ?? 'default'; const color = INBOUND_PROTOCOL_COLORS[proto] ?? 'default';
const label = ib?.remark?.trim() || ib?.tag || ''; const label = formatInboundLabel(ib?.tag, ib?.remark);
return ( return (
<Tooltip key={id} title={label}> <Tooltip key={id} title={label}>
<Tag color={color}>{label}</Tag> <Tag color={color}>{label}</Tag>
+3 -2
View File
@@ -47,6 +47,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useTheme } from '@/hooks/useTheme'; import { useTheme } from '@/hooks/useTheme';
import { formatInboundLabel } from '@/lib/inbounds/label';
import { useMediaQuery } from '@/hooks/useMediaQuery'; import { useMediaQuery } from '@/hooks/useMediaQuery';
import { useWebSocket } from '@/hooks/useWebSocket'; import { useWebSocket } from '@/hooks/useWebSocket';
import { useClients } from '@/hooks/useClients'; import { useClients } from '@/hooks/useClients';
@@ -303,7 +304,7 @@ export default function ClientsPage() {
function inboundLabel(id: number) { function inboundLabel(id: number) {
const ib = inboundsById[id]; const ib = inboundsById[id];
return ib?.remark?.trim() || ib?.tag || ''; return formatInboundLabel(ib?.tag, ib?.remark);
} }
const clientBucket = useCallback((row: ClientRecord | null | undefined): Bucket | null => { const clientBucket = useCallback((row: ClientRecord | null | undefined): Bucket | null => {
@@ -684,7 +685,7 @@ export default function ClientsPage() {
const ib = inboundsById[id]; const ib = inboundsById[id];
const proto = (ib?.protocol || '').toLowerCase(); const proto = (ib?.protocol || '').toLowerCase();
const color = INBOUND_PROTOCOL_COLORS[proto] ?? 'default'; const color = INBOUND_PROTOCOL_COLORS[proto] ?? 'default';
const compactLabel = ib?.remark?.trim() || ib?.tag || ''; const compactLabel = formatInboundLabel(ib?.tag, ib?.remark);
return ( return (
<Tooltip key={id} title={inboundLabel(id)}> <Tooltip key={id} title={inboundLabel(id)}>
<Tag color={color} style={{ margin: 2 }}> <Tag color={color} style={{ margin: 2 }}>
+2 -1
View File
@@ -18,6 +18,7 @@ import dayjs from 'dayjs';
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import type { InboundOption } from '@/hooks/useClients'; import type { InboundOption } from '@/hooks/useClients';
import { formatInboundLabel } from '@/lib/inbounds/label';
import { emptyFilters, type ClientFilters } from './filters'; import { emptyFilters, type ClientFilters } from './filters';
interface FilterDrawerProps { interface FilterDrawerProps {
@@ -50,7 +51,7 @@ export default function FilterDrawer({
const inboundOptions = useMemo( const inboundOptions = useMemo(
() => inbounds.map((ib) => ({ () => inbounds.map((ib) => ({
value: ib.id, value: ib.id,
label: ib.remark?.trim() || ib.tag || '', label: formatInboundLabel(ib.tag, ib.remark),
})), })),
[inbounds], [inbounds],
); );
@@ -4,6 +4,7 @@ import { Alert, Input, Modal, Select, Space, Table, Tag, Typography, message } f
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { HttpUtil } from '@/utils'; import { HttpUtil } from '@/utils';
import { formatInboundLabel } from '@/lib/inbounds/label';
import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound'; import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
import { isInboundMultiUser } from '../list'; import { isInboundMultiUser } from '../list';
@@ -69,7 +70,7 @@ export default function AttachClientsModal({
if (!source) return []; if (!source) return [];
return (dbInbounds || []) return (dbInbounds || [])
.filter((ib) => ib.id !== source.id && isInboundMultiUser(ib)) .filter((ib) => ib.id !== source.id && isInboundMultiUser(ib))
.map((ib) => ({ value: ib.id, label: ib.remark?.trim() || ib.tag || '' })); .map((ib) => ({ value: ib.id, label: formatInboundLabel(ib.tag, ib.remark) }));
}, [dbInbounds, source]); }, [dbInbounds, source]);
const filteredRows = useMemo(() => { const filteredRows = useMemo(() => {
@@ -150,7 +151,7 @@ export default function AttachClientsModal({
}} }}
okText={t('pages.inbounds.attachClients')} okText={t('pages.inbounds.attachClients')}
cancelText={t('cancel')} cancelText={t('cancel')}
title={t('pages.inbounds.attachClientsTitle', { remark: source?.remark?.trim() || source?.tag || '' })} title={t('pages.inbounds.attachClientsTitle', { remark: formatInboundLabel(source?.tag, source?.remark) })}
width={680} width={680}
> >
{messageContextHolder} {messageContextHolder}
@@ -4,6 +4,7 @@ import { Alert, Input, Modal, Select, Space, Spin, Table, Tag, Typography, messa
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { HttpUtil } from '@/utils'; import { HttpUtil } from '@/utils';
import { formatInboundLabel } from '@/lib/inbounds/label';
import type { DBInbound } from '@/models/dbinbound'; import type { DBInbound } from '@/models/dbinbound';
interface AttachExistingClientsModalProps { interface AttachExistingClientsModalProps {
@@ -170,7 +171,7 @@ export default function AttachExistingClientsModal({
okButtonProps={{ disabled: selectedEmails.length === 0, loading: saving }} okButtonProps={{ disabled: selectedEmails.length === 0, loading: saving }}
okText={t('pages.inbounds.attachClients')} okText={t('pages.inbounds.attachClients')}
cancelText={t('cancel')} cancelText={t('cancel')}
title={t('pages.inbounds.attachExistingTitle', { remark: target?.remark?.trim() || target?.tag || '' })} title={t('pages.inbounds.attachExistingTitle', { remark: formatInboundLabel(target?.tag, target?.remark) })}
width={680} width={680}
> >
{messageContextHolder} {messageContextHolder}
@@ -2,8 +2,8 @@ import { Tooltip } from 'antd';
import { csv } from './helpers'; import { csv } from './helpers';
export default function CriterionRow({ label, value, title }: { label: string; value?: string; title: string }) { export default function CriterionRow({ label, value, values, title }: { label: string; value?: string; values?: string[]; title: string }) {
const parts = csv(value); const parts = values ?? csv(value);
if (parts.length === 0) return null; if (parts.length === 0) return null;
return ( return (
<Tooltip title={title}> <Tooltip title={title}>
@@ -1,9 +1,11 @@
import { useState } from 'react'; import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Alert, Button, Col, Input, InputNumber, Row, Select, Space, Tag } from 'antd'; import { Alert, Button, Col, Input, InputNumber, Row, Select, Space, Tag } from 'antd';
import { AimOutlined } from '@ant-design/icons'; import { AimOutlined } from '@ant-design/icons';
import { HttpUtil } from '@/utils'; import { HttpUtil } from '@/utils';
import { useInboundOptions } from '@/api/queries/useInboundOptions';
import { buildRemarkByTag, formatInboundTag } from './helpers';
interface RouteTesterProps { interface RouteTesterProps {
inboundTags: string[]; inboundTags: string[];
@@ -21,6 +23,8 @@ const PROTOCOL_OPTIONS = ['http', 'tls', 'quic', 'bittorrent'].map((p) => ({ lab
export default function RouteTester({ inboundTags, isMobile }: RouteTesterProps) { export default function RouteTester({ inboundTags, isMobile }: RouteTesterProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { data: inboundOptions } = useInboundOptions();
const remarkByTag = useMemo(() => buildRemarkByTag(inboundOptions || []), [inboundOptions]);
const [dest, setDest] = useState(''); const [dest, setDest] = useState('');
const [port, setPort] = useState<number | null>(443); const [port, setPort] = useState<number | null>(443);
const [network, setNetwork] = useState('tcp'); const [network, setNetwork] = useState('tcp');
@@ -97,7 +101,7 @@ export default function RouteTester({ inboundTags, isMobile }: RouteTesterProps)
allowClear allowClear
value={inboundTag} value={inboundTag}
onChange={setInboundTag} onChange={setInboundTag}
options={inboundTags.filter(Boolean).map((tag) => ({ label: tag, value: tag }))} options={inboundTags.filter(Boolean).map((tag) => ({ label: formatInboundTag(tag, remarkByTag), value: tag }))}
/> />
</Col> </Col>
<Col xs={12} sm={4}> <Col xs={12} sm={4}>
@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Dropdown, Tag, Tooltip } from 'antd'; import { Button, Dropdown, Tag, Tooltip } from 'antd';
import { import {
@@ -11,7 +12,8 @@ import {
HolderOutlined, HolderOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { chipPreview, ruleCriteriaChips } from './helpers'; import { useInboundOptions } from '@/api/queries/useInboundOptions';
import { buildRemarkByTag, chipPreview, inboundTagChipPreview, inboundTagsDisplayTitle, ruleCriteriaChips } from './helpers';
import type { RuleRow } from './types'; import type { RuleRow } from './types';
interface RuleCardListProps { interface RuleCardListProps {
@@ -36,6 +38,8 @@ export default function RuleCardList({
confirmDelete, confirmDelete,
}: RuleCardListProps) { }: RuleCardListProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { data: inboundOptions } = useInboundOptions();
const remarkByTag = useMemo(() => buildRemarkByTag(inboundOptions || []), [inboundOptions]);
return ( return (
<div className="rule-list"> <div className="rule-list">
{rows.length === 0 ? ( {rows.length === 0 ? (
@@ -74,7 +78,11 @@ export default function RuleCardList({
<div className="flow-side"> <div className="flow-side">
<span className="flow-label">{t('pages.xray.Inbounds')}</span> <span className="flow-label">{t('pages.xray.Inbounds')}</span>
{rule.inboundTag ? ( {rule.inboundTag ? (
<Tag color="blue" className="flow-tag">{chipPreview(rule.inboundTag)}</Tag> <Tooltip title={inboundTagsDisplayTitle(rule.inboundTag, remarkByTag)}>
<Tag color="blue" className="flow-tag">
{inboundTagChipPreview(rule.inboundTag, remarkByTag)}
</Tag>
</Tooltip>
) : ( ) : (
<span className="criterion-empty">any</span> <span className="criterion-empty">any</span>
)} )}
@@ -5,6 +5,7 @@ import { PlusOutlined, MinusOutlined, QuestionCircleOutlined } from '@ant-design
import { InputAddon } from '@/components/ui'; import { InputAddon } from '@/components/ui';
import { useInboundOptions } from '@/api/queries/useInboundOptions'; import { useInboundOptions } from '@/api/queries/useInboundOptions';
import { RuleFormSchema, type RuleFormValues } from '@/schemas/xray'; import { RuleFormSchema, type RuleFormValues } from '@/schemas/xray';
import { buildRemarkByTag, formatInboundTag } from './helpers';
export interface RoutingRule { export interface RoutingRule {
type?: string; type?: string;
@@ -74,13 +75,7 @@ export default function RuleFormModal({
const isEdit = rule != null; const isEdit = rule != null;
const { data: inboundOptions } = useInboundOptions(); const { data: inboundOptions } = useInboundOptions();
const remarkByTag = useMemo(() => { const remarkByTag = useMemo(() => buildRemarkByTag(inboundOptions || []), [inboundOptions]);
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(() => { useEffect(() => {
if (!open) return; if (!open) return;
@@ -279,7 +274,7 @@ export default function RuleFormModal({
mode="multiple" mode="multiple"
value={form.inboundTag} value={form.inboundTag}
onChange={(v) => update('inboundTag', v)} onChange={(v) => update('inboundTag', v)}
options={inboundTags.map((tag) => ({ value: tag, label: remarkByTag[tag] || tag }))} options={inboundTags.map((tag) => ({ value: tag, label: formatInboundTag(tag, remarkByTag) }))}
/> />
</Form.Item> </Form.Item>
+53 -2
View File
@@ -11,13 +11,64 @@ export function csv(value?: string): string[] {
return String(value).split(',').map((s) => s.trim()).filter(Boolean); return String(value).split(',').map((s) => s.trim()).filter(Boolean);
} }
export function chipPreview(value?: string): string { export function chipPreviewParts(parts: string[]): string {
const parts = csv(value);
if (parts.length === 0) return ''; if (parts.length === 0) return '';
if (parts.length === 1) return parts[0]; if (parts.length === 1) return parts[0];
return `${parts[0]} +${parts.length - 1}`; return `${parts[0]} +${parts.length - 1}`;
} }
export function chipPreview(value?: string): string {
return chipPreviewParts(csv(value));
}
/** Same lookup as RuleFormModal inbound select: remark first, else tag. */
export function buildRemarkByTag(
options: Array<{ tag?: string; remark?: string }>,
): Record<string, string> {
const map: Record<string, string> = {};
for (const ib of options) {
if (ib.tag) map[ib.tag] = ib.remark?.trim() || ib.tag;
}
return map;
}
/** Format a single inbound tag as `tag (remark)`, or just `tag` when no distinct remark. */
export function formatInboundTag(
tag: string,
remarkByTag: Record<string, string> = {},
): string {
const label = remarkByTag[tag]?.trim();
if (!label || label === tag) return tag;
return `${tag} (${label})`;
}
/**
* Formatted inbound entries — `tag (remark)` when a distinct remark exists, else
* `tag`. Returns an array (not a joined string) so callers never have to re-split
* on commas, which a remark may legitimately contain.
*/
export function formatInboundTagList(
tags?: string,
remarkByTag: Record<string, string> = {},
): string[] {
return csv(tags).map((tag) => formatInboundTag(tag, remarkByTag));
}
export function inboundTagsDisplayTitle(
tags?: string,
remarkByTag: Record<string, string> = {},
): string | undefined {
const list = formatInboundTagList(tags, remarkByTag);
return list.length > 0 ? list.join(', ') : undefined;
}
export function inboundTagChipPreview(
tags?: string,
remarkByTag: Record<string, string> = {},
): string {
return chipPreviewParts(formatInboundTagList(tags, remarkByTag));
}
export function ruleCriteriaChips(rule: RuleRow) { export function ruleCriteriaChips(rule: RuleRow) {
const chips: { label: string; value?: string }[] = []; const chips: { label: string; value?: string }[] = [];
if (rule.domain) chips.push({ label: 'Domain', value: rule.domain }); if (rule.domain) chips.push({ label: 'Domain', value: rule.domain });
@@ -13,7 +13,9 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { useInboundOptions } from '@/api/queries/useInboundOptions';
import CriterionRow from './CriterionRow'; import CriterionRow from './CriterionRow';
import { buildRemarkByTag, formatInboundTagList, inboundTagsDisplayTitle } from './helpers';
import type { RuleRow } from './types'; import type { RuleRow } from './types';
interface RoutingColumnsParams { interface RoutingColumnsParams {
@@ -40,6 +42,8 @@ export function useRoutingColumns({
confirmDelete, confirmDelete,
}: RoutingColumnsParams): ColumnsType<RuleRow> { }: RoutingColumnsParams): ColumnsType<RuleRow> {
const { t } = useTranslation(); const { t } = useTranslation();
const { data: inboundOptions } = useInboundOptions();
const remarkByTag = useMemo(() => buildRemarkByTag(inboundOptions || []), [inboundOptions]);
return useMemo( return useMemo(
() => [ () => [
{ {
@@ -131,13 +135,22 @@ export function useRoutingColumns({
align: 'left', align: 'left',
width: 180, width: 180,
key: 'inbound', key: 'inbound',
render: (_v, record) => ( render: (_v, record) => {
<div className="criterion-flow"> const inboundParts = formatInboundTagList(record.inboundTag, remarkByTag);
{record.inboundTag && <CriterionRow label="Tag" value={record.inboundTag} title={`Inbound tag: ${record.inboundTag}`} />} return (
{record.user && <CriterionRow label="User" value={record.user} title={`User: ${record.user}`} />} <div className="criterion-flow">
{!record.inboundTag && !record.user && <span className="criterion-empty"></span>} {inboundParts.length > 0 && (
</div> <CriterionRow
), label="Tag"
values={inboundParts}
title={`Inbound tag: ${inboundTagsDisplayTitle(record.inboundTag, remarkByTag) ?? inboundParts.join(', ')}`}
/>
)}
{record.user && <CriterionRow label="User" value={record.user} title={`User: ${record.user}`} />}
{inboundParts.length === 0 && !record.user && <span className="criterion-empty"></span>}
</div>
);
},
}, },
{ {
title: t('pages.xray.Outbounds'), title: t('pages.xray.Outbounds'),
@@ -171,7 +184,6 @@ export function useRoutingColumns({
), ),
}, },
], ],
// eslint-disable-next-line react-hooks/exhaustive-deps [t, isMobile, rowsLength, showSource, showBalancer, remarkByTag, onHandlePointerDown, openEdit, moveUp, moveDown, confirmDelete],
[t, isMobile, rowsLength, showSource, showBalancer],
); );
} }