From a7ccef5075c41b02b4d85f4ba5c835172ed2cc25 Mon Sep 17 00:00:00 2001
From: Tunglies <77394545+Tunglies@users.noreply.github.com>
Date: Sun, 15 Nov 2026 00:59:23 +0800
Subject: [PATCH] feat(connections): support parent domain rule editing
Closes #7900
---
Changelog.md | 2 +-
.../connection/connection-rule-dialog.tsx | 168 ++++++++++++------
src/components/connection/connection-rule.ts | 31 +++-
src/locales/ar/connections.json | 5 +-
src/locales/de/connections.json | 5 +-
src/locales/en/connections.json | 5 +-
src/locales/es/connections.json | 5 +-
src/locales/fa/connections.json | 5 +-
src/locales/id/connections.json | 5 +-
src/locales/jp/connections.json | 5 +-
src/locales/ko/connections.json | 5 +-
src/locales/pt/connections.json | 5 +-
src/locales/ru/connections.json | 5 +-
src/locales/tr/connections.json | 5 +-
src/locales/tt/connections.json | 5 +-
src/locales/zh/connections.json | 5 +-
src/locales/zhtw/connections.json | 5 +-
src/types/generated/i18n-keys.ts | 3 +
src/types/generated/i18n-resources.ts | 3 +
19 files changed, 198 insertions(+), 79 deletions(-)
diff --git a/Changelog.md b/Changelog.md
index 9aa3e0a67..ab44384c0 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -46,7 +46,7 @@
✨ 新增功能
- 新增连接页面按网络协议(TCP/UDP)筛选连接
-- 新增从连接详情快捷设置域名、进程或源/目标 IP 规则,支持子域匹配与修改已有规则
+- 新增从连接详情快捷设置域名、进程或源/目标 IP 规则,支持父域及子域匹配与修改已有规则
diff --git a/src/components/connection/connection-rule-dialog.tsx b/src/components/connection/connection-rule-dialog.tsx
index d31361ec8..b8e40e9ad 100644
--- a/src/components/connection/connection-rule-dialog.tsx
+++ b/src/components/connection/connection-rule-dialog.tsx
@@ -28,6 +28,8 @@ import { showNotice } from '@/services/notice-service'
import {
getConnectionRuleTargets,
+ getConnectionRules,
+ isConnectionRuleDomain,
findConnectionRule,
saveConnectionRule,
type ConnectionRuleKind,
@@ -66,20 +68,28 @@ export function ConnectionRuleDialog({
)
const [policy, setPolicy] = useState(null)
const [scope, setScope] = useState<'DOMAIN' | 'DOMAIN-SUFFIX'>('DOMAIN')
- const [existing, setExisting] = useState
- > | null>(null)
+ const [domainValue, setDomainValue] = useState(targets.domain?.value ?? '')
+ const [rules, setRules] = useState(null)
const [loadAttempt, setLoadAttempt] = useState(0)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const selected = targets[kind]
- const modifying = Boolean(
- existing?.[
- kind === 'domain' && scope === 'DOMAIN-SUFFIX' ? 'suffix' : kind
- ],
- )
+ const editingDomain = kind === 'domain' && scope === 'DOMAIN-SUFFIX'
+ const invalidDomain =
+ editingDomain &&
+ !isConnectionRuleDomain(domainValue, targets.domain?.value ?? '')
+ const condition = selected
+ ? {
+ type: kind === 'domain' ? scope : selected.type,
+ value: editingDomain ? domainValue.trim() : selected.value,
+ }
+ : undefined
+ const existing =
+ rules && condition && !invalidDomain
+ ? findConnectionRule(rules, condition)
+ : undefined
+ const modifying = Boolean(existing)
const groups = proxyView
? proxyView.groups.length || !proxyView.global
? proxyView.groups
@@ -97,40 +107,27 @@ export function ConnectionRuleDialog({
}
const unavailablePolicy = Boolean(policy && !policies.includes(policy))
- const blocked = loading || saving || !existing
+ const blocked = loading || saving || rules === null
useEffect(() => {
let cancelled = false
readProfileFile(target.rulesUid)
.then((content) => {
+ const loadedRules = getConnectionRules(content)
const ruleTargets = getConnectionRuleTargets(target.metadata)
- const rules = {
- domain: ruleTargets.domain
- ? findConnectionRule(content, ruleTargets.domain)
- : undefined,
- suffix: ruleTargets.domain
- ? findConnectionRule(content, {
- ...ruleTargets.domain,
- type: 'DOMAIN-SUFFIX',
- })
- : undefined,
- process: ruleTargets.process
- ? findConnectionRule(content, ruleTargets.process)
- : undefined,
- source: ruleTargets.source
- ? findConnectionRule(content, ruleTargets.source)
- : undefined,
- destination: ruleTargets.destination
- ? findConnectionRule(content, ruleTargets.destination)
- : undefined,
- }
- if (cancelled) return
const initialKind = getInitialKind(target)
- const initial =
- initialKind === 'domain'
- ? (rules.domain ?? rules.suffix)
- : rules[initialKind]
- setExisting(rules)
+ const selectedTarget = ruleTargets[initialKind]
+ const initial = selectedTarget
+ ? (findConnectionRule(loadedRules, selectedTarget) ??
+ (initialKind === 'domain'
+ ? findConnectionRule(loadedRules, {
+ ...selectedTarget,
+ type: 'DOMAIN-SUFFIX',
+ })
+ : undefined))
+ : undefined
+ if (cancelled) return
+ setRules(loadedRules)
setPolicy(initial?.policy ?? null)
setScope(initial?.type === 'DOMAIN-SUFFIX' ? 'DOMAIN-SUFFIX' : 'DOMAIN')
})
@@ -147,8 +144,15 @@ export function ConnectionRuleDialog({
}, [target, loadAttempt])
const handleSave = useLockFn(async () => {
- if (blocked || !selected || !policy || !policies.includes(policy)) return
- if (/[\r\n,]/u.test(selected.value) || /[\r\n,]/u.test(policy)) {
+ if (
+ blocked ||
+ !condition ||
+ invalidDomain ||
+ !policy ||
+ !policies.includes(policy)
+ )
+ return
+ if (/[\r\n,]/u.test(condition.value) || /[\r\n,]/u.test(policy)) {
setError(t('connections.ruleDialog.unsupportedValue'))
return
}
@@ -171,11 +175,7 @@ export function ConnectionRuleDialog({
}
await checkProfile()
const content = await readProfileFile(target.rulesUid)
- const updated = saveConnectionRule(
- content,
- { ...selected, type: kind === 'domain' ? scope : selected.type },
- policy,
- )
+ const updated = saveConnectionRule(content, condition, policy)
await checkProfile()
return saveProfileFile(target.rulesUid, updated)
},
@@ -214,10 +214,18 @@ export function ConnectionRuleDialog({
disabled={blocked}
onChange={(event) => {
const next = event.target.value as ConnectionRuleKind
+ const nextTarget = targets[next]
const rule =
- next === 'domain'
- ? (existing?.domain ?? existing?.suffix)
- : existing?.[next]
+ rules && nextTarget
+ ? (findConnectionRule(rules, nextTarget) ??
+ (next === 'domain'
+ ? findConnectionRule(rules, {
+ ...nextTarget,
+ type: 'DOMAIN-SUFFIX',
+ })
+ : undefined))
+ : undefined
+ setDomainValue(targets.domain?.value ?? '')
setKind(next)
setPolicy(rule?.policy ?? null)
setScope(
@@ -270,11 +278,47 @@ export function ConnectionRuleDialog({
{
+ const value = event.target.value
+ setDomainValue(value)
+ const match =
+ rules &&
+ isConnectionRuleDomain(value, targets.domain?.value ?? '')
+ ? findConnectionRule(rules, {
+ type: 'DOMAIN-SUFFIX',
+ value: value.trim(),
+ })
+ : undefined
+ if (match) setPolicy(match.policy)
+ }}
+ error={invalidDomain}
+ helperText={
+ editingDomain ? (
+ <>
+ {t('connections.ruleDialog.connectionHost', {
+ host: targets.domain?.value,
+ })}
+ {invalidDomain && (
+ <>
+
+ {t('connections.ruleDialog.invalidDomain')}
+ >
+ )}
+ >
+ ) : undefined
+ }
slotProps={{
- input: { readOnly: true },
- htmlInput: { title: selected?.value },
+ input: { readOnly: !editingDomain },
+ htmlInput: {
+ title: editingDomain ? domainValue : selected?.value,
+ },
}}
/>
)}
@@ -285,11 +329,17 @@ export function ConnectionRuleDialog({
aria-label={t('connections.ruleDialog.domainScope')}
value={scope}
onChange={(_, value) => {
- setScope(value as 'DOMAIN' | 'DOMAIN-SUFFIX')
- setPolicy(
- existing?.[value === 'DOMAIN-SUFFIX' ? 'suffix' : 'domain']
- ?.policy ?? null,
- )
+ const nextScope = value as 'DOMAIN' | 'DOMAIN-SUFFIX'
+ setScope(nextScope)
+ setDomainValue(targets.domain?.value ?? '')
+ const match =
+ rules && targets.domain
+ ? findConnectionRule(rules, {
+ ...targets.domain,
+ type: nextScope,
+ })
+ : undefined
+ setPolicy(match?.policy ?? null)
}}
sx={{ mt: 0.5 }}
>
@@ -363,7 +413,7 @@ export function ConnectionRuleDialog({
{error}
- {!existing && (
+ {rules === null && (