mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
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:
parent
10c185a592
commit
61105c2b1a
11 changed files with 45 additions and 14 deletions
21
frontend/src/api/queries/useInboundOptions.ts
Normal file
21
frontend/src/api/queries/useInboundOptions.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -36,7 +36,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.tag,
|
label: ib.remark?.trim() || ib.tag || '',
|
||||||
}));
|
}));
|
||||||
}, [inbounds]);
|
}, [inbounds]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,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.tag,
|
label: ib.remark?.trim() || ib.tag || '',
|
||||||
}));
|
}));
|
||||||
}, [inbounds]);
|
}, [inbounds]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,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.tag ?? '',
|
label: ib.remark?.trim() || ib.tag || '',
|
||||||
value: ib.id,
|
value: ib.id,
|
||||||
})),
|
})),
|
||||||
[inbounds],
|
[inbounds],
|
||||||
|
|
|
||||||
|
|
@ -261,9 +261,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.tag ?? '',
|
label: ib.remark?.trim() || ib.tag || '',
|
||||||
value: ib.id,
|
value: ib.id,
|
||||||
title: ib.tag ?? '',
|
title: ib.remark?.trim() || ib.tag || '',
|
||||||
})),
|
})),
|
||||||
[inbounds],
|
[inbounds],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -382,7 +382,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?.tag ?? '';
|
const label = ib?.remark?.trim() || ib?.tag || '';
|
||||||
return (
|
return (
|
||||||
<Tooltip key={id} title={label}>
|
<Tooltip key={id} title={label}>
|
||||||
<Tag color={color}>{label}</Tag>
|
<Tag color={color}>{label}</Tag>
|
||||||
|
|
|
||||||
|
|
@ -304,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?.tag ?? '';
|
return ib?.remark?.trim() || ib?.tag || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientBucket = useCallback((row: ClientRecord | null | undefined): Bucket | null => {
|
const clientBucket = useCallback((row: ClientRecord | null | undefined): Bucket | null => {
|
||||||
|
|
@ -694,7 +694,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?.tag ?? '';
|
const compactLabel = ib?.remark?.trim() || ib?.tag || '';
|
||||||
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 }}>
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,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.tag ?? '',
|
label: ib.remark?.trim() || ib.tag || '',
|
||||||
})),
|
})),
|
||||||
[inbounds],
|
[inbounds],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,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.tag ?? '' }));
|
.map((ib) => ({ value: ib.id, label: ib.remark?.trim() || ib.tag || '' }));
|
||||||
}, [dbInbounds, source]);
|
}, [dbInbounds, source]);
|
||||||
|
|
||||||
const filteredRows = useMemo(() => {
|
const filteredRows = useMemo(() => {
|
||||||
|
|
@ -150,7 +150,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?.tag ?? '' })}
|
title={t('pages.inbounds.attachClientsTitle', { remark: source?.remark?.trim() || source?.tag || '' })}
|
||||||
width={680}
|
width={680}
|
||||||
>
|
>
|
||||||
{messageContextHolder}
|
{messageContextHolder}
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,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?.tag ?? '' })}
|
title={t('pages.inbounds.attachExistingTitle', { remark: target?.remark?.trim() || target?.tag || '' })}
|
||||||
width={680}
|
width={680}
|
||||||
>
|
>
|
||||||
{messageContextHolder}
|
{messageContextHolder}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button, Form, Input, Modal, Select, Space, Tooltip } from 'antd';
|
import { Button, Form, Input, Modal, Select, Space, Tooltip } from 'antd';
|
||||||
import { PlusOutlined, MinusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
import { PlusOutlined, MinusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||||
import { InputAddon } from '@/components/ui';
|
import { InputAddon } from '@/components/ui';
|
||||||
|
import { useInboundOptions } from '@/api/queries/useInboundOptions';
|
||||||
import { RuleFormSchema, type RuleFormValues } from '@/schemas/xray';
|
import { RuleFormSchema, type RuleFormValues } from '@/schemas/xray';
|
||||||
|
|
||||||
export interface RoutingRule {
|
export interface RoutingRule {
|
||||||
|
|
@ -72,6 +73,15 @@ export default function RuleFormModal({
|
||||||
const [form, setForm] = useState<FormState>(initialForm);
|
const [form, setForm] = useState<FormState>(initialForm);
|
||||||
const isEdit = rule != null;
|
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(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
if (rule) {
|
if (rule) {
|
||||||
|
|
@ -269,7 +279,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: tag }))}
|
options={inboundTags.map((tag) => ({ value: tag, label: remarkByTag[tag] || tag }))}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue