diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index e7581aaec..8d1275b28 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -1621,6 +1621,9 @@ "example": 3, "type": "integer" }, + "outboundTag": { + "type": "string" + }, "panelVersion": { "example": "v3.x.x", "type": "string" @@ -1708,6 +1711,7 @@ "memPct", "name", "onlineCount", + "outboundTag", "panelVersion", "pinnedCertSha256", "port", @@ -6118,6 +6122,7 @@ "memPct": 45.1, "name": "de-fra-1", "onlineCount": 3, + "outboundTag": "", "panelVersion": "v3.x.x", "parentGuid": "", "pinnedCertSha256": "", diff --git a/frontend/src/api/queries/useOutboundTags.ts b/frontend/src/api/queries/useOutboundTags.ts index 102dea7f8..33c242886 100644 --- a/frontend/src/api/queries/useOutboundTags.ts +++ b/frontend/src/api/queries/useOutboundTags.ts @@ -7,7 +7,8 @@ import { fetchXrayConfig } from '@/hooks/useXraySetting'; // inbound's Telegram traffic to. Shares the cached xray config query so opening // the inbound form costs no extra request when the Xray page was already // visited; `select` derives just the tag list without disturbing other readers. -export function useOutboundTags() { +export function useOutboundTags(opts?: { excludeBlackhole?: boolean }) { + const excludeBlackhole = opts?.excludeBlackhole ?? false; return useQuery({ queryKey: keys.xray.config(), queryFn: fetchXrayConfig, @@ -15,8 +16,10 @@ export function useOutboundTags() { select: (data): string[] => { const tags = new Set(); for (const o of data?.xraySetting?.outbounds ?? []) { - const tag = (o as { tag?: string } | null)?.tag; - if (tag) tags.add(tag); + const ob = o as { tag?: string; protocol?: string } | null; + if (!ob?.tag) continue; + if (excludeBlackhole && ob.protocol === 'blackhole') continue; + tags.add(ob.tag); } for (const t of data?.subscriptionOutboundTags ?? []) { if (t) tags.add(t); diff --git a/frontend/src/generated/examples.ts b/frontend/src/generated/examples.ts index fa76c9e63..59b9f783d 100644 --- a/frontend/src/generated/examples.ts +++ b/frontend/src/generated/examples.ts @@ -354,6 +354,7 @@ export const EXAMPLES: Record = { "memPct": 45.1, "name": "de-fra-1", "onlineCount": 3, + "outboundTag": "", "panelVersion": "v3.x.x", "parentGuid": "", "pinnedCertSha256": "", diff --git a/frontend/src/generated/schemas.ts b/frontend/src/generated/schemas.ts index 3dea9c489..9a524fac1 100644 --- a/frontend/src/generated/schemas.ts +++ b/frontend/src/generated/schemas.ts @@ -1595,6 +1595,9 @@ export const SCHEMAS: Record = { "example": 3, "type": "integer" }, + "outboundTag": { + "type": "string" + }, "panelVersion": { "example": "v3.x.x", "type": "string" @@ -1682,6 +1685,7 @@ export const SCHEMAS: Record = { "memPct", "name", "onlineCount", + "outboundTag", "panelVersion", "pinnedCertSha256", "port", diff --git a/frontend/src/generated/types.ts b/frontend/src/generated/types.ts index f38e609b8..884a4d5dc 100644 --- a/frontend/src/generated/types.ts +++ b/frontend/src/generated/types.ts @@ -358,6 +358,7 @@ export interface Node { memPct: number; name: string; onlineCount: number; + outboundTag: string; panelVersion: string; parentGuid?: string; pinnedCertSha256: string; diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index 9a49eefec..cfdc086f1 100644 --- a/frontend/src/generated/zod.ts +++ b/frontend/src/generated/zod.ts @@ -384,6 +384,7 @@ export const NodeSchema = z.object({ memPct: z.number(), name: z.string(), onlineCount: z.number().int(), + outboundTag: z.string(), panelVersion: z.string(), parentGuid: z.string().optional(), pinnedCertSha256: z.string(), diff --git a/frontend/src/pages/nodes/NodeFormModal.tsx b/frontend/src/pages/nodes/NodeFormModal.tsx index 6d5f58d9a..3941f70bf 100644 --- a/frontend/src/pages/nodes/NodeFormModal.tsx +++ b/frontend/src/pages/nodes/NodeFormModal.tsx @@ -18,6 +18,7 @@ import type { RemoteInboundOption } from '@/api/queries/useNodeMutations'; import type { Msg } from '@/utils'; import { NodeFormSchema, type NodeFormValues, type ProbeResult } from '@/schemas/node'; import { antdRule } from '@/utils/zodForm'; +import { useOutboundTags } from '@/api/queries/useOutboundTags'; import './NodeFormModal.css'; type Mode = 'add' | 'edit'; @@ -49,6 +50,7 @@ function defaultValues(): NodeFormValues { pinnedCertSha256: '', inboundSyncMode: 'all', inboundTags: [], + outboundTag: '', }; } @@ -75,6 +77,7 @@ export default function NodeFormModal({ const scheme = Form.useWatch('scheme', form) ?? 'https'; const tlsVerifyMode = Form.useWatch('tlsVerifyMode', form) ?? 'verify'; const inboundSyncMode = Form.useWatch('inboundSyncMode', form) ?? 'all'; + const { data: outboundTags } = useOutboundTags({ excludeBlackhole: true }); useEffect(() => { if (!open) return; @@ -117,6 +120,7 @@ export default function NodeFormModal({ pinnedCertSha256: values.tlsVerifyMode === 'pin' ? values.pinnedCertSha256.trim() : '', inboundSyncMode: values.inboundSyncMode, inboundTags: values.inboundSyncMode === 'selected' ? values.inboundTags : [], + outboundTag: values.outboundTag || '', }; } @@ -356,6 +360,19 @@ export default function NodeFormModal({ + +