mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-15 07:12:09 +08:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cdd6f8308 | ||
|
|
a7ccef5075 | ||
|
|
18611cbfae | ||
|
|
b5b6e38c6b | ||
|
|
42f2d7be50 |
@@ -33,6 +33,7 @@
|
|||||||
- 修复断开链式代理时清空已选节点,导致再次连接需要重新选择的问题
|
- 修复断开链式代理时清空已选节点,导致再次连接需要重新选择的问题
|
||||||
- 修复「连接」页表格排序时表头换行、标题位置跳动的问题
|
- 修复「连接」页表格排序时表头换行、标题位置跳动的问题
|
||||||
- 修复「TUN 设置」未手动保存时,界面显示的默认 MTU 1500 未生效的问题
|
- 修复「TUN 设置」未手动保存时,界面显示的默认 MTU 1500 未生效的问题
|
||||||
|
- 修复连接详情较窄时操作按钮偏离右侧
|
||||||
|
|
||||||
**🍎 macOS**
|
**🍎 macOS**
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||||
|
|
||||||
- 新增连接页面按网络协议(TCP/UDP)筛选连接
|
- 新增连接页面按网络协议(TCP/UDP)筛选连接
|
||||||
|
- 新增从连接详情快捷设置域名、进程或源/目标 IP 规则,支持父域及子域匹配与修改已有规则
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,24 @@ import { useCallback, useImperativeHandle, useState, type Ref } from 'react'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { closeConnection } from 'tauri-plugin-mihomo-api'
|
import { closeConnection } from 'tauri-plugin-mihomo-api'
|
||||||
|
|
||||||
|
import { useProfiles } from '@/hooks/use-profiles'
|
||||||
import parseTraffic from '@/utils/parse-traffic'
|
import parseTraffic from '@/utils/parse-traffic'
|
||||||
|
|
||||||
|
import {
|
||||||
|
getConnectionRuleTargets,
|
||||||
|
type ConnectionRuleKind,
|
||||||
|
} from './connection-rule'
|
||||||
|
import {
|
||||||
|
ConnectionRuleDialog,
|
||||||
|
type ConnectionRuleTarget,
|
||||||
|
} from './connection-rule-dialog'
|
||||||
|
|
||||||
export interface ConnectionDetailRef {
|
export interface ConnectionDetailRef {
|
||||||
open: (detail: IConnectionsItem, closed: boolean) => void
|
open: (
|
||||||
|
detail: IConnectionsItem,
|
||||||
|
closed: boolean,
|
||||||
|
preferredKind?: ConnectionRuleKind,
|
||||||
|
) => void
|
||||||
close: () => void
|
close: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,7 +30,12 @@ export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
|
|||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [detail, setDetail] = useState<IConnectionsItem | null>(null)
|
const [detail, setDetail] = useState<IConnectionsItem | null>(null)
|
||||||
const [closed, setClosed] = useState(false)
|
const [closed, setClosed] = useState(false)
|
||||||
|
const [preferredKind, setPreferredKind] =
|
||||||
|
useState<ConnectionRuleKind>('domain')
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
const [ruleTarget, setRuleTarget] = useState<ConnectionRuleTarget | null>(
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
const onClose = useCallback(() => {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
@@ -25,51 +44,90 @@ export function ConnectionDetail({ ref }: { ref?: Ref<ConnectionDetailRef> }) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: (detail: IConnectionsItem, closed: boolean) => {
|
open: (
|
||||||
|
detail: IConnectionsItem,
|
||||||
|
closed: boolean,
|
||||||
|
preferredKind = 'domain',
|
||||||
|
) => {
|
||||||
if (open) return
|
if (open) return
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
setDetail(detail)
|
setDetail(detail)
|
||||||
setClosed(closed)
|
setClosed(closed)
|
||||||
|
setPreferredKind(preferredKind)
|
||||||
},
|
},
|
||||||
close: onClose,
|
close: onClose,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Snackbar
|
<Snackbar
|
||||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
||||||
open={open}
|
open={open}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
sx={{
|
sx={{
|
||||||
'.MuiSnackbarContent-root': {
|
'.MuiSnackbarContent-root': {
|
||||||
maxWidth: '520px',
|
width: '520px',
|
||||||
|
maxWidth: 'calc(100vw - 32px)',
|
||||||
|
boxSizing: 'border-box',
|
||||||
maxHeight: '480px',
|
maxHeight: '480px',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
},
|
},
|
||||||
|
'.MuiSnackbarContent-message': { width: '100%', minWidth: 0 },
|
||||||
}}
|
}}
|
||||||
message={
|
message={
|
||||||
detail ? (
|
detail ? (
|
||||||
<InnerConnectionDetail
|
<InnerConnectionDetail
|
||||||
data={detail}
|
data={detail}
|
||||||
closed={closed}
|
closed={closed}
|
||||||
|
preferredKind={preferredKind}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
onAddRule={(target) => {
|
||||||
|
setRuleTarget(target)
|
||||||
|
onClose()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{ruleTarget && (
|
||||||
|
<ConnectionRuleDialog
|
||||||
|
target={ruleTarget}
|
||||||
|
onClose={() => setRuleTarget(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InnerProps {
|
interface InnerProps {
|
||||||
data: IConnectionsItem
|
data: IConnectionsItem
|
||||||
closed: boolean
|
closed: boolean
|
||||||
|
preferredKind: ConnectionRuleKind
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
|
onAddRule: (target: ConnectionRuleTarget) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
const InnerConnectionDetail = ({
|
||||||
|
data,
|
||||||
|
closed,
|
||||||
|
preferredKind,
|
||||||
|
onClose,
|
||||||
|
onAddRule,
|
||||||
|
}: InnerProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { current } = useProfiles()
|
||||||
const { metadata, rulePayload } = data
|
const { metadata, rulePayload } = data
|
||||||
|
const targets = getConnectionRuleTargets(metadata)
|
||||||
|
const canAddRule = Boolean(
|
||||||
|
current?.uid &&
|
||||||
|
current.option?.rules &&
|
||||||
|
(targets.domain ||
|
||||||
|
targets.process ||
|
||||||
|
targets.source ||
|
||||||
|
targets.destination),
|
||||||
|
)
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const chains = [...data.chains].reverse().join(' / ')
|
const chains = [...data.chains].reverse().join(' / ')
|
||||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule
|
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule
|
||||||
@@ -105,7 +163,7 @@ const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
|||||||
{ label: t('connections.components.fields.rule'), value: rule },
|
{ label: t('connections.components.fields.rule'), value: rule },
|
||||||
{
|
{
|
||||||
label: t('connections.components.fields.process'),
|
label: t('connections.components.fields.process'),
|
||||||
value: `${metadata.process}${metadata.processPath ? `(${metadata.processPath})` : ''}`,
|
value: `${metadata.process ?? ''}${metadata.processPath ? `(${metadata.processPath})` : ''}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('connections.components.fields.time'),
|
label: t('connections.components.fields.time'),
|
||||||
@@ -132,7 +190,14 @@ const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
|||||||
const onDelete = useLockFn(async () => closeConnection(data.id))
|
const onDelete = useLockFn(async () => closeConnection(data.id))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ userSelect: 'text', color: theme.palette.text.secondary }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
minWidth: 0,
|
||||||
|
userSelect: 'text',
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{information.map((each) => (
|
{information.map((each) => (
|
||||||
<div key={each.label}>
|
<div key={each.label}>
|
||||||
<b>{each.label}</b>
|
<b>{each.label}</b>
|
||||||
@@ -147,8 +212,32 @@ const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: 1,
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
mt: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
disabled={!canAddRule}
|
||||||
|
onClick={() => {
|
||||||
|
if (!current?.uid || !current.option?.rules) return
|
||||||
|
onAddRule({
|
||||||
|
metadata: { ...metadata },
|
||||||
|
profileUid: current.uid,
|
||||||
|
profileName: current.name || current.uid,
|
||||||
|
rulesUid: current.option.rules,
|
||||||
|
preferredKind,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('connections.ruleDialog.entry')}
|
||||||
|
</Button>
|
||||||
{!closed && (
|
{!closed && (
|
||||||
<Box sx={{ textAlign: 'right' }}>
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
title={t('connections.components.actions.closeConnection')}
|
title={t('connections.components.actions.closeConnection')}
|
||||||
@@ -159,6 +248,18 @@ const InnerConnectionDetail = ({ data, closed, onClose }: InnerProps) => {
|
|||||||
>
|
>
|
||||||
{t('connections.components.actions.closeConnection')}
|
{t('connections.components.actions.closeConnection')}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{!canAddRule && (
|
||||||
|
<Box sx={{ mt: 1, typography: 'caption' }}>
|
||||||
|
{t(
|
||||||
|
targets.domain ||
|
||||||
|
targets.process ||
|
||||||
|
targets.source ||
|
||||||
|
targets.destination
|
||||||
|
? 'connections.ruleDialog.noProfile'
|
||||||
|
: 'connections.ruleDialog.noMetadata',
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import { closeConnection } from 'tauri-plugin-mihomo-api'
|
|||||||
|
|
||||||
import { RelativeTime } from './connection-relative-time'
|
import { RelativeTime } from './connection-relative-time'
|
||||||
import type { ConnectionRowView } from './connection-row-view'
|
import type { ConnectionRowView } from './connection-row-view'
|
||||||
|
import type { ConnectionRuleKind } from './connection-rule'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
row: ConnectionRowView
|
row: ConnectionRowView
|
||||||
closed: boolean
|
closed: boolean
|
||||||
onShowDetail: (id: string) => void
|
onShowDetail: (id: string, preferredKind?: ConnectionRuleKind) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagStyle = {
|
const tagStyle = {
|
||||||
@@ -86,7 +87,17 @@ export const ConnectionRowItem = memo(
|
|||||||
<div style={tagsStyle}>
|
<div style={tagsStyle}>
|
||||||
<span style={tagStyle}>{row.network}</span>
|
<span style={tagStyle}>{row.network}</span>
|
||||||
<span style={tagStyle}>{row.type}</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>}
|
{row.chains && <span style={tagStyle}>{row.chains}</span>}
|
||||||
<span style={tagStyle}>
|
<span style={tagStyle}>
|
||||||
<RelativeTime start={row.time} />
|
<RelativeTime start={row.time} />
|
||||||
|
|||||||
@@ -0,0 +1,477 @@
|
|||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Autocomplete,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
FormControlLabel,
|
||||||
|
MenuItem,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material'
|
||||||
|
import { useLockFn } from 'ahooks'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import {
|
||||||
|
useClashConfigData,
|
||||||
|
useProxiesData,
|
||||||
|
} from '@/providers/app-data-context'
|
||||||
|
import { getProfiles, readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||||
|
import { mutate } from '@/services/mutate'
|
||||||
|
import { showNotice } from '@/services/notice-service'
|
||||||
|
|
||||||
|
import {
|
||||||
|
getConnectionRuleTargets,
|
||||||
|
getConnectionRules,
|
||||||
|
isConnectionRuleDomain,
|
||||||
|
findConnectionRule,
|
||||||
|
saveConnectionRule,
|
||||||
|
type ConnectionRuleKind,
|
||||||
|
} from './connection-rule'
|
||||||
|
|
||||||
|
export interface ConnectionRuleTarget {
|
||||||
|
metadata: IConnectionsItem['metadata']
|
||||||
|
profileUid: string
|
||||||
|
profileName: string
|
||||||
|
rulesUid: string
|
||||||
|
preferredKind: ConnectionRuleKind
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
target: ConnectionRuleTarget
|
||||||
|
onClose: () => void
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { proxyView } = useProxiesData()
|
||||||
|
const { clashConfig } = useClashConfigData()
|
||||||
|
const targets = getConnectionRuleTargets(target.metadata)
|
||||||
|
const [kind, setKind] = useState<ConnectionRuleKind>(() =>
|
||||||
|
getInitialKind(target),
|
||||||
|
)
|
||||||
|
const [policy, setPolicy] = useState<string | null>(null)
|
||||||
|
const [scope, setScope] = useState<'DOMAIN' | 'DOMAIN-SUFFIX'>('DOMAIN')
|
||||||
|
const [domainValue, setDomainValue] = useState(targets.domain?.value ?? '')
|
||||||
|
const [rules, setRules] = useState<string[] | null>(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 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
|
||||||
|
: [proxyView.global]
|
||||||
|
: []
|
||||||
|
const policies = [
|
||||||
|
...new Set(['DIRECT', 'REJECT', ...groups.map((group) => group.name)]),
|
||||||
|
]
|
||||||
|
|
||||||
|
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))
|
||||||
|
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 initialKind = getInitialKind(target)
|
||||||
|
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')
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (!cancelled)
|
||||||
|
setError(err instanceof Error ? err.message : String(err))
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setLoading(false)
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [target, loadAttempt])
|
||||||
|
|
||||||
|
const handleSave = useLockFn(async () => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
setSaving(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
const result = await mutate(
|
||||||
|
async () => {
|
||||||
|
const checkProfile = async () => {
|
||||||
|
const profiles = await getProfiles()
|
||||||
|
const current = profiles.items?.find(
|
||||||
|
(item) => item.uid === profiles.current,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
profiles.current !== target.profileUid ||
|
||||||
|
current?.option?.rules !== target.rulesUid
|
||||||
|
) {
|
||||||
|
throw new Error(t('connections.ruleDialog.profileChanged'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await checkProfile()
|
||||||
|
const content = await readProfileFile(target.rulesUid)
|
||||||
|
const updated = saveConnectionRule(content, condition, policy)
|
||||||
|
await checkProfile()
|
||||||
|
return saveProfileFile(target.rulesUid, updated)
|
||||||
|
},
|
||||||
|
{ id: `save-profile-file:${target.rulesUid}`, errorNotice: false },
|
||||||
|
)
|
||||||
|
if (!result.ok || !result.value) {
|
||||||
|
setError(t('connections.ruleDialog.applyFailed'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showNotice.success('connections.ruleDialog.saved')
|
||||||
|
onClose()
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err))
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open onClose={saving ? undefined : onClose} fullWidth maxWidth="sm">
|
||||||
|
<DialogTitle>
|
||||||
|
{t(
|
||||||
|
modifying
|
||||||
|
? 'connections.ruleDialog.modifyTitle'
|
||||||
|
: 'connections.ruleDialog.title',
|
||||||
|
)}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 1 }}>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
label={t('connections.ruleDialog.matchBy')}
|
||||||
|
value={kind}
|
||||||
|
disabled={blocked}
|
||||||
|
onChange={(event) => {
|
||||||
|
const next = event.target.value as ConnectionRuleKind
|
||||||
|
const nextTarget = targets[next]
|
||||||
|
const rule =
|
||||||
|
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(
|
||||||
|
rule?.type === 'DOMAIN-SUFFIX' ? 'DOMAIN-SUFFIX' : 'DOMAIN',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(['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' ? (
|
||||||
|
<>
|
||||||
|
{target.metadata.process?.trim() && (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
label={kindLabels.process}
|
||||||
|
value={target.metadata.process}
|
||||||
|
slotProps={{
|
||||||
|
input: { readOnly: true },
|
||||||
|
htmlInput: { title: target.metadata.process },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selected?.type === 'PROCESS-PATH' && (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
label={t('connections.ruleDialog.fullPath')}
|
||||||
|
value={selected.value}
|
||||||
|
slotProps={{
|
||||||
|
input: { readOnly: true },
|
||||||
|
htmlInput: { title: selected.value },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
label={
|
||||||
|
editingDomain
|
||||||
|
? t('connections.ruleDialog.matchingDomain')
|
||||||
|
: kindLabels[kind]
|
||||||
|
}
|
||||||
|
value={editingDomain ? domainValue : (selected?.value ?? '')}
|
||||||
|
disabled={editingDomain && blocked}
|
||||||
|
onChange={(event) => {
|
||||||
|
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 && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
{t('connections.ruleDialog.invalidDomain')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
slotProps={{
|
||||||
|
input: { readOnly: !editingDomain },
|
||||||
|
htmlInput: {
|
||||||
|
title: editingDomain ? domainValue : selected?.value,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{kind === 'domain' && (
|
||||||
|
<RadioGroup
|
||||||
|
row
|
||||||
|
aria-label={t('connections.ruleDialog.domainScope')}
|
||||||
|
value={scope}
|
||||||
|
onChange={(_, value) => {
|
||||||
|
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 }}
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
value="DOMAIN"
|
||||||
|
control={<Radio size="small" />}
|
||||||
|
disabled={blocked}
|
||||||
|
label={t('connections.ruleDialog.exactDomain')}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
value="DOMAIN-SUFFIX"
|
||||||
|
control={<Radio size="small" />}
|
||||||
|
disabled={blocked}
|
||||||
|
label={t('connections.ruleDialog.allSubdomains')}
|
||||||
|
/>
|
||||||
|
</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'
|
||||||
|
? t('connections.ruleDialog.processPathHint')
|
||||||
|
: t('connections.ruleDialog.processNameHint')}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Autocomplete
|
||||||
|
size="small"
|
||||||
|
options={
|
||||||
|
unavailablePolicy && policy ? [policy, ...policies] : policies
|
||||||
|
}
|
||||||
|
getOptionLabel={(value) =>
|
||||||
|
value === 'DIRECT'
|
||||||
|
? t('proxies.components.enums.policies.DIRECT')
|
||||||
|
: value === 'REJECT'
|
||||||
|
? t('proxies.components.enums.policies.REJECT')
|
||||||
|
: value
|
||||||
|
}
|
||||||
|
getOptionDisabled={(value) => !policies.includes(value)}
|
||||||
|
value={policy}
|
||||||
|
onChange={(_, value) => setPolicy(value)}
|
||||||
|
disabled={blocked}
|
||||||
|
loading={loading}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label={t('rules.modals.editor.form.labels.proxyPolicy')}
|
||||||
|
margin="normal"
|
||||||
|
error={unavailablePolicy}
|
||||||
|
helperText={
|
||||||
|
unavailablePolicy
|
||||||
|
? t('connections.ruleDialog.policyUnavailable')
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{clashConfig && clashConfig.mode !== 'rule' && (
|
||||||
|
<Alert severity="info" sx={{ mt: 2 }}>
|
||||||
|
{t('connections.ruleDialog.ruleModeHint')}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Alert severity="error">
|
||||||
|
{error}
|
||||||
|
{rules === null && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
setLoadAttempt((attempt) => attempt + 1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('shared.actions.retry')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ display: 'block', mt: 2, overflowWrap: 'anywhere' }}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
modifying
|
||||||
|
? 'connections.ruleDialog.editDestination'
|
||||||
|
: 'connections.ruleDialog.destination',
|
||||||
|
{
|
||||||
|
name: target.profileName,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ display: 'block' }}
|
||||||
|
>
|
||||||
|
{t('connections.ruleDialog.newConnections')}
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button disabled={saving} onClick={onClose}>
|
||||||
|
{t('shared.actions.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
loading={saving}
|
||||||
|
disabled={
|
||||||
|
blocked ||
|
||||||
|
!condition ||
|
||||||
|
invalidDomain ||
|
||||||
|
!policy ||
|
||||||
|
!policies.includes(policy)
|
||||||
|
}
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
modifying
|
||||||
|
? 'connections.ruleDialog.saveAndApply'
|
||||||
|
: 'connections.ruleDialog.addAndApply',
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
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'
|
||||||
|
| 'sourceIP'
|
||||||
|
| 'destinationIP'
|
||||||
|
| 'remoteDestination'
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
const host = metadata.host?.trim() ?? ''
|
||||||
|
const path = metadata.processPath
|
||||||
|
const name = metadata.process
|
||||||
|
const domain =
|
||||||
|
host && !isIP(host) && isFQDN(host, { require_tld: false })
|
||||||
|
? { type: 'DOMAIN', value: host }
|
||||||
|
: undefined
|
||||||
|
const process = path?.trim()
|
||||||
|
? { type: 'PROCESS-PATH', value: path }
|
||||||
|
: name?.trim()
|
||||||
|
? { type: 'PROCESS-NAME', value: name }
|
||||||
|
: undefined
|
||||||
|
const source = getAddressRule(metadata.sourceIP, true)
|
||||||
|
const destination =
|
||||||
|
getAddressRule(metadata.destinationIP, false) ??
|
||||||
|
getAddressRule(metadata.remoteDestination, false)
|
||||||
|
return { domain, process, source, destination }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isConnectionRuleDomain(value: string, host: string) {
|
||||||
|
const domain = value.trim()
|
||||||
|
if (!domain || isIP(domain) || !isFQDN(domain, { require_tld: false }))
|
||||||
|
return false
|
||||||
|
const normalized = domain.toLowerCase()
|
||||||
|
const current = host.toLowerCase()
|
||||||
|
return (
|
||||||
|
normalized === current ||
|
||||||
|
(domain.split('.').length >= 2 && current.endsWith(`.${normalized}`))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRuleExtension(content: string) {
|
||||||
|
const parsed: unknown = load(content)
|
||||||
|
if (
|
||||||
|
parsed !== undefined &&
|
||||||
|
parsed !== null &&
|
||||||
|
(typeof parsed !== 'object' ||
|
||||||
|
Array.isArray(parsed) ||
|
||||||
|
Object.getPrototypeOf(parsed) !== Object.prototype)
|
||||||
|
) {
|
||||||
|
throw new Error('The rule extension must be a YAML mapping.')
|
||||||
|
}
|
||||||
|
const config = (parsed ?? {}) as Record<string, unknown>
|
||||||
|
for (const key of ['prepend', 'append', 'delete']) {
|
||||||
|
const sequence = config[key]
|
||||||
|
if (
|
||||||
|
sequence != null &&
|
||||||
|
(!Array.isArray(sequence) ||
|
||||||
|
sequence.some((item) => typeof item !== 'string'))
|
||||||
|
) {
|
||||||
|
throw new Error('The rule extension contains an invalid rule list.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuleCondition {
|
||||||
|
type: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesConnectionRule(raw: string, target: RuleCondition) {
|
||||||
|
const parts = raw.split(',').map((part) => part.trim())
|
||||||
|
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 &&
|
||||||
|
parts[1].toLowerCase() === target.value.toLowerCase()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return parts[0] === target.type && parts[1] === target.value
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConnectionRules(content: string): string[] {
|
||||||
|
const config = parseRuleExtension(content)
|
||||||
|
return [
|
||||||
|
...((config.prepend ?? []) as string[]),
|
||||||
|
...((config.append ?? []) as string[]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findConnectionRule(rules: string[], target: RuleCondition) {
|
||||||
|
for (const raw of rules) {
|
||||||
|
if (matchesConnectionRule(raw, target)) {
|
||||||
|
const [type, , policy] = raw.split(',').map((part) => part.trim())
|
||||||
|
return { type, policy }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveConnectionRule(
|
||||||
|
content: string,
|
||||||
|
target: RuleCondition,
|
||||||
|
policy: string,
|
||||||
|
): string {
|
||||||
|
const config = parseRuleExtension(content)
|
||||||
|
const rule = `${target.type},${target.value},${policy}`
|
||||||
|
let replaced = false
|
||||||
|
for (const key of ['prepend', 'append']) {
|
||||||
|
if (!Array.isArray(config[key])) continue
|
||||||
|
config[key] = (config[key] as string[]).flatMap((raw) => {
|
||||||
|
if (!matchesConnectionRule(raw, target)) return [raw]
|
||||||
|
if (replaced) return []
|
||||||
|
replaced = true
|
||||||
|
return [raw.split(',').length === 4 ? `${rule},no-resolve` : rule]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!replaced) {
|
||||||
|
config.prepend = [rule, ...((config.prepend ?? []) as string[])]
|
||||||
|
}
|
||||||
|
return dump(config, { forceQuotes: true })
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
getConnectionStartTime,
|
getConnectionStartTime,
|
||||||
getConnectionTypeLabel,
|
getConnectionTypeLabel,
|
||||||
} from './connection-row-view'
|
} from './connection-row-view'
|
||||||
|
import type { ConnectionRuleKind } from './connection-rule'
|
||||||
|
|
||||||
const ROW_HEIGHT = 40
|
const ROW_HEIGHT = 40
|
||||||
const RESIZE_HANDLE_WIDTH = 6
|
const RESIZE_HANDLE_WIDTH = 6
|
||||||
@@ -247,7 +248,7 @@ const renderCell = (
|
|||||||
interface RowComponentProps {
|
interface RowComponentProps {
|
||||||
row: IConnectionsItem
|
row: IConnectionsItem
|
||||||
columns: DisplayColumn[]
|
columns: DisplayColumn[]
|
||||||
onShowDetail: (id: string) => void
|
onShowDetail: (id: string, preferredKind?: ConnectionRuleKind) => void
|
||||||
getSnapshot: (row: IConnectionsItem) => TableRowSnapshot
|
getSnapshot: (row: IConnectionsItem) => TableRowSnapshot
|
||||||
borderColor: string
|
borderColor: string
|
||||||
virtualTop: number
|
virtualTop: number
|
||||||
@@ -263,7 +264,22 @@ const RowComponent = memo(
|
|||||||
virtualTop,
|
virtualTop,
|
||||||
}: RowComponentProps) {
|
}: RowComponentProps) {
|
||||||
const handleClick = useCallback(
|
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],
|
[onShowDetail, row.id],
|
||||||
)
|
)
|
||||||
const snapshot = getSnapshot(row)
|
const snapshot = getSnapshot(row)
|
||||||
@@ -285,6 +301,7 @@ const RowComponent = memo(
|
|||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<div
|
<div
|
||||||
key={column.field}
|
key={column.field}
|
||||||
|
data-connection-field={column.field}
|
||||||
style={{
|
style={{
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
flex: `0 0 ${column.size}px`,
|
flex: `0 0 ${column.size}px`,
|
||||||
@@ -318,7 +335,7 @@ const RowComponent = memo(
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
connections: IConnectionsItem[]
|
connections: IConnectionsItem[]
|
||||||
onShowDetail: (id: string) => void
|
onShowDetail: (id: string, preferredKind?: ConnectionRuleKind) => void
|
||||||
columnManagerOpen: boolean
|
columnManagerOpen: boolean
|
||||||
onCloseColumnManager: () => void
|
onCloseColumnManager: () => void
|
||||||
}
|
}
|
||||||
@@ -333,7 +350,8 @@ export const ConnectionTable = (props: Props) => {
|
|||||||
const onShowDetailRef = useRef(rawOnShowDetail)
|
const onShowDetailRef = useRef(rawOnShowDetail)
|
||||||
onShowDetailRef.current = rawOnShowDetail
|
onShowDetailRef.current = rawOnShowDetail
|
||||||
const onShowDetail = useCallback(
|
const onShowDetail = useCallback(
|
||||||
(id: string) => onShowDetailRef.current(id),
|
(id: string, preferredKind?: ConnectionRuleKind) =>
|
||||||
|
onShowDetailRef.current(id, preferredKind),
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|||||||
@@ -30,5 +30,39 @@
|
|||||||
"title": "الأعمدة",
|
"title": "الأعمدة",
|
||||||
"dragHandle": "Drag handle"
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ruleDialog": {
|
||||||
|
"title": "Add rule…",
|
||||||
|
"matchBy": "Match by",
|
||||||
|
"domain": "Domain",
|
||||||
|
"unavailable": "Not available",
|
||||||
|
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||||
|
"newConnections": "New connections will match the updated configuration.",
|
||||||
|
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||||
|
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||||
|
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||||
|
"addAndApply": "Add and apply",
|
||||||
|
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||||
|
"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, 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",
|
||||||
|
"domainScope": "Domain scope",
|
||||||
|
"exactDomain": "Only this domain",
|
||||||
|
"allSubdomains": "This domain and all its subdomains",
|
||||||
|
"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",
|
||||||
|
"connectionHost": "Current connection domain: {{host}}",
|
||||||
|
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||||
|
"matchingDomain": "Matching domain"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,39 @@
|
|||||||
"title": "Spalten",
|
"title": "Spalten",
|
||||||
"dragHandle": "Ziehpunkt"
|
"dragHandle": "Ziehpunkt"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ruleDialog": {
|
||||||
|
"title": "Add rule…",
|
||||||
|
"matchBy": "Match by",
|
||||||
|
"domain": "Domain",
|
||||||
|
"unavailable": "Not available",
|
||||||
|
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||||
|
"newConnections": "New connections will match the updated configuration.",
|
||||||
|
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||||
|
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||||
|
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||||
|
"addAndApply": "Add and apply",
|
||||||
|
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||||
|
"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, 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",
|
||||||
|
"domainScope": "Domain scope",
|
||||||
|
"exactDomain": "Only this domain",
|
||||||
|
"allSubdomains": "This domain and all its subdomains",
|
||||||
|
"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",
|
||||||
|
"connectionHost": "Current connection domain: {{host}}",
|
||||||
|
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||||
|
"matchingDomain": "Matching domain"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,39 @@
|
|||||||
"title": "Columns",
|
"title": "Columns",
|
||||||
"dragHandle": "Drag handle"
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ruleDialog": {
|
||||||
|
"title": "Add rule…",
|
||||||
|
"matchBy": "Match by",
|
||||||
|
"domain": "Domain",
|
||||||
|
"unavailable": "Not available",
|
||||||
|
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||||
|
"newConnections": "New connections will match the updated configuration.",
|
||||||
|
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||||
|
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||||
|
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||||
|
"addAndApply": "Add and apply",
|
||||||
|
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||||
|
"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, 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",
|
||||||
|
"domainScope": "Domain scope",
|
||||||
|
"exactDomain": "Only this domain",
|
||||||
|
"allSubdomains": "This domain and all its subdomains",
|
||||||
|
"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",
|
||||||
|
"connectionHost": "Current connection domain: {{host}}",
|
||||||
|
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||||
|
"matchingDomain": "Matching domain"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,39 @@
|
|||||||
"title": "Columnas",
|
"title": "Columnas",
|
||||||
"dragHandle": "Drag handle"
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ruleDialog": {
|
||||||
|
"title": "Add rule…",
|
||||||
|
"matchBy": "Match by",
|
||||||
|
"domain": "Domain",
|
||||||
|
"unavailable": "Not available",
|
||||||
|
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||||
|
"newConnections": "New connections will match the updated configuration.",
|
||||||
|
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||||
|
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||||
|
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||||
|
"addAndApply": "Add and apply",
|
||||||
|
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||||
|
"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, 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",
|
||||||
|
"domainScope": "Domain scope",
|
||||||
|
"exactDomain": "Only this domain",
|
||||||
|
"allSubdomains": "This domain and all its subdomains",
|
||||||
|
"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",
|
||||||
|
"connectionHost": "Current connection domain: {{host}}",
|
||||||
|
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||||
|
"matchingDomain": "Matching domain"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,39 @@
|
|||||||
"title": "ستونها",
|
"title": "ستونها",
|
||||||
"dragHandle": "Drag handle"
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ruleDialog": {
|
||||||
|
"title": "Add rule…",
|
||||||
|
"matchBy": "Match by",
|
||||||
|
"domain": "Domain",
|
||||||
|
"unavailable": "Not available",
|
||||||
|
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||||
|
"newConnections": "New connections will match the updated configuration.",
|
||||||
|
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||||
|
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||||
|
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||||
|
"addAndApply": "Add and apply",
|
||||||
|
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||||
|
"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, 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",
|
||||||
|
"domainScope": "Domain scope",
|
||||||
|
"exactDomain": "Only this domain",
|
||||||
|
"allSubdomains": "This domain and all its subdomains",
|
||||||
|
"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",
|
||||||
|
"connectionHost": "Current connection domain: {{host}}",
|
||||||
|
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||||
|
"matchingDomain": "Matching domain"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,39 @@
|
|||||||
"title": "Kolom",
|
"title": "Kolom",
|
||||||
"dragHandle": "Drag handle"
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ruleDialog": {
|
||||||
|
"title": "Add rule…",
|
||||||
|
"matchBy": "Match by",
|
||||||
|
"domain": "Domain",
|
||||||
|
"unavailable": "Not available",
|
||||||
|
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||||
|
"newConnections": "New connections will match the updated configuration.",
|
||||||
|
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||||
|
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||||
|
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||||
|
"addAndApply": "Add and apply",
|
||||||
|
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||||
|
"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, 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",
|
||||||
|
"domainScope": "Domain scope",
|
||||||
|
"exactDomain": "Only this domain",
|
||||||
|
"allSubdomains": "This domain and all its subdomains",
|
||||||
|
"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",
|
||||||
|
"connectionHost": "Current connection domain: {{host}}",
|
||||||
|
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||||
|
"matchingDomain": "Matching domain"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,39 @@
|
|||||||
"title": "列",
|
"title": "列",
|
||||||
"dragHandle": "Drag handle"
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ruleDialog": {
|
||||||
|
"title": "Add rule…",
|
||||||
|
"matchBy": "Match by",
|
||||||
|
"domain": "Domain",
|
||||||
|
"unavailable": "Not available",
|
||||||
|
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||||
|
"newConnections": "New connections will match the updated configuration.",
|
||||||
|
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||||
|
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||||
|
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||||
|
"addAndApply": "Add and apply",
|
||||||
|
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||||
|
"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, 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",
|
||||||
|
"domainScope": "Domain scope",
|
||||||
|
"exactDomain": "Only this domain",
|
||||||
|
"allSubdomains": "This domain and all its subdomains",
|
||||||
|
"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",
|
||||||
|
"connectionHost": "Current connection domain: {{host}}",
|
||||||
|
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||||
|
"matchingDomain": "Matching domain"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,39 @@
|
|||||||
"title": "열",
|
"title": "열",
|
||||||
"dragHandle": "Drag handle"
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ruleDialog": {
|
||||||
|
"title": "Add rule…",
|
||||||
|
"matchBy": "Match by",
|
||||||
|
"domain": "Domain",
|
||||||
|
"unavailable": "Not available",
|
||||||
|
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||||
|
"newConnections": "New connections will match the updated configuration.",
|
||||||
|
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||||
|
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||||
|
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||||
|
"addAndApply": "Add and apply",
|
||||||
|
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||||
|
"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, 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",
|
||||||
|
"domainScope": "Domain scope",
|
||||||
|
"exactDomain": "Only this domain",
|
||||||
|
"allSubdomains": "This domain and all its subdomains",
|
||||||
|
"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",
|
||||||
|
"connectionHost": "Current connection domain: {{host}}",
|
||||||
|
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||||
|
"matchingDomain": "Matching domain"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,39 @@
|
|||||||
"title": "Colunas",
|
"title": "Colunas",
|
||||||
"dragHandle": "Alça de arrastar"
|
"dragHandle": "Alça de arrastar"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ruleDialog": {
|
||||||
|
"title": "Add rule…",
|
||||||
|
"matchBy": "Match by",
|
||||||
|
"domain": "Domain",
|
||||||
|
"unavailable": "Not available",
|
||||||
|
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||||
|
"newConnections": "New connections will match the updated configuration.",
|
||||||
|
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||||
|
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||||
|
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||||
|
"addAndApply": "Add and apply",
|
||||||
|
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||||
|
"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, 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",
|
||||||
|
"domainScope": "Domain scope",
|
||||||
|
"exactDomain": "Only this domain",
|
||||||
|
"allSubdomains": "This domain and all its subdomains",
|
||||||
|
"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",
|
||||||
|
"connectionHost": "Current connection domain: {{host}}",
|
||||||
|
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||||
|
"matchingDomain": "Matching domain"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,39 @@
|
|||||||
"title": "Столбцы",
|
"title": "Столбцы",
|
||||||
"dragHandle": "Маркер перетаскивания"
|
"dragHandle": "Маркер перетаскивания"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ruleDialog": {
|
||||||
|
"title": "Add rule…",
|
||||||
|
"matchBy": "Match by",
|
||||||
|
"domain": "Domain",
|
||||||
|
"unavailable": "Not available",
|
||||||
|
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||||
|
"newConnections": "New connections will match the updated configuration.",
|
||||||
|
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||||
|
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||||
|
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||||
|
"addAndApply": "Add and apply",
|
||||||
|
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||||
|
"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, 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",
|
||||||
|
"domainScope": "Domain scope",
|
||||||
|
"exactDomain": "Only this domain",
|
||||||
|
"allSubdomains": "This domain and all its subdomains",
|
||||||
|
"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",
|
||||||
|
"connectionHost": "Current connection domain: {{host}}",
|
||||||
|
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||||
|
"matchingDomain": "Matching domain"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,39 @@
|
|||||||
"title": "Sütunlar",
|
"title": "Sütunlar",
|
||||||
"dragHandle": "Sürükleme tutamacı"
|
"dragHandle": "Sürükleme tutamacı"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ruleDialog": {
|
||||||
|
"title": "Add rule…",
|
||||||
|
"matchBy": "Match by",
|
||||||
|
"domain": "Domain",
|
||||||
|
"unavailable": "Not available",
|
||||||
|
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||||
|
"newConnections": "New connections will match the updated configuration.",
|
||||||
|
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||||
|
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||||
|
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||||
|
"addAndApply": "Add and apply",
|
||||||
|
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||||
|
"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, 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",
|
||||||
|
"domainScope": "Domain scope",
|
||||||
|
"exactDomain": "Only this domain",
|
||||||
|
"allSubdomains": "This domain and all its subdomains",
|
||||||
|
"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",
|
||||||
|
"connectionHost": "Current connection domain: {{host}}",
|
||||||
|
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||||
|
"matchingDomain": "Matching domain"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,39 @@
|
|||||||
"title": "Баганалар",
|
"title": "Баганалар",
|
||||||
"dragHandle": "Drag handle"
|
"dragHandle": "Drag handle"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ruleDialog": {
|
||||||
|
"title": "Add rule…",
|
||||||
|
"matchBy": "Match by",
|
||||||
|
"domain": "Domain",
|
||||||
|
"unavailable": "Not available",
|
||||||
|
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||||
|
"newConnections": "New connections will match the updated configuration.",
|
||||||
|
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||||
|
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||||
|
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||||
|
"addAndApply": "Add and apply",
|
||||||
|
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||||
|
"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, 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",
|
||||||
|
"domainScope": "Domain scope",
|
||||||
|
"exactDomain": "Only this domain",
|
||||||
|
"allSubdomains": "This domain and all its subdomains",
|
||||||
|
"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",
|
||||||
|
"connectionHost": "Current connection domain: {{host}}",
|
||||||
|
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||||
|
"matchingDomain": "Matching domain"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,39 @@
|
|||||||
"title": "列设置",
|
"title": "列设置",
|
||||||
"dragHandle": "拖拽控件"
|
"dragHandle": "拖拽控件"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ruleDialog": {
|
||||||
|
"title": "添加规则…",
|
||||||
|
"matchBy": "匹配对象",
|
||||||
|
"domain": "域名",
|
||||||
|
"unavailable": "未获取",
|
||||||
|
"destination": "添加到「{{name}}」的前置规则。",
|
||||||
|
"newConnections": "新建立的连接将按更新后的配置匹配。",
|
||||||
|
"processPathHint": "匹配此路径对应进程访问的所有目标。",
|
||||||
|
"processNameHint": "匹配所有同名进程访问的所有目标。",
|
||||||
|
"ruleModeHint": "规则分流在规则模式下生效。",
|
||||||
|
"addAndApply": "添加并应用",
|
||||||
|
"profileChanged": "当前订阅已切换,请关闭弹窗后重新添加规则。",
|
||||||
|
"applyFailed": "规则未能应用,已保留当前选择。",
|
||||||
|
"unsupportedValue": "此内容包含快捷规则不支持的字符。",
|
||||||
|
"saved": "规则已保存,配置已应用。",
|
||||||
|
"noMetadata": "此连接没有可用于设置规则的域名、进程或 IP 信息。",
|
||||||
|
"noProfile": "当前没有可编辑规则扩展的活动订阅。",
|
||||||
|
"entry": "设置规则…",
|
||||||
|
"modifyTitle": "修改规则",
|
||||||
|
"domainScope": "域名匹配范围",
|
||||||
|
"exactDomain": "仅此域名",
|
||||||
|
"allSubdomains": "此域名及所有子域名",
|
||||||
|
"policyUnavailable": "此策略已不可用,请选择其他策略。",
|
||||||
|
"editDestination": "修改「{{name}}」中的现有规则,保留排序。",
|
||||||
|
"saveAndApply": "保存并应用",
|
||||||
|
"sourceIp": "源 IP",
|
||||||
|
"destinationIp": "目标 IP",
|
||||||
|
"sourceHint": "匹配来自此 IP 的所有请求,可能包含其他应用,不限定端口。",
|
||||||
|
"destinationHint": "匹配访问此目标 IP 的请求,覆盖所有域名,不限定端口。",
|
||||||
|
"fullPath": "完整路径",
|
||||||
|
"connectionHost": "当前连接域名:{{host}}",
|
||||||
|
"invalidDomain": "请输入当前连接域名或其父域名,父域名至少包含两级。",
|
||||||
|
"matchingDomain": "匹配域名"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,39 @@
|
|||||||
"title": "欄位設定",
|
"title": "欄位設定",
|
||||||
"dragHandle": "拖曳以移動"
|
"dragHandle": "拖曳以移動"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ruleDialog": {
|
||||||
|
"title": "Add rule…",
|
||||||
|
"matchBy": "Match by",
|
||||||
|
"domain": "Domain",
|
||||||
|
"unavailable": "Not available",
|
||||||
|
"destination": "Add to the prepend rules of “{{name}}”.",
|
||||||
|
"newConnections": "New connections will match the updated configuration.",
|
||||||
|
"processPathHint": "Matches all destinations accessed by processes at this path.",
|
||||||
|
"processNameHint": "Matches all destinations accessed by processes with this name.",
|
||||||
|
"ruleModeHint": "Rule-based routing takes effect in Rule mode.",
|
||||||
|
"addAndApply": "Add and apply",
|
||||||
|
"profileChanged": "The active profile changed. Close this dialog and add the rule again.",
|
||||||
|
"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, 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",
|
||||||
|
"domainScope": "Domain scope",
|
||||||
|
"exactDomain": "Only this domain",
|
||||||
|
"allSubdomains": "This domain and all its subdomains",
|
||||||
|
"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",
|
||||||
|
"connectionHost": "Current connection domain: {{host}}",
|
||||||
|
"invalidDomain": "Enter the current domain or a parent domain with at least two labels.",
|
||||||
|
"matchingDomain": "Matching domain"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
getConnectionStartTime,
|
getConnectionStartTime,
|
||||||
useConnectionRowViews,
|
useConnectionRowViews,
|
||||||
} from '@/components/connection/connection-row-view'
|
} from '@/components/connection/connection-row-view'
|
||||||
|
import type { ConnectionRuleKind } from '@/components/connection/connection-rule'
|
||||||
import { ConnectionTable } from '@/components/connection/connection-table'
|
import { ConnectionTable } from '@/components/connection/connection-table'
|
||||||
import { useConnectionData } from '@/hooks/use-connection-data'
|
import { useConnectionData } from '@/hooks/use-connection-data'
|
||||||
import { useConnectionSetting } from '@/hooks/use-connection-setting'
|
import { useConnectionSetting } from '@/hooks/use-connection-setting'
|
||||||
@@ -161,10 +162,14 @@ const ConnectionsPage = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const showDetailById = useCallback(
|
const showDetailById = useCallback(
|
||||||
(id: string) => {
|
(id: string, preferredKind: ConnectionRuleKind = 'domain') => {
|
||||||
const connection = filterConn.find((item) => item.id === id)
|
const connection = filterConn.find((item) => item.id === id)
|
||||||
if (connection) {
|
if (connection) {
|
||||||
detailRef.current?.open(connection, connectionsType === 'closed')
|
detailRef.current?.open(
|
||||||
|
connection,
|
||||||
|
connectionsType === 'closed',
|
||||||
|
preferredKind,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[connectionsType, filterConn],
|
[connectionsType, filterConn],
|
||||||
|
|||||||
@@ -22,6 +22,38 @@ export const translationKeys = [
|
|||||||
'connections.components.actions.closeConnection',
|
'connections.components.actions.closeConnection',
|
||||||
'connections.components.columnManager.title',
|
'connections.components.columnManager.title',
|
||||||
'connections.components.columnManager.dragHandle',
|
'connections.components.columnManager.dragHandle',
|
||||||
|
'connections.ruleDialog.title',
|
||||||
|
'connections.ruleDialog.matchBy',
|
||||||
|
'connections.ruleDialog.domain',
|
||||||
|
'connections.ruleDialog.unavailable',
|
||||||
|
'connections.ruleDialog.destination',
|
||||||
|
'connections.ruleDialog.newConnections',
|
||||||
|
'connections.ruleDialog.processPathHint',
|
||||||
|
'connections.ruleDialog.processNameHint',
|
||||||
|
'connections.ruleDialog.ruleModeHint',
|
||||||
|
'connections.ruleDialog.addAndApply',
|
||||||
|
'connections.ruleDialog.profileChanged',
|
||||||
|
'connections.ruleDialog.applyFailed',
|
||||||
|
'connections.ruleDialog.unsupportedValue',
|
||||||
|
'connections.ruleDialog.saved',
|
||||||
|
'connections.ruleDialog.noMetadata',
|
||||||
|
'connections.ruleDialog.noProfile',
|
||||||
|
'connections.ruleDialog.entry',
|
||||||
|
'connections.ruleDialog.modifyTitle',
|
||||||
|
'connections.ruleDialog.domainScope',
|
||||||
|
'connections.ruleDialog.exactDomain',
|
||||||
|
'connections.ruleDialog.allSubdomains',
|
||||||
|
'connections.ruleDialog.policyUnavailable',
|
||||||
|
'connections.ruleDialog.editDestination',
|
||||||
|
'connections.ruleDialog.saveAndApply',
|
||||||
|
'connections.ruleDialog.sourceIp',
|
||||||
|
'connections.ruleDialog.destinationIp',
|
||||||
|
'connections.ruleDialog.sourceHint',
|
||||||
|
'connections.ruleDialog.destinationHint',
|
||||||
|
'connections.ruleDialog.fullPath',
|
||||||
|
'connections.ruleDialog.connectionHost',
|
||||||
|
'connections.ruleDialog.invalidDomain',
|
||||||
|
'connections.ruleDialog.matchingDomain',
|
||||||
'home.page.tooltips.lightweightMode',
|
'home.page.tooltips.lightweightMode',
|
||||||
'home.page.tooltips.manual',
|
'home.page.tooltips.manual',
|
||||||
'home.page.tooltips.settings',
|
'home.page.tooltips.settings',
|
||||||
|
|||||||
@@ -36,6 +36,40 @@ export interface TranslationResources {
|
|||||||
page: {
|
page: {
|
||||||
title: string
|
title: string
|
||||||
}
|
}
|
||||||
|
ruleDialog: {
|
||||||
|
addAndApply: string
|
||||||
|
allSubdomains: string
|
||||||
|
applyFailed: string
|
||||||
|
connectionHost: string
|
||||||
|
destination: string
|
||||||
|
destinationHint: string
|
||||||
|
destinationIp: string
|
||||||
|
domain: string
|
||||||
|
domainScope: string
|
||||||
|
editDestination: string
|
||||||
|
entry: string
|
||||||
|
exactDomain: string
|
||||||
|
fullPath: string
|
||||||
|
invalidDomain: string
|
||||||
|
matchBy: string
|
||||||
|
matchingDomain: string
|
||||||
|
modifyTitle: string
|
||||||
|
newConnections: string
|
||||||
|
noMetadata: string
|
||||||
|
noProfile: string
|
||||||
|
policyUnavailable: string
|
||||||
|
processNameHint: string
|
||||||
|
processPathHint: string
|
||||||
|
profileChanged: string
|
||||||
|
ruleModeHint: string
|
||||||
|
saveAndApply: string
|
||||||
|
saved: string
|
||||||
|
sourceHint: string
|
||||||
|
sourceIp: string
|
||||||
|
title: string
|
||||||
|
unavailable: string
|
||||||
|
unsupportedValue: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
home: {
|
home: {
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
Reference in New Issue
Block a user