mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-04 01:17:15 +00:00
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:
@@ -14,6 +14,36 @@
|
|||||||
transition: opacity 0.15s;
|
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 {
|
.drag-handle:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export default function RoutingTab({
|
|||||||
if (rule.attrs && typeof rule.attrs === 'object' && !Array.isArray(rule.attrs)) {
|
if (rule.attrs && typeof rule.attrs === 'object' && !Array.isArray(rule.attrs)) {
|
||||||
r.attrs = JSON.stringify(rule.attrs, null, 2);
|
r.attrs = JSON.stringify(rule.attrs, null, 2);
|
||||||
}
|
}
|
||||||
|
r.comment = rule.comment || undefined;
|
||||||
r.outboundTag = rule.outboundTag;
|
r.outboundTag = rule.outboundTag;
|
||||||
r.balancerTag = rule.balancerTag;
|
r.balancerTag = rule.balancerTag;
|
||||||
return r;
|
return r;
|
||||||
|
|||||||
@@ -181,6 +181,13 @@ export default function RuleCardList({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{rule.comment && (
|
||||||
|
<Tooltip title={rule.comment}>
|
||||||
|
<div className="rule-comment">
|
||||||
|
<span className="rule-comment-text">{rule.comment}</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { buildRemarkByTag, formatInboundTag, isApiRule } from './helpers';
|
|||||||
|
|
||||||
export interface RoutingRule {
|
export interface RoutingRule {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
comment?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
domain?: string | string[];
|
domain?: string | string[];
|
||||||
ip?: string | string[];
|
ip?: string | string[];
|
||||||
@@ -42,6 +43,7 @@ interface RuleFormModalProps {
|
|||||||
|
|
||||||
const initialForm = (): RuleFormValues => ({
|
const initialForm = (): RuleFormValues => ({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
comment: '',
|
||||||
domain: '',
|
domain: '',
|
||||||
ip: '',
|
ip: '',
|
||||||
port: '',
|
port: '',
|
||||||
@@ -104,6 +106,7 @@ export default function RuleFormModal({
|
|||||||
if (rule) {
|
if (rule) {
|
||||||
methods.reset({
|
methods.reset({
|
||||||
enabled: rule.enabled !== false,
|
enabled: rule.enabled !== false,
|
||||||
|
comment: rule.comment || '',
|
||||||
domain: Array.isArray(rule.domain) ? rule.domain.join(',') : rule.domain || '',
|
domain: Array.isArray(rule.domain) ? rule.domain.join(',') : rule.domain || '',
|
||||||
ip: Array.isArray(rule.ip) ? rule.ip.join(',') : rule.ip || '',
|
ip: Array.isArray(rule.ip) ? rule.ip.join(',') : rule.ip || '',
|
||||||
port: rule.port || '',
|
port: rule.port || '',
|
||||||
@@ -132,6 +135,7 @@ export default function RuleFormModal({
|
|||||||
const built: Record<string, unknown> = {
|
const built: Record<string, unknown> = {
|
||||||
type: 'field',
|
type: 'field',
|
||||||
enabled: v.enabled,
|
enabled: v.enabled,
|
||||||
|
comment: v.comment,
|
||||||
domain: csv(v.domain),
|
domain: csv(v.domain),
|
||||||
ip: csv(v.ip),
|
ip: csv(v.ip),
|
||||||
port: v.port,
|
port: v.port,
|
||||||
@@ -185,6 +189,10 @@ export default function RuleFormModal({
|
|||||||
<Switch disabled={isApiRule(rule ?? {})} />
|
<Switch disabled={isApiRule(rule ?? {})} />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField name="comment" label={t('comment')}>
|
||||||
|
<Input maxLength={200} showCount placeholder={t('comment')} />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name="sourceIP"
|
name="sourceIP"
|
||||||
label={
|
label={
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export interface RuleRow {
|
export interface RuleRow {
|
||||||
key: number;
|
key: number;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
comment?: string;
|
||||||
domain?: string;
|
domain?: string;
|
||||||
ip?: string;
|
ip?: string;
|
||||||
port?: string;
|
port?: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Dropdown, Switch, Tag } from 'antd';
|
import { Button, Dropdown, Switch, Tag, Tooltip } from 'antd';
|
||||||
import {
|
import {
|
||||||
MoreOutlined,
|
MoreOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
@@ -193,6 +193,20 @@ export function useRoutingColumns({
|
|||||||
</div>
|
</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'),
|
title: t('pages.inbounds.network'),
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type RuleWebhook = z.infer<typeof RuleWebhookSchema>;
|
|||||||
export const RuleObjectSchema = z.object({
|
export const RuleObjectSchema = z.object({
|
||||||
type: z.literal('field').default('field'),
|
type: z.literal('field').default('field'),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
|
comment: z.string().optional(),
|
||||||
domain: z.array(z.string()).optional(),
|
domain: z.array(z.string()).optional(),
|
||||||
ip: z.array(z.string()).optional(),
|
ip: z.array(z.string()).optional(),
|
||||||
port: PortValueSchema.optional(),
|
port: PortValueSchema.optional(),
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ export const OutboundTestResultListSchema = z.array(OutboundTestResultSchema);
|
|||||||
|
|
||||||
export const RuleFormSchema = z.object({
|
export const RuleFormSchema = z.object({
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
|
comment: z.string(),
|
||||||
domain: z.string(),
|
domain: z.string(),
|
||||||
ip: z.string(),
|
ip: z.string(),
|
||||||
port: z.string(),
|
port: z.string(),
|
||||||
|
|||||||
@@ -1060,9 +1060,9 @@ func resolveXrayLogPaths(logCfg json_util.RawMessage) json_util.RawMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// stripDisabledRules removes routing rules marked `enabled: false` from the
|
// stripDisabledRules removes routing rules marked `enabled: false` from the
|
||||||
// generated runtime config and strips the panel-only `enabled` key from the
|
// generated runtime config and strips panel-only keys (`enabled`, `comment`)
|
||||||
// rest, since xray-core has no such field. The internal api rule is always
|
// from the rest, since xray-core has no such fields. The internal api rule is
|
||||||
// kept (see isApiRule) so traffic stats can't be toggled off. The stored
|
// always kept (see isApiRule) so traffic stats can't be toggled off. The stored
|
||||||
// template is untouched — only the generated config is filtered.
|
// template is untouched — only the generated config is filtered.
|
||||||
func stripDisabledRules(routerCfg json_util.RawMessage) json_util.RawMessage {
|
func stripDisabledRules(routerCfg json_util.RawMessage) json_util.RawMessage {
|
||||||
if len(routerCfg) == 0 {
|
if len(routerCfg) == 0 {
|
||||||
@@ -1097,6 +1097,10 @@ func stripDisabledRules(routerCfg json_util.RawMessage) json_util.RawMessage {
|
|||||||
delete(rule, "enabled")
|
delete(rule, "enabled")
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
|
if _, exists := rule["comment"]; exists {
|
||||||
|
delete(rule, "comment")
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
activeRules = append(activeRules, rule)
|
activeRules = append(activeRules, rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user