SPB Git forge
15commits 1branches 0releases
29.7 MBsize
maindefault branch
10 days agolast push
TypeScript 36.3% Python 31.8% Go 18% JavaScript 9.8% Shell 1.9% SQL 1.4% CSS 0.5%
7.1 KB · 176 lines tsx
Raw Blame History
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