import { Download, KeyRound, Pencil, Plus, ShieldOff, Trash2 } from 'lucide-react' 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 { fmtAgo, fmtTime } from '@shared/format' import { useQuery } from '@shared/hooks' import { Chip, Confirm, CopyButton, Dialog, downloadText, Empty, ErrorNotice, Field, Spinner, useAction } from '@shared/ui' /** probe.json 只在创建和轮换密钥时返回一次:这里让管理员马上下载或复制。 */ function IdentityDialog({ identity, title, onClose }: { identity: ProbeIdentityDto; title: string; onClose: () => void }) { const json = JSON.stringify(identity, null, 2) return ( 我已保存 } >
私钥只显示这一次,关闭之后无法再找回。请现在保存 probe.json。
{`monitor-probe --config /etc/monitor/probe.json`}
查看内容
            {json}
          
) } function NameDialog({ title, initial, onClose, onSubmit }: { title: string; initial: string; onClose: () => void; onSubmit: (name: string) => Promise }) { const [name, setName] = useState(initial) const [busy, setBusy] = useState(false) return ( } > setName(e.target.value)} autoFocus /> ) } type Pending = { kind: 'rotate' | 'revoke' | 'delete'; probe: ProbeDto } export function Probes() { const probes = useQuery(() => api.get>('/api/admin/probes'), [], 8000) const { run } = useAction() const [creating, setCreating] = useState(false) const [renaming, setRenaming] = useState() const [pending, setPending] = useState() const [identity, setIdentity] = useState<{ title: string; value: ProbeIdentityDto }>() const list = probes.data?.items ?? [] const confirmText = { rotate: { title: '轮换密钥', danger: true, text: '旧密钥立即失效,所有 Agent 会断开这个 Probe 的会话。你需要把新的 probe.json 放到这台 Probe 上并重启它。' }, revoke: { title: '吊销 Probe', danger: true, text: '所有 Agent 会立刻断开并拒绝这个 Probe。记录和历史数据保留,但它不能再连接,也不能恢复。' }, delete: { title: '删除 Probe', danger: true, text: '删除这个 Probe 和它的线路。已经入库的数据按保留策略自然过期。' }, } return ( <>

Probe

{probes.loading && !probes.data ? : null}
{probes.data && list.length === 0 ? (
还没有 Probe。点击右上角「创建 Probe」,由后端生成身份,再把 probe.json 放到家宽机器上。
) : null} {list.map((p) => (
{p.name} {p.revoked ? 已吊销 : 0 ? 'ok' : ''}>{p.connections.length > 0 ? `连着 ${p.connections.length} 个 Agent` : '没有连接'}} {p.id} {!p.revoked ? ( <> ) : null}

创建于 {fmtTime(p.created_at)} {p.revoked_at ? `,吊销于 ${fmtTime(p.revoked_at)}` : ''}

{p.connections.length > 0 ? ( {p.connections.map((c) => ( ))}
Agent 版本 来源地址 连接时间
{c.agent_name} {c.version ?? '—'} {c.remote ?? '—'} {fmtTime(c.connected_since)}({fmtAgo(c.connected_since)})
) : null}
))}
{creating ? ( setCreating(false)} onSubmit={async (name) => { let created: ProbeIdentityDto | undefined const ok = await run(async () => { created = await api.post('/api/admin/probes', { name }) }) if (ok && created) { setIdentity({ title: `Probe ${name} 已创建`, value: created }) probes.reload() } return ok }} /> ) : null} {renaming ? ( setRenaming(undefined)} onSubmit={async (name) => { const ok = await run(() => api.patch(`/api/admin/probes/${renaming.id}`, { name }), '已保存') probes.reload() return ok }} /> ) : null} {pending ? ( setPending(undefined)} onConfirm={async () => { const { kind, probe } = pending if (kind === 'rotate') { let rotated: ProbeIdentityDto | undefined const ok = await run(async () => { rotated = await api.post(`/api/admin/probes/${probe.id}/rotate`) }) if (ok && rotated) setIdentity({ title: `${probe.name} 的新密钥`, value: rotated }) } else if (kind === 'revoke') { await run(() => api.post(`/api/admin/probes/${probe.id}/revoke`), '已吊销') } else { await run(() => api.del(`/api/admin/probes/${probe.id}`), '已删除') } probes.reload() }} > {confirmText[pending.kind].text} ) : null} {identity ? setIdentity(undefined)} /> : null} ) }