Files
3x-ui/frontend/src/pages/xray/routing/RuleCardList.tsx
T
aleskxyz 8f408d2d6a 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>
2026-06-11 12:46:24 +02:00

127 lines
4.8 KiB
TypeScript

import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Dropdown, Tag, Tooltip } from 'antd';
import {
MoreOutlined,
EditOutlined,
DeleteOutlined,
ExportOutlined,
ClusterOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
HolderOutlined,
} from '@ant-design/icons';
import { useInboundOptions } from '@/api/queries/useInboundOptions';
import { buildRemarkByTag, chipPreview, inboundTagChipPreview, inboundTagsDisplayTitle, ruleCriteriaChips } from './helpers';
import type { RuleRow } from './types';
interface RuleCardListProps {
rows: RuleRow[];
draggedIndex: number | null;
dropTargetIndex: number | null;
onHandlePointerDown: (idx: number, ev: React.PointerEvent) => void;
openEdit: (idx: number) => void;
moveUp: (idx: number) => void;
moveDown: (idx: number) => void;
confirmDelete: (idx: number) => void;
}
export default function RuleCardList({
rows,
draggedIndex,
dropTargetIndex,
onHandlePointerDown,
openEdit,
moveUp,
moveDown,
confirmDelete,
}: RuleCardListProps) {
const { t } = useTranslation();
const { data: inboundOptions } = useInboundOptions();
const remarkByTag = useMemo(() => buildRemarkByTag(inboundOptions || []), [inboundOptions]);
return (
<div className="rule-list">
{rows.length === 0 ? (
<div className="rule-empty">—</div>
) : (
rows.map((rule, index) => (
<div
key={rule.key}
className={`rule-card ${draggedIndex === index ? 'row-dragging' : ''} ${
dropTargetIndex === index && draggedIndex != null && index < draggedIndex ? 'drop-before' : ''
} ${dropTargetIndex === index && draggedIndex != null && index > draggedIndex ? 'drop-after' : ''}`}
data-row-key={index}
>
<div className="rule-card-head">
<HolderOutlined
className="drag-handle"
onPointerDown={(ev) => onHandlePointerDown(index, ev)}
/>
<span className="rule-number">#{index + 1}</span>
<Dropdown
trigger={['click']}
menu={{
items: [
{ key: 'edit', label: <><EditOutlined /> {t('edit')}</>, onClick: () => openEdit(index) },
{ key: 'up', label: <ArrowUpOutlined />, disabled: index === 0, onClick: () => moveUp(index) },
{ key: 'down', label: <ArrowDownOutlined />, disabled: index === rows.length - 1, onClick: () => moveDown(index) },
{ key: 'del', danger: true, label: <><DeleteOutlined /> {t('delete')}</>, onClick: () => confirmDelete(index) },
],
}}
>
<Button shape="circle" size="small" icon={<MoreOutlined />} />
</Dropdown>
</div>
<div className="rule-flow">
<div className="flow-side">
<span className="flow-label">{t('pages.xray.Inbounds')}</span>
{rule.inboundTag ? (
<Tooltip title={inboundTagsDisplayTitle(rule.inboundTag, remarkByTag)}>
<Tag color="blue" className="flow-tag">
{inboundTagChipPreview(rule.inboundTag, remarkByTag)}
</Tag>
</Tooltip>
) : (
<span className="criterion-empty">any</span>
)}
</div>
<span className="flow-arrow">→</span>
<div className="flow-side flow-side-target">
<span className="flow-label">
{rule.balancerTag ? t('pages.xray.balancer') || 'Balancer' : t('pages.xray.Outbounds')}
</span>
{rule.outboundTag ? (
<Tag color="green" className="flow-tag">
<ExportOutlined /> {rule.outboundTag}
</Tag>
) : rule.balancerTag ? (
<Tag color="purple" className="flow-tag">
<ClusterOutlined /> {rule.balancerTag}
</Tag>
) : (
<span className="criterion-empty">—</span>
)}
</div>
</div>
{ruleCriteriaChips(rule).length > 0 && (
<div className="rule-criteria">
{ruleCriteriaChips(rule).map((chip) => (
<Tooltip key={chip.label} title={`${chip.label}: ${chip.value}`}>
<span className="criterion-chip">
<span className="criterion-chip-label">{chip.label}</span>
<span className="criterion-chip-value">{chipPreview(chip.value)}</span>
</span>
</Tooltip>
))}
</div>
)}
</div>
))
)}
</div>
);
}