feat(dashboard)!: add routing table and split endpoint settings
- Add #/routing-table with one-second polling and route filters - Separate auto-connect targets from local external endpoints - Remove the obsolete interfaces navigation module BREAKING CHANGE: #/interfaces is removed.
This commit is contained in:
+109
-139
@@ -88,55 +88,6 @@ function toAddrString(item: unknown): string {
|
||||
return String(item)
|
||||
}
|
||||
|
||||
interface ConnectionItem {
|
||||
id: string
|
||||
address: string
|
||||
autoConnect: boolean
|
||||
}
|
||||
|
||||
function parseConnectionsFromConfig(config: KLALBControllerConfig | null): ConnectionItem[] {
|
||||
if (!config) return []
|
||||
const lineTable = (
|
||||
Array.isArray(config.externalEndpoints)
|
||||
? config.externalEndpoints
|
||||
: Array.isArray(config.openConnections)
|
||||
? config.openConnections
|
||||
: Array.isArray(config.LineTable)
|
||||
? config.LineTable
|
||||
: []
|
||||
).map(toAddrString)
|
||||
const connectTable = (
|
||||
Array.isArray(config.autoConnections)
|
||||
? config.autoConnections
|
||||
: Array.isArray(config.ConnectLineTable)
|
||||
? config.ConnectLineTable
|
||||
: []
|
||||
).map(toAddrString)
|
||||
|
||||
const items: ConnectionItem[] = []
|
||||
// openConnections (autoConnect: false)
|
||||
lineTable.forEach((addr, index) => {
|
||||
if (addr && addr.trim() !== '') {
|
||||
items.push({
|
||||
id: `open-${index}-${addr}`,
|
||||
address: addr,
|
||||
autoConnect: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
// autoConnections (autoConnect: true)
|
||||
connectTable.forEach((addr, index) => {
|
||||
if (addr && addr.trim() !== '') {
|
||||
items.push({
|
||||
id: `auto-${index}-${addr}`,
|
||||
address: addr,
|
||||
autoConnect: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
function SettingsForm({
|
||||
initialConfig,
|
||||
onSave,
|
||||
@@ -163,8 +114,26 @@ function SettingsForm({
|
||||
initialConfig.TUNName.trim() !== '')
|
||||
)
|
||||
|
||||
const [connections, setConnections] = useState<ConnectionItem[]>(() =>
|
||||
parseConnectionsFromConfig(initialConfig)
|
||||
const [autoConnections, setAutoConnections] = useState<string[]>(() =>
|
||||
(
|
||||
Array.isArray(initialConfig.autoConnections)
|
||||
? initialConfig.autoConnections
|
||||
: Array.isArray(initialConfig.ConnectLineTable)
|
||||
? initialConfig.ConnectLineTable
|
||||
: []
|
||||
).map(toAddrString)
|
||||
)
|
||||
|
||||
const [externalEndpoints, setExternalEndpoints] = useState<string[]>(() =>
|
||||
(
|
||||
Array.isArray(initialConfig.externalEndpoints)
|
||||
? initialConfig.externalEndpoints
|
||||
: Array.isArray(initialConfig.openConnections)
|
||||
? initialConfig.openConnections
|
||||
: Array.isArray(initialConfig.LineTable)
|
||||
? initialConfig.LineTable
|
||||
: []
|
||||
).map(toAddrString)
|
||||
)
|
||||
|
||||
const [ntpServers, setNtpServers] = useState<string[]>(() =>
|
||||
@@ -196,14 +165,12 @@ function SettingsForm({
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const cleanConnections = connections.filter((c) => c.address.trim() !== '')
|
||||
// 互斥分离:选中自动连接的进 autoConnections,未选中的进 externalEndpoints
|
||||
const updatedExternalEndpoints = cleanConnections
|
||||
.filter((c) => !c.autoConnect)
|
||||
.map((c) => c.address.trim())
|
||||
const updatedAutoConnections = cleanConnections
|
||||
.filter((c) => c.autoConnect)
|
||||
.map((c) => c.address.trim())
|
||||
const updatedAutoConnections = autoConnections
|
||||
.filter((address) => address.trim() !== '')
|
||||
.map((address) => address.trim())
|
||||
const updatedExternalEndpoints = externalEndpoints
|
||||
.filter((address) => address.trim() !== '')
|
||||
.map((address) => address.trim())
|
||||
|
||||
// 保存时同时携带新键名与历史别名,兼容新旧后端
|
||||
const denyQuery =
|
||||
@@ -248,34 +215,6 @@ function SettingsForm({
|
||||
await onSave(updatedConfig as KLALBControllerConfig)
|
||||
}
|
||||
|
||||
// --- Dynamic Connection Items Handlers ---
|
||||
const addConnection = () => {
|
||||
setConnections((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: `conn-${Date.now()}`,
|
||||
address: '',
|
||||
autoConnect: true,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
const updateConnection = (
|
||||
index: number,
|
||||
field: 'address' | 'autoConnect',
|
||||
value: string | boolean
|
||||
) => {
|
||||
setConnections((prev) => {
|
||||
const next = [...prev]
|
||||
next[index] = { ...next[index], [field]: value }
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const removeConnection = (index: number) => {
|
||||
setConnections((prev) => prev.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
// --- Generic Dynamic List Handlers ---
|
||||
const addListItem = (setter: React.Dispatch<React.SetStateAction<string[]>>) => {
|
||||
setter((prev) => [...prev, ''])
|
||||
@@ -688,72 +627,50 @@ function SettingsForm({
|
||||
|
||||
{/* Tab 3: 连接与同步列表 (Lists) */}
|
||||
<TabsContent value="lists" className="m-0 flex flex-col gap-6">
|
||||
{/* 1. Connections List (with Auto Connect toggle) */}
|
||||
{/* 1. Automatic Connection Endpoints */}
|
||||
<Card className="shadow-2xs">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||||
<div>
|
||||
<CardTitle className="text-base">连接列表</CardTitle>
|
||||
<CardDescription>
|
||||
配置开放的 Socket 连接地址,开启「自动连接」将作为启动自动连接目标,未开启则作为对外开放连接
|
||||
</CardDescription>
|
||||
<CardTitle className="text-base">与以下端点自动建立连接</CardTitle>
|
||||
<CardDescription>本节点启动时主动连接的远端端点</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={addConnection}
|
||||
onClick={() => addListItem(setAutoConnections)}
|
||||
>
|
||||
<Plus data-icon="inline-start" />
|
||||
添加连接
|
||||
添加端点
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup className="gap-3">
|
||||
{connections.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed p-6 text-center text-xs text-muted-foreground">
|
||||
暂未配置连接条目,请点击右上角「添加连接」
|
||||
<FieldGroup className="gap-2">
|
||||
{autoConnections.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
|
||||
未配置自动连接端点
|
||||
</div>
|
||||
) : (
|
||||
connections.map((item, idx) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex flex-col gap-3 rounded-lg border bg-card p-3 shadow-2xs sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<Input
|
||||
className="flex-1 font-mono text-xs"
|
||||
placeholder="tcp://kne01.yoyo250.fun:4565 或 udp://..."
|
||||
value={item.address}
|
||||
onChange={(e) =>
|
||||
updateConnection(idx, 'address', e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center justify-end gap-4">
|
||||
<Field orientation="horizontal" className="flex items-center gap-2">
|
||||
<FieldLabel htmlFor={`auto-conn-${item.id}`} className="cursor-pointer text-xs text-muted-foreground">
|
||||
自动连接
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id={`auto-conn-${item.id}`}
|
||||
checked={item.autoConnect}
|
||||
onCheckedChange={(checked) =>
|
||||
updateConnection(idx, 'autoConnect', checked)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={() => removeConnection(idx)}
|
||||
title="删除该连接"
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
autoConnections.map((endpoint, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<Input
|
||||
className="flex-1 font-mono text-xs"
|
||||
placeholder="tcp://kne01.yoyo250.fun:4565 或 udp://..."
|
||||
value={endpoint}
|
||||
onChange={(e) =>
|
||||
updateListItem(idx, e.target.value, setAutoConnections)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={() => removeListItem(idx, setAutoConnections)}
|
||||
title="删除该端点"
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -761,7 +678,60 @@ function SettingsForm({
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 2. NTP Server List */}
|
||||
{/* 2. Published External Endpoints */}
|
||||
<Card className="shadow-2xs">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||||
<div>
|
||||
<CardTitle className="text-base">本机外部端点</CardTitle>
|
||||
<CardDescription>
|
||||
本节点对外发布且可被其他节点访问的端点,不是远端连接目标
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => addListItem(setExternalEndpoints)}
|
||||
>
|
||||
<Plus data-icon="inline-start" />
|
||||
添加端点
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<FieldGroup className="gap-2">
|
||||
{externalEndpoints.length === 0 ? (
|
||||
<div className="rounded-lg border border-dashed p-4 text-center text-xs text-muted-foreground">
|
||||
未配置本机外部端点
|
||||
</div>
|
||||
) : (
|
||||
externalEndpoints.map((endpoint, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<Input
|
||||
className="flex-1 font-mono text-xs"
|
||||
placeholder="tcp://本机公网地址:4565 或 udp://..."
|
||||
value={endpoint}
|
||||
onChange={(e) =>
|
||||
updateListItem(idx, e.target.value, setExternalEndpoints)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={() => removeListItem(idx, setExternalEndpoints)}
|
||||
title="删除该端点"
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 3. NTP Server List */}
|
||||
<Card className="shadow-2xs">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-3">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user