feat: choose probe/agent links from the admin, fix the overview popover [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful

The panel decides which probes link to which agents (probes dial out): an
auto-link setting plus per-pair manual overrides with a link matrix in the
admin, and the hovered overview card is lifted above its neighbours.
This commit is contained in:
justme committed 2026-10-06 23:22:45 +08:00
1 parent 251e0cee99
commit 295aa1c5cb
32 files changed
+788 -39

No files matched your search

+79 -1
View File
@@ -2,12 +2,89 @@ import { Download, KeyRound, Pencil, Plus, ShieldOff, Trash2 } from 'lucide-reac
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { api } from '@shared/api/client'
import type { Page, ProbeDto, ProbeIdentityDto } from '@shared/api/types'
import type { LinkDto, LinkMatrixDto, Page, ProbeDto, ProbeIdentityDto } from '@shared/api/types'
import { fmtAgo, fmtTime } from '@shared/format'
import { useQuery } from '@shared/hooks'
import { NodeTag, useNodeInfos } from '@shared/nodes'
import { Chip, Confirm, CopyButton, Dialog, downloadText, Empty, ErrorNotice, Field, Spinner, useAction } from '@shared/ui'
/**
* 线路:哪个 Probe 连哪个 Agent。自动连接开着时所有 Probe 和 Agent 两两连,关着时只连手动选中的;
* 每一对都可以手动强制开或关(点一下切换,「↺」回到自动)。Probe 始终是主动拨出的一方。
*/
function LinkMatrix() {
const matrix = useQuery(() => api.get<LinkMatrixDto>('/api/admin/links'), [], 8000)
const { busy, run } = useAction()
const items = matrix.data?.items ?? []
const probes = [...new Map(items.map((i) => [i.probe_id, i.probe_name])).entries()]
const agents = [...new Map(items.map((i) => [i.agent_id, i.agent_name])).entries()]
const cell = (probeId: string, agentId: string) => items.find((i) => i.probe_id === probeId && i.agent_id === agentId)
const set = async (link: LinkDto, enabled: boolean | null) => {
await run(() => api.put(`/api/admin/links/${link.probe_id}/${link.agent_id}`, { enabled }))
matrix.reload()
}
const setAuto = async (on: boolean) => {
await run(() => api.patch('/api/admin/settings', { 'fleet.auto_link': on }), on ? '已开启自动连接' : '已关闭自动连接')
matrix.reload()
}
if (items.length === 0 && !matrix.loading) return null
return (
<div className="card">
<div className="row" style={{ marginBottom: 12 }}>
<b>线路</b>
<span className="muted">Probe 主动连接 Agent。点格子手动开关;「↺」回到自动。</span>
<label className="switch right">
<input type="checkbox" checked={matrix.data?.auto_link ?? true} disabled={busy} onChange={(e) => setAuto(e.target.checked)} />
自动连接 {matrix.data?.auto_link === false ? '关' : '开'}
</label>
</div>
<ErrorNotice error={matrix.error} />
<table className="table">
<thead>
<tr>
<th>Probe \ Agent</th>
{agents.map(([id, name]) => (
<th key={id}>{name}</th>
))}
</tr>
</thead>
<tbody>
{probes.map(([probeId, probeName]) => (
<tr key={probeId}>
<td style={{ fontWeight: 600 }}>{probeName}</td>
{agents.map(([agentId]) => {
const link = cell(probeId, agentId)
if (!link) return <td key={agentId} />
const tone = link.connected ? 'ok' : link.linked ? 'warn' : ''
const text = link.connected ? '已连接' : link.linked ? '待连接' : '不连'
return (
<td key={agentId}>
<span className="row" style={{ gap: 6 }}>
<button type="button" className="btn small" disabled={busy} title={link.linked ? '点一下:手动关闭' : '点一下:手动开启'} onClick={() => set(link, !link.linked)}>
<Chip tone={tone}>{text}</Chip>
</button>
{link.override !== null && link.override !== undefined ? (
<>
<span className="faint" title="手动设置">
手动
</span>
<button type="button" className="btn small text" disabled={busy} title="回到自动" onClick={() => set(link, null)}>
↺
</button>
</>
) : null}
</span>
</td>
)
})}
</tr>
))}
</tbody>
</table>
</div>
)
}
/** probe.json 只在创建和轮换密钥时返回一次:这里让管理员马上下载或复制。 */
function IdentityDialog({ identity, title, onClose }: { identity: ProbeIdentityDto; title: string; onClose: () => void }) {
const json = JSON.stringify(identity, null, 2)
@@ -116,6 +193,7 @@ export function Probes() {
<ErrorNotice error={probes.error} />
<div className="stack">
<LinkMatrix />
{probes.data && list.length === 0 ? (
<div className="card">
<Empty>还没有 Probe。点击右上角「创建 Probe」,由后端生成身份,再把 probe.json 放到家宽机器上。</Empty>