spb/internetpressure
Public
TypeScript 36.3%
Python 31.8%
Go 18%
JavaScript 9.8%
Shell 1.9%
SQL 1.4%
CSS 0.5%
1'use client';23import { useState } from 'react';4import { StatusDot } from '@/components/ui/primitives';5import { adminFetch } from '@/lib/admin-fetch';6import { fmtInt, fmtPct } from '@/lib/format';7import { Time } from '@/lib/time';8import type { AdminProbe } from '@/lib/types';9import { AdminPage, ErrorNote, Field, Panel, Toast, btnCls, btnPrimary, inputCls, useAdmin } from './shared';1011const EMPTY = { probe_id: '', name: '', region: 'na-east', country: 'CA', city: '', provider: '', asn: 0, lat: 0, lon: 0 };1213export function Probes() {14 const { data, err, reload } = useAdmin<{ probes: AdminProbe[] }>('/probes');15 const [form, setForm] = useState<typeof EMPTY | null>(null);16 const [key, setKey] = useState<{ probe_id: string; key: string } | null>(null);17 const [msg, setMsg] = useState<string | null>(null);18 const [actErr, setActErr] = useState<string | null>(null);19 const [busy, setBusy] = useState(false);2021 const run = async (fn: () => Promise<unknown>, ok: string) => {22 setBusy(true);23 setActErr(null);24 try {25 await fn();26 setMsg(ok);27 reload();28 } catch (e) {29 setActErr(String(e));30 } finally {31 setBusy(false);32 setTimeout(() => setMsg(null), 3000);33 }34 };3536 return (37 <AdminPage38 title="Probes"39 desc="Register probes and manage their keys. A key is shown exactly once at creation or rotation — copy it into the agent configuration."40 right={41 <button type="button" className={btnPrimary} onClick={() => setForm({ ...EMPTY })}>42 + probe43 </button>44 }45 >46 <ErrorNote err={err ?? actErr} />47 <Toast msg={msg} />48 {key && (49 <Panel title={`Key for ${key.probe_id} — shown once`} className="mb-3 border-warn/60">50 <div className="flex flex-wrap items-center gap-2">51 <code className="num break-all rounded-[3px] bg-panel-2 px-2 py-1 text-[12px] text-ink">{key.key}</code>52 <button53 type="button"54 className={btnCls}55 onClick={() => {56 void navigator.clipboard.writeText(key.key);57 setMsg('Key copied');58 setTimeout(() => setMsg(null), 2000);59 }}60 >61 copy62 </button>63 <button type="button" className={btnCls} onClick={() => setKey(null)}>64 dismiss65 </button>66 </div>67 </Panel>68 )}69 {form && (70 <Panel title="Register probe" className="mb-3">71 <form72 className="grid grid-cols-2 gap-3 md:grid-cols-5"73 onSubmit={(e) => {74 e.preventDefault();75 void run(async () => {76 const res = await adminFetch<AdminProbe & { key: string }>('/probes', { method: 'POST', body: { ...form, asn: Number(form.asn), lat: Number(form.lat), lon: Number(form.lon) } });77 setKey({ probe_id: res.probe_id, key: res.key });78 setForm(null);79 }, 'Probe registered');80 }}81 >82 {(83 [84 ['probe_id', 'text'],85 ['name', 'text'],86 ['region', 'text'],87 ['country', 'text'],88 ['city', 'text'],89 ['provider', 'text'],90 ['asn', 'number'],91 ['lat', 'number'],92 ['lon', 'number'],93 ] as [keyof typeof EMPTY, string][]94 ).map(([k, type]) => (95 <Field key={k} label={k}>96 <input required={k !== 'city'} type={type} step="any" value={form[k]} onChange={(e) => setForm({ ...form, [k]: type === 'number' ? Number(e.target.value) : e.target.value })} className={`${inputCls} num w-full`} />97 </Field>98 ))}99 <div className="col-span-2 flex items-end gap-2 md:col-span-1">100 <button type="submit" className={btnPrimary} disabled={busy}>101 Create102 </button>103 <button type="button" className={btnCls} onClick={() => setForm(null)}>104 Cancel105 </button>106 </div>107 </form>108 </Panel>109 )}110 <div className="scroll-x">111 <table className="tbl">112 <thead>113 <tr>114 <th>Probe</th>115 <th>Status</th>116 <th>Region</th>117 <th>Location</th>118 <th>Provider</th>119 <th className="r">ASN</th>120 <th className="r">Meas./h</th>121 <th className="r">Uptime</th>122 <th>Version</th>123 <th className="r">Last seen</th>124 <th>Enabled</th>125 <th></th>126 </tr>127 </thead>128 <tbody>129 {(data?.probes ?? []).map((p) => (130 <tr key={p.probe_id} className={p.enabled === false ? 'opacity-50' : ''}>131 <td>132 <span className="num text-ink">{p.probe_id}</span> <span className="text-ink-2">{p.name}</span>133 </td>134 <td>135 <StatusDot status={p.status} />136 </td>137 <td className="num text-ink-2">{p.region}</td>138 <td className="text-ink-2">139 {p.city}, {p.country} <span className="num text-ink-3">{p.lat}, {p.lon}</span>140 </td>141 <td className="text-ink-2">{p.provider}</td>142 <td className="num r">{p.asn}</td>143 <td className="num r">{fmtInt(p.measurements_1h)}</td>144 <td className="num r">{fmtPct(p.uptime_24h, 1)}</td>145 <td className="num text-ink-2">{p.version ?? '—'}</td>146 <td className="num r text-ink-2">{p.last_seen ? <Time ts={p.last_seen} style="time" /> : '—'}</td>147 <td>148 <button type="button" className="text-[11px] uppercase tracking-[0.1em]" style={{ color: p.enabled === false ? 'var(--ink-3)' : 'var(--ok)' }} onClick={() => void run(() => adminFetch(`/probes/${encodeURIComponent(p.probe_id)}`, { method: 'PATCH', body: { enabled: p.enabled === false } }), p.enabled === false ? 'Probe enabled' : 'Probe disabled')}>149 {p.enabled === false ? 'disabled' : 'enabled'}150 </button>151 </td>152 <td className="r">153 <button154 type="button"155 className="text-[11px] text-ink-2 hover:text-ink"156 disabled={busy}157 onClick={() => {158 if (confirm(`Rotate the key of ${p.probe_id}? The agent must be reconfigured with the new key.`))159 void run(async () => {160 const res = await adminFetch<AdminProbe & { key: string }>(`/probes/${encodeURIComponent(p.probe_id)}/rotate-key`, { method: 'POST' });161 setKey({ probe_id: p.probe_id, key: res.key });162 }, 'Key rotated');163 }}164 >165 rotate key166 </button>167 </td>168 </tr>169 ))}170 </tbody>171 </table>172 </div>173 </AdminPage>174 );175}176