feat(connections): prefer clicked targets and support IP rules

This commit is contained in:
Tunglies
2026-11-15 11:57:55 +08:00
parent e75b6c9a46
commit 9004a8cdf8
23 changed files with 222 additions and 43 deletions
+1 -1
View File
@@ -51,7 +51,7 @@
<summary><strong> ✨ 新增功能 </strong></summary>
- 新增连接页面按网络协议(TCP/UDP)筛选连接
- 新增从连接详情快捷设置域名或进程规则,支持子域匹配与修改已有规则
- 新增从连接详情快捷设置域名、进程或源/目标 IP 规则,支持子域匹配与修改已有规则
</details>
@@ -8,14 +8,21 @@ import { closeConnection } from 'tauri-plugin-mihomo-api'
import { useProfiles } from '@/hooks/use-profiles'
import parseTraffic from '@/utils/parse-traffic'
import { getConnectionRuleTargets } from './connection-rule'
import {
getConnectionRuleTargets,
type ConnectionRuleKind,
} from './connection-rule'
import {
ConnectionRuleDialog,
type ConnectionRuleTarget,
} from './connection-rule-dialog'
export interface ConnectionDetailRef {
open: (detail: IConnectionsItem, closed: boolean) => void
open: (
detail: IConnectionsItem,
closed: boolean,
preferredKind?: ConnectionRuleKind,
) => void
close: () => void
}
@@ -23,6 +30,8 @@ export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
const [open, setOpen] = useState(false)
const [detail, setDetail] = useState<IConnectionsItem | null>(null)
const [closed, setClosed] = useState(false)
const [preferredKind, setPreferredKind] =
useState<ConnectionRuleKind>('domain')
const theme = useTheme()
const [ruleTarget, setRuleTarget] = useState<ConnectionRuleTarget | null>(
null,
@@ -35,11 +44,16 @@ export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
}, [])
useImperativeHandle(ref, () => ({
open: (detail: IConnectionsItem, closed: boolean) => {
open: (
detail: IConnectionsItem,
closed: boolean,
preferredKind = 'domain',
) => {
if (open) return
setOpen(true)
setDetail(detail)
setClosed(closed)
setPreferredKind(preferredKind)
},
close: onClose,
}))
@@ -67,6 +81,7 @@ export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
<InnerConnectionDetail
data={detail}
closed={closed}
preferredKind={preferredKind}
onClose={onClose}
onAddRule={(target) => {
setRuleTarget(target)
@@ -89,6 +104,7 @@ export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
interface InnerProps {
data: IConnectionsItem
closed: boolean
preferredKind: ConnectionRuleKind
onClose?: () => void
onAddRule: (target: ConnectionRuleTarget) => void
}
@@ -96,6 +112,7 @@ interface InnerProps {
const InnerConnectionDetail = ({
data,
closed,
preferredKind,
onClose,
onAddRule,
}: InnerProps) => {
@@ -106,7 +123,10 @@ const InnerConnectionDetail = ({
const canAddRule = Boolean(
current?.uid &&
current.option?.rules &&
(targets.domain || targets.process),
(targets.domain ||
targets.process ||
targets.source ||
targets.destination),
)
const theme = useTheme()
const chains = [...data.chains].reverse().join(' / ')
@@ -211,6 +231,7 @@ const InnerConnectionDetail = ({
profileUid: current.uid,
profileName: current.name || current.uid,
rulesUid: current.option.rules,
preferredKind,
})
}}
>
@@ -232,7 +253,10 @@ const InnerConnectionDetail = ({
{!canAddRule && (
<Box sx={{ mt: 1, typography: 'caption' }}>
{t(
targets.domain || targets.process
targets.domain ||
targets.process ||
targets.source ||
targets.destination
? 'connections.ruleDialog.noProfile'
: 'connections.ruleDialog.noMetadata',
)}
@@ -7,11 +7,12 @@ import { closeConnection } from 'tauri-plugin-mihomo-api'
import { RelativeTime } from './connection-relative-time'
import type { ConnectionRowView } from './connection-row-view'
import type { ConnectionRuleKind } from './connection-rule'
interface Props {
row: ConnectionRowView
closed: boolean
onShowDetail: (id: string) => void
onShowDetail: (id: string, preferredKind?: ConnectionRuleKind) => void
}
const tagStyle = {
@@ -86,7 +87,17 @@ export const ConnectionRowItem = memo(
<div style={tagsStyle}>
<span style={tagStyle}>{row.network}</span>
<span style={tagStyle}>{row.type}</span>
{row.process && <span style={tagStyle}>{row.process}</span>}
{row.process && (
<span
style={tagStyle}
onClick={(event) => {
event.stopPropagation()
onShowDetail(row.id, 'process')
}}
>
{row.process}
</span>
)}
{row.chains && <span style={tagStyle}>{row.chains}</span>}
<span style={tagStyle}>
<RelativeTime start={row.time} />
@@ -30,6 +30,7 @@ import {
getConnectionRuleTargets,
findConnectionRule,
saveConnectionRule,
type ConnectionRuleKind,
} from './connection-rule'
export interface ConnectionRuleTarget {
@@ -37,9 +38,17 @@ export interface ConnectionRuleTarget {
profileUid: string
profileName: string
rulesUid: string
preferredKind: ConnectionRuleKind
}
type ConnectionRuleKind = 'domain' | 'process'
function getInitialKind(target: ConnectionRuleTarget) {
const targets = getConnectionRuleTargets(target.metadata)
return targets[target.preferredKind]
? target.preferredKind
: ((['domain', 'process', 'source', 'destination'] as const).find(
(kind) => targets[kind],
) ?? 'domain')
}
export function ConnectionRuleDialog({
target,
@@ -52,8 +61,8 @@ export function ConnectionRuleDialog({
const { proxyView } = useProxiesData()
const { clashConfig } = useClashConfigData()
const targets = getConnectionRuleTargets(target.metadata)
const [kind, setKind] = useState<ConnectionRuleKind>(
targets.domain ? 'domain' : 'process',
const [kind, setKind] = useState<ConnectionRuleKind>(() =>
getInitialKind(target),
)
const [policy, setPolicy] = useState<string | null>(null)
const [scope, setScope] = useState<'DOMAIN' | 'DOMAIN-SUFFIX'>('DOMAIN')
@@ -83,6 +92,8 @@ export function ConnectionRuleDialog({
const kindLabels = {
domain: t('connections.ruleDialog.domain'),
process: t('connections.components.fields.process'),
source: t('connections.ruleDialog.sourceIp'),
destination: t('connections.ruleDialog.destinationIp'),
}
const unavailablePolicy = Boolean(policy && !policies.includes(policy))
@@ -106,9 +117,15 @@ export function ConnectionRuleDialog({
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 = ruleTargets.domain ? 'domain' : 'process'
const initialKind = getInitialKind(target)
const initial =
initialKind === 'domain'
? (rules.domain ?? rules.suffix)
@@ -208,13 +225,19 @@ export function ConnectionRuleDialog({
)
}}
>
{(['domain', 'process'] as const).map((option) => (
<MenuItem key={option} value={option} disabled={!targets[option]}>
{kindLabels[option]}
{!targets[option] &&
` · ${t('connections.ruleDialog.unavailable')}`}
</MenuItem>
))}
{(['domain', 'process', 'source', 'destination'] as const).map(
(option) => (
<MenuItem
key={option}
value={option}
disabled={!targets[option]}
>
{kindLabels[option]}
{!targets[option] &&
` · ${t('connections.ruleDialog.unavailable')}`}
</MenuItem>
),
)}
</TextField>
{kind === 'process' ? (
<>
@@ -284,6 +307,15 @@ export function ConnectionRuleDialog({
/>
</RadioGroup>
)}
{(kind === 'source' || kind === 'destination') && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{t(
kind === 'source'
? 'connections.ruleDialog.sourceHint'
: 'connections.ruleDialog.destinationHint',
)}
</Typography>
)}
{kind === 'process' && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
{selected?.type === 'PROCESS-PATH'
+29 -4
View File
@@ -2,10 +2,27 @@ import { dump, load } from 'js-yaml'
import isFQDN from 'validator/es/lib/isFQDN'
import isIP from 'validator/es/lib/isIP'
export type ConnectionRuleKind = 'domain' | 'process' | 'source' | 'destination'
function getAddressRule(address: string | undefined, source: boolean) {
const ip = address?.trim()
if (!ip || !isIP(ip)) return undefined
const ipv4 = isIP(ip, 4)
return {
type: source ? 'SRC-IP-CIDR' : ipv4 ? 'IP-CIDR' : 'IP-CIDR6',
value: `${ip}/${ipv4 ? 32 : 128}`,
}
}
export function getConnectionRuleTargets(
metadata: Pick<
IConnectionsItem['metadata'],
'host' | 'process' | 'processPath'
| 'host'
| 'process'
| 'processPath'
| 'sourceIP'
| 'destinationIP'
| 'remoteDestination'
>,
) {
const host = metadata.host?.trim() ?? ''
@@ -20,7 +37,11 @@ export function getConnectionRuleTargets(
: name?.trim()
? { type: 'PROCESS-NAME', value: name }
: undefined
return { domain, process }
const source = getAddressRule(metadata.sourceIP, true)
const destination =
getAddressRule(metadata.destinationIP, false) ??
getAddressRule(metadata.remoteDestination, false)
return { domain, process, source, destination }
}
function parseRuleExtension(content: string) {
@@ -55,7 +76,11 @@ interface RuleCondition {
function matchesConnectionRule(raw: string, target: RuleCondition) {
const parts = raw.split(',').map((part) => part.trim())
if (parts.length !== 3) return false
const hasNoResolve =
(target.type === 'IP-CIDR' || target.type === 'IP-CIDR6') &&
parts.length === 4 &&
parts[3] === 'no-resolve'
if (parts.length !== 3 && !hasNoResolve) return false
if (target.type === 'DOMAIN' || target.type === 'DOMAIN-SUFFIX') {
return (
parts[0] === target.type &&
@@ -92,7 +117,7 @@ export function saveConnectionRule(
if (!matchesConnectionRule(raw, target)) return [raw]
if (replaced) return []
replaced = true
return [rule]
return [raw.split(',').length === 4 ? `${rule},no-resolve` : rule]
})
}
if (!replaced) {
+22 -4
View File
@@ -29,6 +29,7 @@ import {
getConnectionStartTime,
getConnectionTypeLabel,
} from './connection-row-view'
import type { ConnectionRuleKind } from './connection-rule'
const ROW_HEIGHT = 40
const RESIZE_HANDLE_WIDTH = 6
@@ -247,7 +248,7 @@ const renderCell = (
interface RowComponentProps {
row: IConnectionsItem
columns: DisplayColumn[]
onShowDetail: (id: string) => void
onShowDetail: (id: string, preferredKind?: ConnectionRuleKind) => void
getSnapshot: (row: IConnectionsItem) => TableRowSnapshot
borderColor: string
virtualTop: number
@@ -263,7 +264,22 @@ const RowComponent = memo(
virtualTop,
}: RowComponentProps) {
const handleClick = useCallback(
() => onShowDetail(row.id),
(event: ReactMouseEvent<HTMLDivElement>) => {
const field =
event.target instanceof Element
? event.target.closest<HTMLElement>('[data-connection-field]')
?.dataset.connectionField
: undefined
const preferredKind =
field === 'process'
? 'process'
: field === 'source'
? 'source'
: field === 'remoteDestination'
? 'destination'
: 'domain'
onShowDetail(row.id, preferredKind)
},
[onShowDetail, row.id],
)
const snapshot = getSnapshot(row)
@@ -285,6 +301,7 @@ const RowComponent = memo(
{columns.map((column) => (
<div
key={column.field}
data-connection-field={column.field}
style={{
boxSizing: 'border-box',
flex: `0 0 ${column.size}px`,
@@ -318,7 +335,7 @@ const RowComponent = memo(
interface Props {
connections: IConnectionsItem[]
onShowDetail: (id: string) => void
onShowDetail: (id: string, preferredKind?: ConnectionRuleKind) => void
columnManagerOpen: boolean
onCloseColumnManager: () => void
}
@@ -333,7 +350,8 @@ export const ConnectionTable = (props: Props) => {
const onShowDetailRef = useRef(rawOnShowDetail)
onShowDetailRef.current = rawOnShowDetail
const onShowDetail = useCallback(
(id: string) => onShowDetailRef.current(id),
(id: string, preferredKind?: ConnectionRuleKind) =>
onShowDetailRef.current(id, preferredKind),
[],
)
const { t } = useTranslation()
+5 -1
View File
@@ -46,7 +46,7 @@
"applyFailed": "The rule could not be applied. Your selection has been kept.",
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
"saved": "Rule saved and configuration applied.",
"noMetadata": "This connection has no domain or process information for adding a rule.",
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
"noProfile": "No active profile with an editable rule extension is available.",
"entry": "Configure rule…",
"modifyTitle": "Modify rule",
@@ -56,6 +56,10 @@
"policyUnavailable": "This policy is no longer available. Choose another policy.",
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
"saveAndApply": "Save and apply",
"sourceIp": "Source IP",
"destinationIp": "Destination IP",
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
"fullPath": "Full path"
}
}
+5 -1
View File
@@ -46,7 +46,7 @@
"applyFailed": "The rule could not be applied. Your selection has been kept.",
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
"saved": "Rule saved and configuration applied.",
"noMetadata": "This connection has no domain or process information for adding a rule.",
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
"noProfile": "No active profile with an editable rule extension is available.",
"entry": "Configure rule…",
"modifyTitle": "Modify rule",
@@ -56,6 +56,10 @@
"policyUnavailable": "This policy is no longer available. Choose another policy.",
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
"saveAndApply": "Save and apply",
"sourceIp": "Source IP",
"destinationIp": "Destination IP",
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
"fullPath": "Full path"
}
}
+5 -1
View File
@@ -46,7 +46,7 @@
"applyFailed": "The rule could not be applied. Your selection has been kept.",
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
"saved": "Rule saved and configuration applied.",
"noMetadata": "This connection has no domain or process information for adding a rule.",
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
"noProfile": "No active profile with an editable rule extension is available.",
"entry": "Configure rule…",
"modifyTitle": "Modify rule",
@@ -56,6 +56,10 @@
"policyUnavailable": "This policy is no longer available. Choose another policy.",
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
"saveAndApply": "Save and apply",
"sourceIp": "Source IP",
"destinationIp": "Destination IP",
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
"fullPath": "Full path"
}
}
+5 -1
View File
@@ -46,7 +46,7 @@
"applyFailed": "The rule could not be applied. Your selection has been kept.",
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
"saved": "Rule saved and configuration applied.",
"noMetadata": "This connection has no domain or process information for adding a rule.",
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
"noProfile": "No active profile with an editable rule extension is available.",
"entry": "Configure rule…",
"modifyTitle": "Modify rule",
@@ -56,6 +56,10 @@
"policyUnavailable": "This policy is no longer available. Choose another policy.",
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
"saveAndApply": "Save and apply",
"sourceIp": "Source IP",
"destinationIp": "Destination IP",
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
"fullPath": "Full path"
}
}
+5 -1
View File
@@ -46,7 +46,7 @@
"applyFailed": "The rule could not be applied. Your selection has been kept.",
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
"saved": "Rule saved and configuration applied.",
"noMetadata": "This connection has no domain or process information for adding a rule.",
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
"noProfile": "No active profile with an editable rule extension is available.",
"entry": "Configure rule…",
"modifyTitle": "Modify rule",
@@ -56,6 +56,10 @@
"policyUnavailable": "This policy is no longer available. Choose another policy.",
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
"saveAndApply": "Save and apply",
"sourceIp": "Source IP",
"destinationIp": "Destination IP",
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
"fullPath": "Full path"
}
}
+5 -1
View File
@@ -46,7 +46,7 @@
"applyFailed": "The rule could not be applied. Your selection has been kept.",
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
"saved": "Rule saved and configuration applied.",
"noMetadata": "This connection has no domain or process information for adding a rule.",
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
"noProfile": "No active profile with an editable rule extension is available.",
"entry": "Configure rule…",
"modifyTitle": "Modify rule",
@@ -56,6 +56,10 @@
"policyUnavailable": "This policy is no longer available. Choose another policy.",
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
"saveAndApply": "Save and apply",
"sourceIp": "Source IP",
"destinationIp": "Destination IP",
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
"fullPath": "Full path"
}
}
+5 -1
View File
@@ -46,7 +46,7 @@
"applyFailed": "The rule could not be applied. Your selection has been kept.",
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
"saved": "Rule saved and configuration applied.",
"noMetadata": "This connection has no domain or process information for adding a rule.",
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
"noProfile": "No active profile with an editable rule extension is available.",
"entry": "Configure rule…",
"modifyTitle": "Modify rule",
@@ -56,6 +56,10 @@
"policyUnavailable": "This policy is no longer available. Choose another policy.",
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
"saveAndApply": "Save and apply",
"sourceIp": "Source IP",
"destinationIp": "Destination IP",
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
"fullPath": "Full path"
}
}
+5 -1
View File
@@ -46,7 +46,7 @@
"applyFailed": "The rule could not be applied. Your selection has been kept.",
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
"saved": "Rule saved and configuration applied.",
"noMetadata": "This connection has no domain or process information for adding a rule.",
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
"noProfile": "No active profile with an editable rule extension is available.",
"entry": "Configure rule…",
"modifyTitle": "Modify rule",
@@ -56,6 +56,10 @@
"policyUnavailable": "This policy is no longer available. Choose another policy.",
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
"saveAndApply": "Save and apply",
"sourceIp": "Source IP",
"destinationIp": "Destination IP",
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
"fullPath": "Full path"
}
}
+5 -1
View File
@@ -46,7 +46,7 @@
"applyFailed": "The rule could not be applied. Your selection has been kept.",
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
"saved": "Rule saved and configuration applied.",
"noMetadata": "This connection has no domain or process information for adding a rule.",
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
"noProfile": "No active profile with an editable rule extension is available.",
"entry": "Configure rule…",
"modifyTitle": "Modify rule",
@@ -56,6 +56,10 @@
"policyUnavailable": "This policy is no longer available. Choose another policy.",
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
"saveAndApply": "Save and apply",
"sourceIp": "Source IP",
"destinationIp": "Destination IP",
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
"fullPath": "Full path"
}
}
+5 -1
View File
@@ -46,7 +46,7 @@
"applyFailed": "The rule could not be applied. Your selection has been kept.",
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
"saved": "Rule saved and configuration applied.",
"noMetadata": "This connection has no domain or process information for adding a rule.",
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
"noProfile": "No active profile with an editable rule extension is available.",
"entry": "Configure rule…",
"modifyTitle": "Modify rule",
@@ -56,6 +56,10 @@
"policyUnavailable": "This policy is no longer available. Choose another policy.",
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
"saveAndApply": "Save and apply",
"sourceIp": "Source IP",
"destinationIp": "Destination IP",
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
"fullPath": "Full path"
}
}
+5 -1
View File
@@ -46,7 +46,7 @@
"applyFailed": "The rule could not be applied. Your selection has been kept.",
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
"saved": "Rule saved and configuration applied.",
"noMetadata": "This connection has no domain or process information for adding a rule.",
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
"noProfile": "No active profile with an editable rule extension is available.",
"entry": "Configure rule…",
"modifyTitle": "Modify rule",
@@ -56,6 +56,10 @@
"policyUnavailable": "This policy is no longer available. Choose another policy.",
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
"saveAndApply": "Save and apply",
"sourceIp": "Source IP",
"destinationIp": "Destination IP",
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
"fullPath": "Full path"
}
}
+5 -1
View File
@@ -46,7 +46,7 @@
"applyFailed": "The rule could not be applied. Your selection has been kept.",
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
"saved": "Rule saved and configuration applied.",
"noMetadata": "This connection has no domain or process information for adding a rule.",
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
"noProfile": "No active profile with an editable rule extension is available.",
"entry": "Configure rule…",
"modifyTitle": "Modify rule",
@@ -56,6 +56,10 @@
"policyUnavailable": "This policy is no longer available. Choose another policy.",
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
"saveAndApply": "Save and apply",
"sourceIp": "Source IP",
"destinationIp": "Destination IP",
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
"fullPath": "Full path"
}
}
+5 -1
View File
@@ -46,7 +46,7 @@
"applyFailed": "规则未能应用,已保留当前选择。",
"unsupportedValue": "此内容包含快捷规则不支持的字符。",
"saved": "规则已保存,配置已应用。",
"noMetadata": "此连接没有可用于添加规则的域名或进程信息。",
"noMetadata": "此连接没有可用于设置规则的域名、进程或 IP 信息。",
"noProfile": "当前没有可编辑规则扩展的活动订阅。",
"entry": "设置规则…",
"modifyTitle": "修改规则",
@@ -56,6 +56,10 @@
"policyUnavailable": "此策略已不可用,请选择其他策略。",
"editDestination": "修改「{{name}}」中的现有规则,保留排序。",
"saveAndApply": "保存并应用",
"sourceIp": "源 IP",
"destinationIp": "目标 IP",
"sourceHint": "匹配来自此 IP 的所有请求,可能包含其他应用,不限定端口。",
"destinationHint": "匹配访问此目标 IP 的请求,覆盖所有域名,不限定端口。",
"fullPath": "完整路径"
}
}
+5 -1
View File
@@ -46,7 +46,7 @@
"applyFailed": "The rule could not be applied. Your selection has been kept.",
"unsupportedValue": "This value contains characters that cannot be used in a quick rule.",
"saved": "Rule saved and configuration applied.",
"noMetadata": "This connection has no domain or process information for adding a rule.",
"noMetadata": "This connection has no domain, process or IP information for configuring a rule.",
"noProfile": "No active profile with an editable rule extension is available.",
"entry": "Configure rule…",
"modifyTitle": "Modify rule",
@@ -56,6 +56,10 @@
"policyUnavailable": "This policy is no longer available. Choose another policy.",
"editDestination": "Update the existing rule in “{{name}}”, keeping its position.",
"saveAndApply": "Save and apply",
"sourceIp": "Source IP",
"destinationIp": "Destination IP",
"sourceHint": "Matches all requests from this IP, including requests from other applications. No port filtering.",
"destinationHint": "Matches this destination IP across all domains. No port filtering.",
"fullPath": "Full path"
}
}
+7 -2
View File
@@ -35,6 +35,7 @@ import {
getConnectionStartTime,
useConnectionRowViews,
} from '@/components/connection/connection-row-view'
import type { ConnectionRuleKind } from '@/components/connection/connection-rule'
import { ConnectionTable } from '@/components/connection/connection-table'
import { useConnectionData } from '@/hooks/use-connection-data'
import { useConnectionSetting } from '@/hooks/use-connection-setting'
@@ -161,10 +162,14 @@ const ConnectionsPage = () => {
)
const showDetailById = useCallback(
(id: string) => {
(id: string, preferredKind: ConnectionRuleKind = 'domain') => {
const connection = filterConn.find((item) => item.id === id)
if (connection) {
detailRef.current?.open(connection, connectionsType === 'closed')
detailRef.current?.open(
connection,
connectionsType === 'closed',
preferredKind,
)
}
},
[connectionsType, filterConn],
+4
View File
@@ -46,6 +46,10 @@ export const translationKeys = [
'connections.ruleDialog.policyUnavailable',
'connections.ruleDialog.editDestination',
'connections.ruleDialog.saveAndApply',
'connections.ruleDialog.sourceIp',
'connections.ruleDialog.destinationIp',
'connections.ruleDialog.sourceHint',
'connections.ruleDialog.destinationHint',
'connections.ruleDialog.fullPath',
'home.page.tooltips.lightweightMode',
'home.page.tooltips.manual',
+4
View File
@@ -41,6 +41,8 @@ export interface TranslationResources {
allSubdomains: string
applyFailed: string
destination: string
destinationHint: string
destinationIp: string
domain: string
domainScope: string
editDestination: string
@@ -59,6 +61,8 @@ export interface TranslationResources {
ruleModeHint: string
saveAndApply: string
saved: string
sourceHint: string
sourceIp: string
title: string
unavailable: string
unsupportedValue: string