feat(routing): add panel-only comment field to routing rules (#6361)

Can annotate rules with a human-readable note for easier management.
The comment is stripped before sending the config to xray-core (same
pattern as the existing 'enabled' flag).

Backend: stripDisabledRules now removes 'comment' from generated config.
Frontend: input field in RuleFormModal, column in desktop table, chip
with tooltip in mobile card list. Schema and type definitions updated.
This commit is contained in:
Sentiago
2026-09-02 21:22:50 +03:00
committed by GitHub
parent 7100fbcd08
commit 1bf078c51e
9 changed files with 71 additions and 4 deletions
@@ -14,6 +14,36 @@
transition: opacity 0.15s;
}
.rule-comment-cell {
display: block;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
color: var(--ant-color-text-tertiary);
}
.rule-comment {
display: flex;
align-items: center;
gap: 4px;
margin-top: 6px;
padding: 2px 6px;
font-size: 12px;
color: var(--ant-color-text-tertiary);
border-radius: 4px;
background: var(--ant-color-fill-tertiary);
max-width: 100%;
overflow: hidden;
}
.rule-comment-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.drag-handle:hover {
opacity: 0.8;
}
@@ -89,6 +89,7 @@ export default function RoutingTab({
if (rule.attrs && typeof rule.attrs === 'object' && !Array.isArray(rule.attrs)) {
r.attrs = JSON.stringify(rule.attrs, null, 2);
}
r.comment = rule.comment || undefined;
r.outboundTag = rule.outboundTag;
r.balancerTag = rule.balancerTag;
return r;
@@ -181,6 +181,13 @@ export default function RuleCardList({
))}
</div>
)}
{rule.comment && (
<Tooltip title={rule.comment}>
<div className="rule-comment">
<span className="rule-comment-text">{rule.comment}</span>
</div>
</Tooltip>
)}
</div>
))
)}
@@ -13,6 +13,7 @@ import { buildRemarkByTag, formatInboundTag, isApiRule } from './helpers';
export interface RoutingRule {
enabled?: boolean;
comment?: string;
type?: string;
domain?: string | string[];
ip?: string | string[];
@@ -42,6 +43,7 @@ interface RuleFormModalProps {
const initialForm = (): RuleFormValues => ({
enabled: true,
comment: '',
domain: '',
ip: '',
port: '',
@@ -104,6 +106,7 @@ export default function RuleFormModal({
if (rule) {
methods.reset({
enabled: rule.enabled !== false,
comment: rule.comment || '',
domain: Array.isArray(rule.domain) ? rule.domain.join(',') : rule.domain || '',
ip: Array.isArray(rule.ip) ? rule.ip.join(',') : rule.ip || '',
port: rule.port || '',
@@ -132,6 +135,7 @@ export default function RuleFormModal({
const built: Record<string, unknown> = {
type: 'field',
enabled: v.enabled,
comment: v.comment,
domain: csv(v.domain),
ip: csv(v.ip),
port: v.port,
@@ -185,6 +189,10 @@ export default function RuleFormModal({
<Switch disabled={isApiRule(rule ?? {})} />
</FormField>
<FormField name="comment" label={t('comment')}>
<Input maxLength={200} showCount placeholder={t('comment')} />
</FormField>
<FormField
name="sourceIP"
label={
+1
View File
@@ -1,6 +1,7 @@
export interface RuleRow {
key: number;
enabled?: boolean;
comment?: string;
domain?: string;
ip?: string;
port?: string;
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Dropdown, Switch, Tag } from 'antd';
import { Button, Dropdown, Switch, Tag, Tooltip } from 'antd';
import {
MoreOutlined,
EditOutlined,
@@ -193,6 +193,20 @@ export function useRoutingColumns({
</div>
),
},
{
title: t('comment'),
align: 'left',
width: 150,
key: 'comment',
render: (_v, record) =>
record.comment ? (
<Tooltip title={record.comment}>
<span className="rule-comment-cell">{record.comment}</span>
</Tooltip>
) : (
<span className="criterion-empty"></span>
),
},
{
title: t('pages.inbounds.network'),
align: 'left',
+1
View File
@@ -15,6 +15,7 @@ export type RuleWebhook = z.infer<typeof RuleWebhookSchema>;
export const RuleObjectSchema = z.object({
type: z.literal('field').default('field'),
enabled: z.boolean().optional(),
comment: z.string().optional(),
domain: z.array(z.string()).optional(),
ip: z.array(z.string()).optional(),
port: PortValueSchema.optional(),
+1
View File
@@ -110,6 +110,7 @@ export const OutboundTestResultListSchema = z.array(OutboundTestResultSchema);
export const RuleFormSchema = z.object({
enabled: z.boolean(),
comment: z.string(),
domain: z.string(),
ip: z.string(),
port: z.string(),
+7 -3
View File
@@ -1060,9 +1060,9 @@ func resolveXrayLogPaths(logCfg json_util.RawMessage) json_util.RawMessage {
}
// stripDisabledRules removes routing rules marked `enabled: false` from the
// generated runtime config and strips the panel-only `enabled` key from the
// rest, since xray-core has no such field. The internal api rule is always
// kept (see isApiRule) so traffic stats can't be toggled off. The stored
// generated runtime config and strips panel-only keys (`enabled`, `comment`)
// from the rest, since xray-core has no such fields. The internal api rule is
// always kept (see isApiRule) so traffic stats can't be toggled off. The stored
// template is untouched — only the generated config is filtered.
func stripDisabledRules(routerCfg json_util.RawMessage) json_util.RawMessage {
if len(routerCfg) == 0 {
@@ -1097,6 +1097,10 @@ func stripDisabledRules(routerCfg json_util.RawMessage) json_util.RawMessage {
delete(rule, "enabled")
changed = true
}
if _, exists := rule["comment"]; exists {
delete(rule, "comment")
changed = true
}
activeRules = append(activeRules, rule)
}