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。 downloadText('probe.json', json)}> 下载 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 ( 取消 { setBusy(true) const ok = await onSubmit(name.trim()) setBusy(false) if (ok) onClose() }} > 确定 > } > 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} setCreating(true)}> 创建 Probe {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} setRenaming(p)}> 改名 {!p.revoked ? ( <> setPending({ kind: 'rotate', probe: p })}> 轮换密钥 setPending({ kind: 'revoke', probe: p })}> 吊销 > ) : null} setPending({ kind: 'delete', probe: p })}> 删除 创建于 {fmtTime(p.created_at)} {p.revoked_at ? `,吊销于 ${fmtTime(p.revoked_at)}` : ''} {p.connections.length > 0 ? ( Agent 版本 来源地址 连接时间 {p.connections.map((c) => ( {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} > ) }
{`monitor-probe --config /etc/monitor/probe.json`}
{json}
创建于 {fmtTime(p.created_at)} {p.revoked_at ? `,吊销于 ${fmtTime(p.revoked_at)}` : ''}